Add prev_blockhash validation to CheckPoint#2115
Add prev_blockhash validation to CheckPoint#2115evanlinjin wants to merge 13 commits intobitcoindevkit:masterfrom
prev_blockhash validation to CheckPoint#2115Conversation
Additionally, `insert` now panics if the genesis block gets displaced (if it existed in the first place). Co-authored-by: valued mammal <valuedmammal@protonmail.com>
Make `TestLocalChain` and `ExpectedResult` generic over checkpoint data type `D`, allowing the same test infrastructure to work with both `BlockHash` and `TestBlock` types. Add `merge_chains_with_prev_blockhash` test to verify that `prev_blockhash` correctly invalidates conflicting blocks and connects disjoint chains. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive tests for CheckPoint::push error cases: - Push fails when height is not greater than current - Push fails when prev_blockhash conflicts with self - Push succeeds when prev_blockhash matches Include tests for CheckPoint::insert conflict handling: - Insert with conflicting prev_blockhash - Insert purges conflicting tail - Insert between conflicting checkpoints 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: valued mammal <valuedmammal@protonmail.com>
Use `D: Clone` instead of `D: Copy`.
Introduce `ApplyBlockError` enum with two variants: - `MissingGenesis`: genesis block is missing or would be altered - `PrevBlockhashMismatch`: block's `prev_blockhash` doesn't match expected This replaces `MissingGenesisError` in several `LocalChain` methods: - `from_blocks` - `from_changeset` - `apply_changeset` Also adds test cases for `merge_chains` with `prev_blockhash` scenarios: - Update displaces invalid block below point of agreement - Update fills gap with matching `prev_blockhash` - Cascading eviction through multiple blocks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Explain the purpose of `CheckPointEntry` and its two variants: - `Occupied`: real checkpoint at this height - `Placeholder`: implied by `prev_blockhash` from checkpoint above Also fix typo: "atleast" → "at least" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
|
This PR is actually ready for review now! |
nymius
left a comment
There was a problem hiding this comment.
Approach ACK 2c29ff1
I think the "ghost" checkpoint approach is good enough for managing the new "not-really-in-chain" checkpoints.
After detaining myself for a while trying to understand for the first time the inner workings of merge_chain, I think it is hard to read, and it's going to get hairy to maintain in the future.
I will come back with some ideas to address that.
Something similar happens with the CheckPointEntry::prev recursion. Some unit test will come handy in the future.
I liked the format of TestLocalChain, the tables were very useful.
| // Continue traversing down (if possible). | ||
| match cp.prev() { | ||
| Some(prev) => cp = prev, | ||
| None => break None, |
There was a problem hiding this comment.
When we hit None on this iterator, it means we have exhausted CheckPoint so we are beyond genesis block?
There was a problem hiding this comment.
No. The original chain may not have a genesis. We will not hit this if the original chain has a genesis block.
There was a problem hiding this comment.
The original chain may not have a genesis.
There is some no-buggy reason this could happen?
There was a problem hiding this comment.
I haven't encountered it in person, but if there's no reason to error/panic, then we shouldn't do it.
CheckPoint is an update type, chain sources might merge chains together before forming an update (not sure).
crates/core/tests/test_checkpoint.rs
Outdated
| assert_eq!(new_cp.height(), 105); | ||
| assert_eq!(new_cp.hash(), hash!("block_105")); | ||
|
|
||
| // Verify chain structure: 100, 105 |
There was a problem hiding this comment.
This is not precisely verifying chain structure. An explicit check for both CheckPoints will be more accurate.
| /// extended. | ||
| /// height order, or there are any `prev_blockhash` mismatches, then returns an `Err(..)` | ||
| /// containing the last checkpoint that would have been extended. | ||
| pub fn from_blocks(blocks: impl IntoIterator<Item = (u32, D)>) -> Result<Self, Option<Self>> { |
There was a problem hiding this comment.
Doesn't this merit an Error and Returns section?
@nymius thanks for looking into that! For this PR, it's good enough to just understand how |
Yes, not intention to turn this into a refactor. Something for another PR. |
- Clarify displaced vs purged terminology in assertion messages - Add explicit checkpoint verification instead of only counting - Remove redundant block_1 from genesis panic tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent `merge_chains` from replacing the genesis block when original and update disagree on the genesis hash. This aligns with `CheckPoint::insert` which already panics on genesis replacement. Also update the "fix blockhash before agreement point" test to operate at a non-genesis height and add a new test for conflicting genesis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| } | ||
|
|
||
| #[cfg(feature = "std")] | ||
| impl std::error::Error for ApplyBlockError {} |
There was a problem hiding this comment.
core::error::Error was stabilized in 1.81, as MSRV is 1.85 now, I think we can remove this (and all the others too)
| // Apply the changeset to produce the final merged chain. | ||
| // | ||
| // `PrevBlockhashMismatch` should never happen because the merge iteration detects | ||
| // `prev_blockhash` conflicts and resolves them by invalidating conflicting blocks (setting | ||
| // them to `None` in the changeset) before we reach this point. | ||
| fn finish<D>( | ||
| original_tip: CheckPoint<D>, | ||
| changeset: ChangeSet<D>, | ||
| ) -> Result<(CheckPoint<D>, ChangeSet<D>), CannotConnectError> | ||
| where | ||
| D: ToBlockHash + fmt::Debug + Clone, | ||
| { | ||
| let new_tip = apply_changeset_to_checkpoint(original_tip, &changeset).map_err(|err| { | ||
| debug_assert!( | ||
| matches!(err, ApplyBlockError::MissingGenesis), | ||
| "PrevBlockhashMismatch should never happen" | ||
| ); | ||
| CannotConnectError { | ||
| try_include_height: 0, | ||
| } | ||
| })?; | ||
| Ok((new_tip, changeset)) | ||
| } |
There was a problem hiding this comment.
This is not improving the comprehension in a meaningful way. I would remove this function and add the comment directly on top of the code, once moved to the end.
| // but no B exists. Update introduces A at height 1, which displaces C because | ||
| // C's `prev_blockhash` ("B") doesn't match A's hash ("A"). | ||
| // | ||
| // Note: This can only happen if chains are constructed incorrectly. |
There was a problem hiding this comment.
If this can only happen if chains are constructed incorrectly, shouldn't the expected behavior be to fail then?
| assert!( | ||
| result.unwrap_err().eq_ptr(&cp), | ||
| "should return self on error" | ||
| ); |
There was a problem hiding this comment.
To give a full picture of the expected behavior I would check the chain still contains block 100.
nymius
left a comment
There was a problem hiding this comment.
The code is addressing the referenced issues, and the changes correlate with the discussion on them.
I haven't found any conceptual concerns, but would like to hear other voices @ValuedMammal.
P.S.: I wasn't expecting to do that many comments, next time I'll push them in a single review. Sorry
| "push should succeed when prev_blockhash matches" | ||
| ); | ||
| let new_cp = result.unwrap(); | ||
| assert_eq!(new_cp.height(), 101); |
There was a problem hiding this comment.
Check that new_cp.len() is 2.
Closes #2021
Related to #2076
Replaces #2024
Replaces #2091
Description
This PR adds
prev_blockhashawareness toCheckPoint, enabling proper chain validation when merging checkpoint chains that store block headers or similar data with previous block hash information.Notes to the reviewers
This PR replaces some prior attempts:
feat(chain)!: make
CheckPointdatafield optional #2024 - where we made theCheckPoint::dataoptional - however this resulted in internal complexity and an API with annoying edge cases. The tests from this PR were still useful.feat(chain)!: Add
prev_blockhashtoToBlockHashtrait #2091 - This second attempt had some good ideas, but was distracted from the goal of EnsureCheckPointchain methods validate and link via previous blockhash #2021. I mostly reused theCheckPoint::insertimplementation of that PR.Changelog notice
Checklists
All Submissions:
New Features: