fix(chain): reject a changeset that replaces the genesis block - #2311
wangzhengzhuo05 wants to merge 1 commit into
Conversation
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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
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 ( 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. |
Solves #2309
Description
LocalChain::apply_changesetwas the only mutation entrypoint that did not enforce theheight-0 invariant. A
ChangeSetcarrying(0, Some(hash))whose hash differs from thechain's genesis was accepted:
apply_changeset_to_checkpointoverwrote height 0 and rebuiltthe chain through
LocalChain::from_blocks, so the chain silently moved onto a differentchain (e.g. another network).
apply_update(viamerge_chains),insert_blockanddisconnect_fromall refuse to alter height 0.This adds
ApplyBlockError::CannotReplaceGenesisand rejects the changeset before anythingis applied:
apply_changeset_to_checkpointchecks aSome(_)height-0 entry against the existinggenesis. If that genesis is not a placeholder and the hashes disagree, it returns
Err(ApplyBlockError::CannotReplaceGenesis { expected })and leaves the chain untouched.placeholder genesis to real data, still apply — the same case
merge_chainsalreadyallows via
!o.is_placeholder().merge_chainsmaps the new variant toCannotConnectError { try_include_height: 0 },consistent with its own genesis guard.
apply_changeset's public signature is unchanged.ApplyBlockErrorgains 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 isrejected and both the genesis hash and the tip are unchanged.
apply_changeset_with_matching_genesis_extends_chain— a(0, Some(genesis))entry plus anew higher block still applies.
apply_changeset_above_genesis_still_applies— the guard is not over-broad.Mutation check: with the height-0 comparison neutered (
changeset.blocks.get(&1)),apply_changeset_does_not_replace_genesisfails withassertion 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.