Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/evo/cbtx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre
llmq::CFinalCommitment qc;
uint256 minedBlockHash;
bool found = llmq::quorumBlockProcessor->GetMinedCommitment(p.first, p2->GetBlockHash(), qc, minedBlockHash);
assert(found);
if (!found) return state.DoS(100, false, REJECT_INVALID, "commitment-not-found");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat missing stored commitments as internal errors

When the Evo DB's inverse-height index points to a missing commitment record—the exact local inconsistency constructed by the new regression test—this failure is unrelated to the candidate block or its sending peer. Returning state.DoS(100, ...) makes CheckCbTxMerkleRoots classify an otherwise valid block as consensus-invalid and allows BlockChecked to penalize the peer; it can also leave the block marked invalid until manual recovery. Report this as state.Error(...) so the node surfaces its local database failure without blaming network input.

Useful? React with 👍 / 👎.

v.emplace_back(::SerializeHash(qc));
hashCount++;
}
Expand All @@ -247,7 +247,7 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre
if (tx->nVersion == 3 && tx->nType == TRANSACTION_QUORUM_COMMITMENT) {
llmq::CFinalCommitmentTxPayload qc;
if (!GetTxPayload(*tx, qc)) {
assert(false);
return state.DoS(100, false, REJECT_INVALID, "bad-qc-payload");
}
if (qc.commitment.IsNull()) {
continue;
Expand All @@ -260,7 +260,9 @@ bool CalcCbTxMerkleRootQuorums(const CBlock& block, const CBlockIndex* pindexPre
}
v.emplace_back(qcHash);
hashCount++;
assert(cmp::less_equal(v.size(), params.signingActiveQuorumCount));
if (cmp::greater(v.size(), params.signingActiveQuorumCount)) {
return state.DoS(100, false, REJECT_INVALID, "excess-quorums");
}
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/evo/deterministicmns.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
if (tx.nType == TRANSACTION_PROVIDER_REGISTER) {
CProRegTx proTx;
if (!GetTxPayload(tx, proTx)) {
assert(false); // this should have been handled already
return _state.DoS(100, false, REJECT_INVALID, "bad-protx-payload");
}

auto dmn = std::make_shared<CDeterministicMN>();
Expand Down Expand Up @@ -747,7 +747,7 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
} else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_SERVICE) {
CProUpServTx proTx;
if (!GetTxPayload(tx, proTx)) {
assert(false); // this should have been handled already
return _state.DoS(100, false, REJECT_INVALID, "bad-protx-payload");
}

if (newList.HasUniqueProperty(proTx.addr) && newList.GetUniquePropertyMN(proTx.addr)->proTxHash != proTx.proTxHash) {
Expand Down Expand Up @@ -784,7 +784,7 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
} else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_REGISTRAR) {
CProUpRegTx proTx;
if (!GetTxPayload(tx, proTx)) {
assert(false); // this should have been handled already
return _state.DoS(100, false, REJECT_INVALID, "bad-protx-payload");
}

CDeterministicMNCPtr dmn = newList.GetMN(proTx.proTxHash);
Expand Down Expand Up @@ -815,7 +815,7 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
} else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_REVOKE) {
CProUpRevTx proTx;
if (!GetTxPayload(tx, proTx)) {
assert(false); // this should have been handled already
return _state.DoS(100, false, REJECT_INVALID, "bad-protx-payload");
}

CDeterministicMNCPtr dmn = newList.GetMN(proTx.proTxHash);
Expand All @@ -836,7 +836,7 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C
} else if (tx.nType == TRANSACTION_QUORUM_COMMITMENT) {
llmq::CFinalCommitmentTxPayload qc;
if (!GetTxPayload(tx, qc)) {
assert(false); // this should have been handled already
return _state.DoS(100, false, REJECT_INVALID, "bad-qc-payload");
}
if (!qc.commitment.IsNull()) {
const auto& params = Params().GetConsensus().llmqs.at((Consensus::LLMQType)qc.commitment.llmqType);
Expand Down
1 change: 1 addition & 0 deletions src/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ add_executable(test_firo
${CMAKE_CURRENT_SOURCE_DIR}/evospork_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/evo_deterministicmns_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/evo_simplifiedmns_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/evo_validation_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/progpow_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/bls_tests.cpp
${CMAKE_CURRENT_SOURCE_DIR}/sparkmessage_tests.cpp
Expand Down
141 changes: 141 additions & 0 deletions src/test/evo_validation_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright (c) 2026 The Firo Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include "test/test_bitcoin.h"

#include "chainparams.h"
#include "compat/endian.h"
#include "consensus/validation.h"
#include "evo/cbtx.h"
#include "evo/deterministicmns.h"
#include "evo/evodb.h"
#include "llmq/quorums_blockprocessor.h"
#include "primitives/block.h"
#include "primitives/transaction.h"
#include "validation.h"

#include <array>
#include <boost/test/unit_test.hpp>
#include <cstdint>
#include <limits>
#include <string>
#include <tuple>

namespace
{

struct EvoValidationTestingSetup : BasicTestingSetup {
std::array<uint256, 101> blockHashes;
std::array<CBlockIndex, 101> blockIndexes;
llmq::CQuorumBlockProcessor processor;
llmq::CQuorumBlockProcessor* previousProcessor;

EvoValidationTestingSetup() : BasicTestingSetup(CBaseChainParams::REGTEST),
processor(*evoDb),
previousProcessor(llmq::quorumBlockProcessor)
{
for (size_t i = 0; i < blockIndexes.size(); ++i) {
const uint32_t encodedHeight = static_cast<uint32_t>(i + 1);
for (size_t byte = 0; byte < sizeof(encodedHeight); ++byte) {
blockHashes[i].begin()[byte] = static_cast<unsigned char>(encodedHeight >> (byte * 8));
}

blockIndexes[i].nHeight = static_cast<int>(i);
blockIndexes[i].pprev = i == 0 ? nullptr : &blockIndexes[i - 1];
blockIndexes[i].phashBlock = &blockHashes[i];
blockIndexes[i].BuildSkip();
}

llmq::quorumBlockProcessor = &processor;
}

~EvoValidationTestingSetup()
{
llmq::quorumBlockProcessor = previousProcessor;
}

const CBlockIndex* Tip() const
{
return &blockIndexes.back();
}
};

} // namespace

BOOST_AUTO_TEST_SUITE(evo_validation_tests)

static CTransactionRef MakeMalformedSpecialTx(int32_t type)
{
CMutableTransaction tx;
tx.nVersion = 3;
tx.nType = type;
return MakeTransactionRef(tx);
}

static CBlock MakeMalformedSpecialTxBlock(int32_t type)
{
CBlock block;
block.vtx.emplace_back(MakeTransactionRef(CMutableTransaction()));
block.vtx.emplace_back(MakeMalformedSpecialTx(type));
return block;
}

BOOST_FIXTURE_TEST_CASE(evo_helpers_reject_invalid_state_without_asserting, EvoValidationTestingSetup)
{
const CBlockIndex* pindexPrev = Tip();

const std::array<int32_t, 5> specialTxTypes{
TRANSACTION_PROVIDER_REGISTER,
TRANSACTION_PROVIDER_UPDATE_SERVICE,
TRANSACTION_PROVIDER_UPDATE_REGISTRAR,
TRANSACTION_PROVIDER_UPDATE_REVOKE,
TRANSACTION_QUORUM_COMMITMENT,
};

for (const int32_t type : specialTxTypes) {
CValidationState state;
CDeterministicMNList list;
const CBlock block = MakeMalformedSpecialTxBlock(type);
bool result;
{
LOCK(deterministicMNManager->cs);
result = deterministicMNManager->BuildNewListFromBlock(block, pindexPrev, state, list, false);
}

BOOST_CHECK(!result);
BOOST_CHECK(state.IsInvalid());
BOOST_CHECK_EQUAL(state.GetRejectReason(), type == TRANSACTION_QUORUM_COMMITMENT ? "bad-qc-payload" : "bad-protx-payload");
}

{
CValidationState state;
uint256 merkleRoot;
const CBlock block = MakeMalformedSpecialTxBlock(TRANSACTION_QUORUM_COMMITMENT);

BOOST_CHECK(!CalcCbTxMerkleRootQuorums(block, pindexPrev, merkleRoot, state));
BOOST_CHECK(state.IsInvalid());
BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-qc-payload");
}

const auto llmqType = Params().GetConsensus().llmqs.begin()->first;
const uint32_t minedHeight = static_cast<uint32_t>(pindexPrev->nHeight);
const auto inverseHeightKey = std::make_tuple(
std::string("q_mcih"),
static_cast<uint8_t>(llmqType),
htobe32(std::numeric_limits<uint32_t>::max() - minedHeight));
BOOST_REQUIRE(!evoDb->Exists(inverseHeightKey));

auto dbTransaction = evoDb->BeginTransaction();
evoDb->Write(inverseHeightKey, 0);

CValidationState state;
uint256 merkleRoot;
CBlock block;
BOOST_CHECK(!CalcCbTxMerkleRootQuorums(block, pindexPrev, merkleRoot, state));
BOOST_CHECK(state.IsInvalid());
BOOST_CHECK_EQUAL(state.GetRejectReason(), "commitment-not-found");
dbTransaction->Rollback();
}

BOOST_AUTO_TEST_SUITE_END()
Loading