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
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
18 changes: 14 additions & 4 deletions src/evo/specialtxman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
#include <util/system.h>
#include <validation.h>

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;
Expand Down Expand Up @@ -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<uint32_t>(pindex->nHeight)) {
return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-cldiff");
}
int curBlockCoinbaseCLHeight = pindex->nHeight - static_cast<int>(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
Expand All @@ -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");
}
Comment thread
thepastaclaw marked this conversation as resolved.
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) {
Expand Down
6 changes: 6 additions & 0 deletions src/evo/specialtxman.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class BlockValidationState;
class CBlock;
class CBlockIndex;
class CCbTx;
class CChain;
class CCoinsViewCache;
class CCreditPoolManager;
class CDeterministicMNList;
Expand Down Expand Up @@ -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<const CBlockIndex*> pindexPrev,
CDeterministicMNManager& dmnman, const CCoinsViewCache& view, const ChainstateManager& chainman,
TxValidationState& state, bool check_sigs);
Expand Down
70 changes: 70 additions & 0 deletions src/test/evo_cbtx_tests.cpp
Original file line number Diff line number Diff line change
@@ -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 <test/util/setup_common.h>

#include <bls/bls.h>
#include <chain.h>
#include <chainlock/chainlock.h>
#include <chainparams.h>
#include <consensus/validation.h>
#include <evo/cbtx.h>
#include <evo/specialtxman.h>
#include <llmq/context.h>
#include <llmq/quorumsman.h>
#include <uint256.h>
#include <validation.h>

#include <cstdint>
#include <limits>

#include <boost/test/unit_test.hpp>

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<uint32_t>(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<uint32_t>::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");
}
Comment thread
thepastaclaw marked this conversation as resolved.

BOOST_AUTO_TEST_SUITE_END()
Loading