Skip to content

Commit 7e32409

Browse files
Merge #7363: fix: validate CbTx chainlock height diff
c93049f test: clarify CbTx chainlock diff ancestor guard (PastaClaw) c892349 fix: validate bestCLHeightDiff range in CheckCbTxBestChainlock (PastaClaw) Pull request description: ## Issue being fixed or feature implemented `CheckCbTxBestChainlock` derived an ancestor height from `bestCLHeightDiff` without first ensuring the derived height was in range. This PR adds an explicit consensus rejection for out-of-range CbTx chainlock height diffs before ancestor lookup. ## What was done? - Validate `bestCLHeightDiff` before deriving and using the referenced chainlock ancestor height. - Return `bad-cbtx-cldiff` for out-of-range values. - Add a defensive null check around the ancestor lookup. - Add focused unit coverage for boundary values in a new `evo_cbtx_tests` suite. ## How Has This Been Tested? Tested locally on macOS arm64: ```text cd src && make -j6 test/test_dash ./test/test_dash --run_test=evo_cbtx_tests ./test/test_dash '--run_test=evo_*' ``` Results: ```text make -j6 test/test_dash: pass ./test/test_dash --run_test=evo_cbtx_tests: 1/1 pass ./test/test_dash '--run_test=evo_*': 38/38 pass ``` Pre-PR review gate: ```text code-review dashpay/dash upstream/develop review/fix-cbtx-chainlock-height-bound "Pre-PR review: validate CbTx bestCLHeightDiff range before ancestor lookup and add focused regression tests" ``` Result: `ship`. ## Breaking Changes None expected. This only rejects malformed CbTx chainlock metadata that references an invalid ancestor height. ## Checklist - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [ ] I have assigned this pull request to a milestone ACKs for top commit: UdjinM6: utACK c93049f Tree-SHA512: 83ce8c3f262930001684a96face20df623f6123f1a9ad169c8c4f0a019a971b54452d19b1b13f3939371186cecd52091b6e20cbde024df088abbc47b34dbb40a
2 parents 2a90f74 + c93049f commit 7e32409

4 files changed

Lines changed: 91 additions & 4 deletions

File tree

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ BITCOIN_TESTS =\
109109
test/descriptor_tests.cpp \
110110
test/dynamic_activation_thresholds_tests.cpp \
111111
test/evo_assetlocks_tests.cpp \
112+
test/evo_cbtx_tests.cpp \
112113
test/evo_deterministicmns_tests.cpp \
113114
test/evo_islock_tests.cpp \
114115
test/evo_mnhf_tests.cpp \

src/evo/specialtxman.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ static bool SetStateVersion(CDeterministicMNState& state_mn, uint16_t nVersion,
9393
return true;
9494
}
9595

