Skip to content

Commit 7dfb4f2

Browse files
evanlinjinclaude
andcommitted
fix(chain)!: make genesis immutable in merge_chains
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>
1 parent a412025 commit 7dfb4f2

2 files changed

Lines changed: 25 additions & 6 deletions

File tree

crates/chain/src/local_chain.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ impl<D> FromIterator<(u32, D)> for ChangeSet<D> {
497497
/// Error when applying blocks to a local chain.
498498
#[derive(Clone, Debug, PartialEq)]
499499
pub enum ApplyBlockError {
500-
/// Genesis block is missing or would be altered.
500+
/// Genesis block is missing.
501501
MissingGenesis,
502502
/// Block's `prev_blockhash` doesn't match the expected block.
503503
PrevBlockhashMismatch {
@@ -510,7 +510,7 @@ impl core::fmt::Display for ApplyBlockError {
510510
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511511
match self {
512512
ApplyBlockError::MissingGenesis => {
513-
write!(f, "genesis block is missing or would be altered")
513+
write!(f, "genesis block is missing")
514514
}
515515
ApplyBlockError::PrevBlockhashMismatch { expected } => write!(
516516
f,
@@ -753,6 +753,13 @@ where
753753
}
754754
}
755755
} else {
756+
// Genesis block (height 0) cannot be replaced. If the original and
757+
// update disagree on genesis, they belong to different chains.
758+
if o.height() == 0 && !o.is_placeholder() {
759+
return Err(CannotConnectError {
760+
try_include_height: 0,
761+
});
762+
}
756763
// We have an invalidation height so we set the height to the updated hash and
757764
// also purge all the original chain block hashes above this block.
758765
changeset.blocks.insert(u.height(), u.data());

crates/chain/tests/test_local_chain.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ fn update_local_chain() {
176176
},
177177
TestLocalChain {
178178
name: "fix blockhash before agreement point",
179-
chain: local_chain![(0, hash!("im-wrong")), (1, hash!("we-agree"))],
180-
update: chain_update![(0, hash!("fix")), (1, hash!("we-agree"))],
179+
chain: local_chain![(0, hash!("_")), (1, hash!("im-wrong")), (2, hash!("we-agree"))],
180+
update: chain_update![(0, hash!("_")), (1, hash!("fix")), (2, hash!("we-agree"))],
181181
exp: ExpectedResult::Ok {
182-
changeset: &[(0, Some(hash!("fix")))],
183-
init_changeset: &[(0, Some(hash!("fix"))), (1, Some(hash!("we-agree")))],
182+
changeset: &[(1, Some(hash!("fix")))],
183+
init_changeset: &[(0, Some(hash!("_"))), (1, Some(hash!("fix"))), (2, Some(hash!("we-agree")))],
184184
},
185185
},
186186
// B and C are in both chain and update
@@ -320,6 +320,18 @@ fn update_local_chain() {
320320
],
321321
},
322322
},
323+
// Conflicting genesis with no point of agreement should fail.
324+
// | 0 | 2
325+
// chain | _ B
326+
// update | _' B'
327+
TestLocalChain {
328+
name: "conflicting genesis without agreement point",
329+
chain: local_chain![(0, hash!("_")), (2, hash!("B"))],
330+
update: chain_update![(0, hash!("_'")), (2, hash!("B'"))],
331+
exp: ExpectedResult::Err(CannotConnectError {
332+
try_include_height: 0,
333+
}),
334+
},
323335
]
324336
.into_iter()
325337
.for_each(TestLocalChain::run);

0 commit comments

Comments
 (0)