Skip to content

Add prev_blockhash validation to CheckPoint#2115

Open
evanlinjin wants to merge 13 commits intobitcoindevkit:masterfrom
evanlinjin:cp_entry
Open

Add prev_blockhash validation to CheckPoint#2115
evanlinjin wants to merge 13 commits intobitcoindevkit:masterfrom
evanlinjin:cp_entry

Conversation

@evanlinjin
Copy link
Member

@evanlinjin evanlinjin commented Feb 5, 2026

Closes #2021
Related to #2076
Replaces #2024
Replaces #2091

Description

This PR adds prev_blockhash awareness to CheckPoint, 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:

Changelog notice

Added:
- `ToBlockHash::prev_blockhash()` - optional method to expose previous block hash
- `CheckPointEntry` - new type for iterating with `prev_blockhash` awareness, yielding "placeholder" entries for heights inferred from `prev_blockhash`
- `ApplyBlockError` - this is a new error type with two variants; `MissingGenesis` and `PrevBlockhashMismatch`. The second variant is a new error case introduced by `prev_blockhash` awareness.

Changed:
- `CheckPoint::push` - now errors when `prev_blockhash` conflicts with current tip (contiguous heights)
- `CheckPoint::insert` - now evicts/displaces checkpoints on `prev_blockhash` conflict
- `merge_chains` - now validates `prev_blockhash` consistency when merging
- `LocalChain<D>` generic parameter - relaxed constraint to `D: Clone` instead of `D: Copy`.

Checklists

All Submissions:

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

evanlinjin and others added 4 commits February 5, 2026 08:37
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>
@evanlinjin evanlinjin self-assigned this Feb 5, 2026
@evanlinjin evanlinjin added the bug Something isn't working label Feb 5, 2026
@evanlinjin evanlinjin moved this to Needs Review in BDK Chain Feb 5, 2026
@evanlinjin evanlinjin marked this pull request as draft February 5, 2026 14:54
evanlinjin and others added 4 commits February 5, 2026 23:57
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>
evanlinjin and others added 3 commits February 6, 2026 09:37
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>
@evanlinjin evanlinjin marked this pull request as ready for review February 7, 2026 10:49
@evanlinjin
Copy link
Member Author

This PR is actually ready for review now!

Copy link
Contributor

@nymius nymius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we hit None on this iterator, it means we have exhausted CheckPoint so we are beyond genesis block?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The original chain may not have a genesis. We will not hit this if the original chain has a genesis block.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original chain may not have a genesis.

There is some no-buggy reason this could happen?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

assert_eq!(new_cp.height(), 105);
assert_eq!(new_cp.hash(), hash!("block_105"));

// Verify chain structure: 100, 105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this merit an Error and Returns section?

@evanlinjin
Copy link
Member Author

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.

@nymius thanks for looking into that! For this PR, it's good enough to just understand how merge_chains is meant to function, and ensure our tests covers all cases and edge cases. Just treat it like a black box for now and we can look into it later.

@nymius
Copy link
Contributor

nymius commented Feb 13, 2026

@nymius thanks for looking into that! For this PR, it's good enough to just understand how merge_chains is meant to function, and ensure our tests covers all cases and edge cases. Just treat it like a black box for now and we can look into it later.

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 {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

Ensure CheckPoint chain methods validate and link via previous blockhash

3 participants

Comments