Skip to content

fix(chain): reject a changeset that replaces the genesis block - #2311

Closed
wangzhengzhuo05 wants to merge 1 commit into
bitcoindevkit:masterfrom
wangzhengzhuo05:fix/apply-changeset-genesis
Closed

wangzhengzhuo05 wants to merge 1 commit into
bitcoindevkit:masterfrom
wangzhengzhuo05:fix/apply-changeset-genesis

Conversation

@wangzhengzhuo05

Copy link
Copy Markdown

Solves #2309

Description

LocalChain::apply_changeset was the only mutation entrypoint that did not enforce the
height-0 invariant. A ChangeSet carrying (0, Some(hash)) whose hash differs from the
chain's genesis was accepted: apply_changeset_to_checkpoint overwrote height 0 and rebuilt
the chain through LocalChain::from_blocks, so the chain silently moved onto a different
chain (e.g. another network). apply_update (via merge_chains), insert_block and
disconnect_from all refuse to alter height 0.

This adds ApplyBlockError::CannotReplaceGenesis and rejects the changeset before anything
is applied:

  • apply_changeset_to_checkpoint checks a Some(_) height-0 entry against the existing
    genesis. If that genesis is not a placeholder and the hashes disagree, it returns
    Err(ApplyBlockError::CannotReplaceGenesis { expected }) and leaves the chain untouched.
  • A height-0 entry that agrees with the current genesis, and one that upgrades a
    placeholder genesis to real data, still apply — the same case merge_chains already
    allows via !o.is_placeholder().
  • merge_chains maps the new variant to CannotConnectError { try_include_height: 0 },
    consistent with its own genesis guard.
  • apply_changeset's public signature is unchanged. ApplyBlockError gains one variant,
    which is a breaking change for exhaustive downstream matches.

Tests

crates/chain/tests/test_genesis_changeset.rs (new file, the path suggested in the issue):

  • apply_changeset_does_not_replace_genesis — the issue's reproduction: the changeset is
    rejected and both the genesis hash and the tip are unchanged.
  • apply_changeset_with_matching_genesis_extends_chain — a (0, Some(genesis)) entry plus a
    new higher block still applies.
  • apply_changeset_above_genesis_still_applies — the guard is not over-broad.
cargo test -p bdk_chain --all-features                                  # ok, 100 tests, 0 failed
cargo clippy -p bdk_chain --all-features --all-targets -- -D warnings   # clean
cargo fmt --all -- --check                                              # clean

Mutation check: with the height-0 comparison neutered (changeset.blocks.get(&1)),
apply_changeset_does_not_replace_genesis fails with assertion failed: result.is_err();
restoring the fix turns the file green again. So the new test really guards the fix.

Notes

This change was implemented with AI assistance (OpenCode + muse-spark 1.3) and verified
locally as described above. The commit is not GPG-signed — no signing key is configured on
this machine; I am happy to re-push a signed commit if the project requires it.

apply_changeset was the only LocalChain mutation entrypoint that did not
enforce the height-0 invariant: a changeset carrying (0, Some(hash)) with a
hash different from the chain's genesis silently rewrote the genesis, moving
the chain onto a different chain (e.g. another network) without any error.
apply_update (via merge_chains), insert_block and disconnect_from all refuse
to alter height 0.

Add ApplyBlockError::CannotReplaceGenesis and check the height-0 entry
against a non-placeholder genesis before applying the changeset. merge_chains
maps the new variant to CannotConnectError { try_include_height: 0 } for
consistency with its own genesis guard.

Closes bitcoindevkit#2309
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 46.66667% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.83%. Comparing base (c6a6073) to head (9b220bc).

Files with missing lines Patch % Lines
crates/chain/src/local_chain.rs 46.66% 6 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2311      +/-   ##
==========================================
- Coverage   78.84%   78.83%   -0.02%     
==========================================
  Files          31       31              
  Lines        6060     6075      +15     
  Branches      288      291       +3     
==========================================
+ Hits         4778     4789      +11     
- Misses       1203     1205       +2     
- Partials       79       81       +2     
Flag Coverage Δ
rust 78.83% <46.66%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@wangzhengzhuo05

Copy link
Copy Markdown
Author

Closing this out.

It has been waiting 5 days without any maintainer review, and we are winding down this contribution effort — a stale PR sitting in the queue costs maintainers attention without giving them a decision to make, so it is better closed than left open indefinitely.

If the direction is useful, the branch is intact on the fork (fix/apply-changeset-genesis, +65/−0 across 2 file(s)) and can be picked up as-is.

What it contains: fix(chain): reject a changeset that replaces the genesis block — Solves #2309

It was verified locally before opening (target test suite green, plus a mutation check that reverts only the fix and confirms the new regression test fails). Thanks for the time.

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant