Skip to content

Conversation

@ValuedMammal
Copy link
Collaborator

@ValuedMammal ValuedMammal commented Jan 7, 2026

This PR adds prev_blockhash method to ToBlockHash trait.

prev_blockhash returns the hash of the previous block data, if it is known, and None otherwise. Implemented prev_blockhash for Header, which returns the header's previous block hash.

We now require checkpoints to link via their prev_blockhash relation.

Fixed CheckPoint::push to now check that if the height being pushed directly follows the current tip height, the previous hash of data is the same as the current tip hash, (provided that the data's prev_blockhash is known). This is necessary to prevent inconsistent or invalid chains from being constructed.

Fixed insert to displace conflicting blocks by reverting to a previous base if it's found to conflict with the inserted data. Also avoid a panic in insert in case push returns an error by returning the last successfully updated tip.

In apply_changeset{_to checkpoint} we now propagate the error from extend. Now that apply_changeset can fail due to inconsistent data in addition to MissingGenesisError, we need to propagate the error rather than expect that the chain update can always succeed.

fix #2095
fix #2021

Changelog notice

Changed

  • Changed LocalChain::from_blocks to return Result<Self, Option<CheckPoint<D>>
  • Changed LocalChain::from_changeset to return Result<Option<Self>, CannotConnectError)
  • Changed LocalChain::apply_changeset to return Result<(), CannotConnectError>

Checklists

All Submissions:

New Features:

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

Bugfixes:

  • This pull request breaks the existing API
  • I've added tests to reproduce the issue which are now passing
  • I'm linking the issue being fixed by this PR

@evanlinjin
Copy link
Member

How does this relate to PR #2024? That PR addresses the same issue and handles validation in insert() and merge_chains as well. As written, this only validates push() with contiguous heights - insert() and merge_chains can still create inconsistent chains. Is this intended as a simpler alternative approach, or a foundation for the work in #2024?

I'm also wondering if this is meant to show that we can achieve simpler validation by not introducing optional data fields. However, there is a gap: without tracking "ghost checkpoints" (positions inferred from prev_blockhash but not yet populated), we can't properly validate in insert() or merge_chains().

For example, if I insert a checkpoint at height 100 with prev_blockhash = X, and later insert at height 99 with blockhash = Y where X != Y, we need to detect that conflict. Either:

The current PR doesn't address either path, so I don't think it fully resolves #2021. Is there a different approach you have in mind for handling these cases?

@ValuedMammal
Copy link
Collaborator Author

Here we don't intend to make the CheckPoint::data field optional, instead displacing a block will just remove it from the chain.

It's missing coverage for when insert properly displaces a conflicting block at the previous height.

The previous BlockId inferred by the current chain could in theory be implemented directly on the CheckPoint like this. I haven't tackled the issues with merge_chains yet.

/// Get the previous [`BlockId`] of this checkpoint.
///
/// I.e. the height and hash of the block immediately preceding this one by consensus,
/// if it can be inferred from the `prev_blockhash` of the inner block data.
pub fn prev_block_id(&self) -> Option<BlockId> {
    let prev_height = self.height().checked_sub(1)?;
    let prev_hash = self.0.data.prev_blockhash()?;
    Some(BlockId {
        height: prev_height,
        hash: prev_hash,
    })
}

@ValuedMammal ValuedMammal force-pushed the feat/toblockhash_prev branch 3 times, most recently from a2ea12f to d5b2513 Compare January 13, 2026 22:20
@ValuedMammal ValuedMammal changed the title feat(core): Add prev_blockhash to trait ToBlockHash feat(core)!: Add prev_blockhash to trait ToBlockHash Jan 13, 2026
@ValuedMammal ValuedMammal marked this pull request as draft January 13, 2026 22:32
Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

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

Since you're taking over ticket #2021, feel free to checkout my unfinished work for inspiration: https://github.com/bitcoindevkit/bdk/compare/cp_entry.

// implication invalid.
tail = vec![];
break cp.prev().expect("can't be called on genesis block");
// We're replacing a block, so the tail is no longer valid.
Copy link
Member

Choose a reason for hiding this comment

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

We should also displace the tail if:

  • data's prev_blockhash conflicts with the block directly below. The block directly below should also be displaced.
  • data's blockhash conflicts with the the prev_blockhash of the block directly above.

To achieve this, I recommend using CheckPointEntry instead of CheckPoint and add a method such as CheckPointEntry::prev() - this requires the CheckPointEntry::BlockId variant to have a CheckPoint field.

Naming suggestion

   /// An entry yielded by [`CheckPointEntryIter`].
   #[derive(Debug, Clone)]
   pub enum CheckPointEntry<D> {
       /// A placeholder entry: there is no checkpoint stored st this height,
       /// but the checkpoint one height above links back here via it's `prev_blockhash`.
       Placeholder {
           /// The block ID at *this* height.
           block_id: BlockId,
           /// The checkpoint one height *above* that links back to `block_id`.
           checkpoint_above: CheckPoint<D>,
       },
       /// A real checkpoint recorded at this height.
       Occupied(CheckPoint<D>),
   }

/// (a.k.a. the update and original chain both have a block above the point of agreement, but
/// their heights do not overlap).
/// - The update attempts to replace the genesis block of the original chain.
fn merge_chains(
Copy link
Member

@evanlinjin evanlinjin Jan 14, 2026

Choose a reason for hiding this comment

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

The refactored merge_chains does not do anything meaningfully different. It also does not make it easier to read.

The important change to make is to make sure merge_chains takes prev_blockhashes into consideration. The refactoring does not do that.

Suggested changes

  • Iterate over CheckPointEntry instead of CheckPoint. Rationale: prev_blockhashes are also block hashes so they can be points of agreements/invalidation.
  • Only insert Some(data) into ChangeSet if there is an actual CheckPoint in the update chain at that height.
  • Only insert None into ChangeSet if there is an actual CheckPoint in the original chain at that height.

Suggested tests

These tests uses a data type that returns Some for prev_blockhash:

prev_blockhash invalidates
  • Original: 1:A, 2:B, 4:D
  • Update: 1:A, 3:C' (prev=2:B')
  • Result: 1:A, 3:C' (prev=2:B')

Connects due to prev_blockhash

  • Original: 2:B, 3:C
  • Update: 2:B, 4:D (prev=3:C)
  • Result: 2:B, 3:C, 4:D

The `chain_update` macro now constructs a `CheckPoint`
instead of a `LocalChain`, enabling test cases of
chain update where no point of agreement exists.
@ValuedMammal ValuedMammal force-pushed the feat/toblockhash_prev branch from d5b2513 to 01800f4 Compare January 15, 2026 18:26
@ValuedMammal ValuedMammal changed the title feat(core)!: Add prev_blockhash to trait ToBlockHash feat(core)!: Add prev_blockhash to ToBlockHash trait Jan 15, 2026
@ValuedMammal ValuedMammal changed the title feat(core)!: Add prev_blockhash to ToBlockHash trait feat(chain)!: Add prev_blockhash to ToBlockHash trait Jan 15, 2026
- `merge_chains` is now implemented on the `LocalChain` type
- `merge_chains` now errors if the update conflicts with the
genesis hash. This brings the code into alignment with the
documentation
- Fixed faulty behavior preventing the ability to switch
forks in a basic reorg scenario. The test case is modeled under
the name "invalidate tip and extend chain".
- Updated test expectations in `test_local_chain.rs`
The `LocalChain` maintains an invariant that a value at height 0
must be present, or the so-called "genesis" block. To prevent
potentially costly lookups every time we want to retrieve the
genesis hash, we store the block hash in the `LocalChain` upon
construction.
Previously, methods of `CheckPoint` were constrained by `D: Copy`,
and this was to prevent a possible memory leak caused by the
use of `mem::forget` in the Drop impl for `CheckPoint`. Since the
memory leak concern has been resolved, we relax the constraint
to `Clone` instead which permits instances of `CheckPoint<D>`
where `D` is not necessarily a Copy type.
`prev_blockhash` returns the hash of the previous block data, if
it is known, and `None` otherwise.

Implement `prev_blockhash` for `Header`, which returns the header's
previous blockhash.
Fixed `CheckPoint::push` to now check that if the height being
pushed directly follows the current tip height, the previous hash of
`data` is the same as the current tip hash, (provided that the data's
`prev_blockhash` is known). This is necessary to prevent inconsistent
or invalid chains from being constructed.

Fixed `insert` to displace conflicting blocks by reverting to a
previous base if it's found to conflict with the inserted data.
Also avoid a panic in `insert` in case `push` returns an error
by returning the last successfully updated tip.
… from `extend`

Now that `apply_changeset` can fail due to inconsistent data in
addition to `MissingGenesisError`, we need to propagate the error
rather than expect that the chain update can always succeed.

Added private struct `ApplyChangeSetError`. This error is returned
by `apply_changeset_to_checkpoint`.

BREAKING:

- Changed `LocalChain::from_blocks` to return
  `Result<Self, Option<CheckPoint<D>>`
- Changed `LocalChain::from_changeset` to return
  `Result<Option<Self>, CannotConnectError)`
- Changed `LocalChain::apply_changeset` to return
  `Result<(), CannotConnectError>`
@ValuedMammal ValuedMammal force-pushed the feat/toblockhash_prev branch from 01800f4 to d160549 Compare January 15, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Question re: when LocalChain::apply_update invalidates checkpoints Ensure CheckPoint chain methods validate and link via previous blockhash

2 participants