diff --git a/src/evo/cbtx.cpp b/src/evo/cbtx.cpp index 313039117b..94ca1ca9ad 100644 --- a/src/evo/cbtx.cpp +++ b/src/evo/cbtx.cpp @@ -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"); v.emplace_back(::SerializeHash(qc)); hashCount++; } @@ -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; @@ -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"); + } } } diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 6ca55554a2..142ae5be3f 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -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(); @@ -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) { @@ -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); @@ -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); @@ -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); diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 6408280c81..2aead67f24 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -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 diff --git a/src/test/evo_validation_tests.cpp b/src/test/evo_validation_tests.cpp new file mode 100644 index 0000000000..0a2ee189cf --- /dev/null +++ b/src/test/evo_validation_tests.cpp @@ -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 +#include +#include +#include +#include +#include + +namespace +{ + +struct EvoValidationTestingSetup : BasicTestingSetup { + std::array blockHashes; + std::array 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(i + 1); + for (size_t byte = 0; byte < sizeof(encodedHeight); ++byte) { + blockHashes[i].begin()[byte] = static_cast(encodedHeight >> (byte * 8)); + } + + blockIndexes[i].nHeight = static_cast(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 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(pindexPrev->nHeight); + const auto inverseHeightKey = std::make_tuple( + std::string("q_mcih"), + static_cast(llmqType), + htobe32(std::numeric_limits::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()