diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 375c9940d059..934a3b066399 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -109,6 +109,7 @@ BITCOIN_TESTS =\ test/descriptor_tests.cpp \ test/dynamic_activation_thresholds_tests.cpp \ test/evo_assetlocks_tests.cpp \ + test/evo_cbtx_tests.cpp \ test/evo_deterministicmns_tests.cpp \ test/evo_islock_tests.cpp \ test/evo_mnhf_tests.cpp \ diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index e154b3248acd..3d90327df3b7 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -30,9 +30,9 @@ #include #include -static bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const Consensus::Params& consensus_params, - const CChain& chain, const llmq::CQuorumManager& qman, - const chainlock::Chainlocks& chainlocks, BlockValidationState& state) +bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const Consensus::Params& consensus_params, + const CChain& chain, const llmq::CQuorumManager& qman, + const chainlock::Chainlocks& chainlocks, BlockValidationState& state) { if (cbTx.nVersion < CCbTx::Version::CLSIG_AND_BALANCE) { return true; @@ -73,6 +73,10 @@ static bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, // IsNull() doesn't exist for CBLSSignature: we assume that a valid BLS sig is non-null if (cbTx.bestCLSignature.IsValid()) { + // Reject out-of-range bestCLHeightDiff that would yield a pre-genesis ancestor height. + if (cbTx.bestCLHeightDiff >= static_cast(pindex->nHeight)) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-cldiff"); + } int curBlockCoinbaseCLHeight = pindex->nHeight - static_cast(cbTx.bestCLHeightDiff) - 1; if (best_clsig.getHeight() == curBlockCoinbaseCLHeight && best_clsig.getSig() == cbTx.bestCLSignature) { // matches our best (but outdated) clsig, no need to verify it again @@ -81,7 +85,13 @@ static bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, cached_pindex = pindex; return true; } - uint256 curBlockCoinbaseCLBlockHash = pindex->GetAncestor(curBlockCoinbaseCLHeight)->GetBlockHash(); + const CBlockIndex* pAncestor = pindex->GetAncestor(curBlockCoinbaseCLHeight); + if (pAncestor == nullptr) { + // Defense-in-depth: the range check above keeps curBlockCoinbaseCLHeight in + // [0, pindex->nHeight - 1], so GetAncestor() should never return nullptr here. + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-cldiff-ancestor"); + } + uint256 curBlockCoinbaseCLBlockHash = pAncestor->GetBlockHash(); chainlock::ChainLockSig clsig{curBlockCoinbaseCLHeight, curBlockCoinbaseCLBlockHash, cbTx.bestCLSignature}; llmq::VerifyRecSigStatus ret = chainlock::VerifyChainLock(consensus_params, chain, qman, clsig); if (ret != llmq::VerifyRecSigStatus::Valid) { diff --git a/src/evo/specialtxman.h b/src/evo/specialtxman.h index a6164069a15e..013fbfa5f1b7 100644 --- a/src/evo/specialtxman.h +++ b/src/evo/specialtxman.h @@ -15,6 +15,7 @@ class BlockValidationState; class CBlock; class CBlockIndex; class CCbTx; +class CChain; class CCoinsViewCache; class CCreditPoolManager; class CDeterministicMNList; @@ -93,6 +94,11 @@ class CSpecialTxProcessor }; +/** Validates the bestCLSignature / bestCLHeightDiff fields embedded in a CbTx payload. */ +bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const Consensus::Params& consensus_params, + const CChain& chain, const llmq::CQuorumManager& qman, + const chainlock::Chainlocks& chainlocks, BlockValidationState& state); + bool CheckProRegTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, const CCoinsViewCache& view, const ChainstateManager& chainman, TxValidationState& state, bool check_sigs); diff --git a/src/test/evo_cbtx_tests.cpp b/src/test/evo_cbtx_tests.cpp new file mode 100644 index 000000000000..363c3c01a27a --- /dev/null +++ b/src/test/evo_cbtx_tests.cpp @@ -0,0 +1,70 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +BOOST_AUTO_TEST_SUITE(evo_cbtx_tests) + +// Out-of-range bestCLHeightDiff (>= pindex->nHeight) must be rejected with +// "bad-cbtx-cldiff" so that the subsequent GetAncestor() call sees a valid height. +// +// The defensive nullptr branch after GetAncestor() returns "bad-cbtx-cldiff-ancestor". +// That branch is unreachable in practice (the range check guarantees the requested +// ancestor height is in [0, pindex->nHeight - 1], for which GetAncestor() never returns +// nullptr) and cannot be exercised from a unit test: a fake CBlockIndex with no pprev +// would trip GetAncestor()'s `assert(pprev)` while walking, not return nullptr. +BOOST_FIXTURE_TEST_CASE(check_cbtx_best_chainlock_rejects_excessive_height_diff, RegTestingSetup) +{ + const auto& consensus_params = Params().GetConsensus(); + const auto& chain = m_node.chainman->ActiveChain(); + auto& qman = *Assert(m_node.llmq_ctx)->qman; + auto& chainlocks = *Assert(m_node.chainlocks); + + // Standalone fake block index with no predecessor, so the prevBlockCoinbaseChainlock + // branch is skipped and the validation path under test is reached directly. + CBlockIndex pindex; + pindex.nHeight = 5; + + // A structurally-valid BLS signature is required for the IsValid() guard. + CBLSSecretKey sk; + sk.MakeNewKey(); + const bool legacy_scheme = bls::bls_legacy_scheme.load(); + CBLSSignature valid_sig = sk.Sign(uint256::ONE, legacy_scheme); + BOOST_REQUIRE(valid_sig.IsValid()); + + CCbTx cbTx; + cbTx.nVersion = CCbTx::Version::CLSIG_AND_BALANCE; + cbTx.bestCLSignature = valid_sig; + + // bestCLHeightDiff == nHeight: lower boundary of the rejected range. + cbTx.bestCLHeightDiff = static_cast(pindex.nHeight); + BlockValidationState state; + BOOST_CHECK(!CheckCbTxBestChainlock(cbTx, &pindex, consensus_params, chain, qman, chainlocks, state)); + BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-cbtx-cldiff"); + + // Upper boundary: uint32_t max. + cbTx.bestCLHeightDiff = std::numeric_limits::max(); + BlockValidationState state_big; + BOOST_CHECK(!CheckCbTxBestChainlock(cbTx, &pindex, consensus_params, chain, qman, chainlocks, state_big)); + BOOST_CHECK_EQUAL(state_big.GetRejectReason(), "bad-cbtx-cldiff"); +} + +BOOST_AUTO_TEST_SUITE_END()