fix: validate CbTx chainlock height diff#7363
Conversation
Reject an out-of-range bestCLHeightDiff that would otherwise produce a pre-genesis ancestor height when combined with pindex->nHeight, returning state.Invalid(BLOCK_CONSENSUS, "bad-cbtx-cldiff") before any GetAncestor() lookup. The GetAncestor() result is also null-checked as defense-in-depth. CheckCbTxBestChainlock is exposed in specialtxman.h alongside the existing Check* prototypes so a focused unit test can cover the new boundary in src/test/evo_cbtx_tests.cpp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughThis PR introduces stricter validation for the Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
CI note: the only red item is |
|
✅ Review complete (commit 7312fff) |
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Small, well-scoped defensive fix that closes a remote crash vector in CheckCbTxBestChainlock by rejecting out-of-range bestCLHeightDiff before calling GetAncestor(), plus a defensive null check on the returned ancestor. The boundary math is correct and the change is strictly more-rejecting, so it cannot reject any historically valid block. Two minor non-blocking observations about test coverage and reject-reason granularity.
🟡 1 suggestion(s) | 💬 1 nitpick(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/test/evo_cbtx_tests.cpp`:
- [SUGGESTION] src/test/evo_cbtx_tests.cpp:56-62: Defensive GetAncestor() null-check branch is not exercised
Both test cases set bestCLHeightDiff to nHeight (5) and uint32_max, so both trip the range check at specialtxman.cpp:77 and return before the new `if (pAncestor == nullptr)` branch at specialtxman.cpp:89 is reached. The commit message highlights the null check as an intentional defense, but from the unit test's perspective that branch is currently unexercised. A case with `bestCLHeightDiff = nHeight - 1` on a standalone CBlockIndex (pprev == nullptr) would pass the range check, compute curBlockCoinbaseCLHeight = 0, and drive GetAncestor() down a chain it cannot walk, covering the defensive path.
|
|
||
| // 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"); | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: Defensive GetAncestor() null-check branch is not exercised
Both test cases set bestCLHeightDiff to nHeight (5) and uint32_max, so both trip the range check at specialtxman.cpp:77 and return before the new if (pAncestor == nullptr) branch at specialtxman.cpp:89 is reached. The commit message highlights the null check as an intentional defense, but from the unit test's perspective that branch is currently unexercised. A case with bestCLHeightDiff = nHeight - 1 on a standalone CBlockIndex (pprev == nullptr) would pass the range check, compute curBlockCoinbaseCLHeight = 0, and drive GetAncestor() down a chain it cannot walk, covering the defensive path.
source: ['claude']
| @@ -81,7 +85,11 @@ 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) { | |||
| return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-cldiff"); | |||
| } | |||
There was a problem hiding this comment.
💬 Nitpick: Both new rejection paths reuse 'bad-cbtx-cldiff'
The range check at line 78 and the defensive null check at line 90 both return state.Invalid(..., "bad-cbtx-cldiff"). They describe distinct conditions (out-of-range diff vs. missing ancestor on an in-range diff), so collapsing them into one reason makes peer-misbehavior triage and log-based diagnostics slightly less precise. Distinguishing them (e.g. bad-cbtx-cldiff-ancestor for the null case) wouldn't change consensus outcomes but would improve operability. Optional.
source: ['claude']
Issue being fixed or feature implemented
CheckCbTxBestChainlockderived an ancestor height frombestCLHeightDiffwithout 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?
bestCLHeightDiffbefore deriving and using the referenced chainlockancestor height.
bad-cbtx-cldifffor out-of-range values.evo_cbtx_testssuite.How Has This Been Tested?
Tested locally on macOS arm64:
Results:
Pre-PR review gate:
Result:
ship.Breaking Changes
None expected. This only rejects malformed CbTx chainlock metadata that
references an invalid ancestor height.
Checklist