96-
static bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const Consensus::Params& consensus_params,
97-
const CChain& chain, const llmq::CQuorumManager& qman,
98-
const chainlock::Chainlocks& chainlocks, BlockValidationState& state)
96+
bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const Consensus::Params& consensus_params,
97+
const CChain& chain, const llmq::CQuorumManager& qman,
98+
const chainlock::Chainlocks& chainlocks, BlockValidationState& state)
9999
{
100100
if (cbTx.nVersion < CCbTx::Version::CLSIG_AND_BALANCE) {
101101
return true;
@@ -136,6 +136,10 @@ static bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex,
136136

137137
// IsNull() doesn't exist for CBLSSignature: we assume that a valid BLS sig is non-null
138138
if (cbTx.bestCLSignature.IsValid()) {
139+
// Reject out-of-range bestCLHeightDiff that would yield a pre-genesis ancestor height.
140+
if (cbTx.bestCLHeightDiff >= static_cast<uint32_t>(pindex->nHeight)) {
141+
return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-cldiff");
142+
}
139143
int curBlockCoinbaseCLHeight = pindex->nHeight - static_cast<int>(cbTx.bestCLHeightDiff) - 1;
140144
if (best_clsig.getHeight() == curBlockCoinbaseCLHeight && best_clsig.getSig() == cbTx.bestCLSignature) {
141145
// matches our best (but outdated) clsig, no need to verify it again
@@ -144,7 +148,13 @@ static bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex,
144148
cached_pindex = pindex;
145149
return true;
146150
}
147-
uint256 curBlockCoinbaseCLBlockHash = pindex->GetAncestor(curBlockCoinbaseCLHeight)->GetBlockHash();
151+
const CBlockIndex* pAncestor = pindex->GetAncestor(curBlockCoinbaseCLHeight);
152+
if (pAncestor == nullptr) {
153+
// Defense-in-depth: the range check above keeps curBlockCoinbaseCLHeight in
154+
// [0, pindex->nHeight - 1], so GetAncestor() should never return nullptr here.
155+
return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-cldiff-ancestor");
156+
}
157+
uint256 curBlockCoinbaseCLBlockHash = pAncestor->GetBlockHash();
148158
chainlock::ChainLockSig clsig{curBlockCoinbaseCLHeight, curBlockCoinbaseCLBlockHash, cbTx.bestCLSignature};
149159
llmq::VerifyRecSigStatus ret = chainlock::VerifyChainLock(consensus_params, chain, qman, clsig);
150160
if (ret != llmq::VerifyRecSigStatus::Valid) {

src/evo/specialtxman.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class BlockValidationState;
1515
class CBlock;
1616
class CBlockIndex;
1717
class CCbTx;
18+
class CChain;
1819
class CCoinsViewCache;
1920
class CCreditPoolManager;
2021
class CDeterministicMNList;
@@ -93,6 +94,11 @@ class CSpecialTxProcessor
9394
};
9495

9596

97+
/** Validates the bestCLSignature / bestCLHeightDiff fields embedded in a CbTx payload. */
98+
bool CheckCbTxBestChainlock(const CCbTx& cbTx, const CBlockIndex* pindex, const Consensus::Params& consensus_params,
99+
const CChain& chain, const llmq::CQuorumManager& qman,
100+
const chainlock::Chainlocks& chainlocks, BlockValidationState& state);
101+
96102
bool CheckProRegTx(const CTransaction& tx, gsl::not_null<const CBlockIndex*> pindexPrev,
97103
CDeterministicMNManager& dmnman, const CCoinsViewCache& view, const ChainstateManager& chainman,
98104
TxValidationState& state, bool check_sigs);

src/test/evo_cbtx_tests.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) 2026 The Dash Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <test/util/setup_common.h>
6+
7+
#include <bls/bls.h>
8+
#include <chain.h>
9+
#include <chainlock/chainlock.h>
10+
#include <chainparams.h>
11+
#include <consensus/validation.h>
12+
#include <evo/cbtx.h>
13+
#include <evo/specialtxman.h>
14+
#include <llmq/context.h>
15+
#include <llmq/quorumsman.h>
16+
#include <uint256.h>
17+
#include <validation.h>
18+
19+
#include <cstdint>
20+
#include <limits>
21+
22+
#include <boost/test/unit_test.hpp>
23+
24+
BOOST_AUTO_TEST_SUITE(evo_cbtx_tests)
25+
26+
// Out-of-range bestCLHeightDiff (>= pindex->nHeight) must be rejected with
27+
// "bad-cbtx-cldiff" so that the subsequent GetAncestor() call sees a valid height.
28+
//
29+
// The defensive nullptr branch after GetAncestor() returns "bad-cbtx-cldiff-ancestor".
30+
// That branch is unreachable in practice (the range check guarantees the requested
31+
// ancestor height is in [0, pindex->nHeight - 1], for which GetAncestor() never returns
32+
// nullptr) and cannot be exercised from a unit test: a fake CBlockIndex with no pprev
33+
// would trip GetAncestor()'s `assert(pprev)` while walking, not return nullptr.
34+
BOOST_FIXTURE_TEST_CASE(check_cbtx_best_chainlock_rejects_excessive_height_diff, RegTestingSetup)
35+
{
36+
const auto& consensus_params = Params().GetConsensus();
37+
const auto& chain = m_node.chainman->ActiveChain();
38+
auto& qman = *Assert(m_node.llmq_ctx)->qman;
39+
auto& chainlocks = *Assert(m_node.chainlocks);
40+
41+
// Standalone fake block index with no predecessor, so the prevBlockCoinbaseChainlock
42+
// branch is skipped and the validation path under test is reached directly.
43+
CBlockIndex pindex;
44+
pindex.nHeight = 5;
45+
46+
// A structurally-valid BLS signature is required for the IsValid() guard.
47+
CBLSSecretKey sk;
48+
sk.MakeNewKey();
49+
const bool legacy_scheme = bls::bls_legacy_scheme.load();
50+
CBLSSignature valid_sig = sk.Sign(uint256::ONE, legacy_scheme);
51+
BOOST_REQUIRE(valid_sig.IsValid());
52+
53+
CCbTx cbTx;
54+
cbTx.nVersion = CCbTx::Version::CLSIG_AND_BALANCE;
55+
cbTx.bestCLSignature = valid_sig;
56+
57+
// bestCLHeightDiff == nHeight: lower boundary of the rejected range.
58+
cbTx.bestCLHeightDiff = static_cast<uint32_t>(pindex.nHeight);
59+
BlockValidationState state;
60+
BOOST_CHECK(!CheckCbTxBestChainlock(cbTx, &pindex, consensus_params, chain, qman, chainlocks, state));
61+
BOOST_CHECK_EQUAL(state.GetRejectReason(), "bad-cbtx-cldiff");
62+
63+
// Upper boundary: uint32_t max.
64+
cbTx.bestCLHeightDiff = std::numeric_limits<uint32_t>::max();
65+
BlockValidationState state_big;
66+
BOOST_CHECK(!CheckCbTxBestChainlock(cbTx, &pindex, consensus_params, chain, qman, chainlocks, state_big));
67+
BOOST_CHECK_EQUAL(state_big.GetRejectReason(), "bad-cbtx-cldiff");
68+
}
69+
70+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)