From ef35b199376b3b88bc7b8985aa1afa6c81bda5c1 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 18 Feb 2026 17:33:29 +0000 Subject: [PATCH] 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 --- crates/chain/src/local_chain.rs | 11 +++++++++-- crates/chain/tests/test_local_chain.rs | 20 ++++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 537e910e..56cde203 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -497,7 +497,7 @@ impl FromIterator<(u32, D)> for ChangeSet { /// Error when applying blocks to a local chain. #[derive(Clone, Debug, PartialEq)] pub enum ApplyBlockError { - /// Genesis block is missing or would be altered. + /// Genesis block is missing. MissingGenesis, /// Block's `prev_blockhash` doesn't match the expected block. PrevBlockhashMismatch { @@ -510,7 +510,7 @@ impl core::fmt::Display for ApplyBlockError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ApplyBlockError::MissingGenesis => { - write!(f, "genesis block is missing or would be altered") + write!(f, "genesis block is missing") } ApplyBlockError::PrevBlockhashMismatch { expected } => write!( f, @@ -748,6 +748,13 @@ where } } } else { + // Genesis block (height 0) cannot be replaced. If the original and + // update disagree on genesis, they belong to different chains. + if o.height() == 0 && !o.is_placeholder() { + return Err(CannotConnectError { + try_include_height: 0, + }); + } // We have an invalidation height so we set the height to the updated hash and // also purge all the original chain block hashes above this block. changeset.blocks.insert(u.height(), u.data()); diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs index 109ac5f5..a9af0248 100644 --- a/crates/chain/tests/test_local_chain.rs +++ b/crates/chain/tests/test_local_chain.rs @@ -176,11 +176,11 @@ fn update_local_chain() { }, TestLocalChain { name: "fix blockhash before agreement point", - chain: local_chain![(0, hash!("im-wrong")), (1, hash!("we-agree"))], - update: chain_update![(0, hash!("fix")), (1, hash!("we-agree"))], + chain: local_chain![(0, hash!("_")), (1, hash!("im-wrong")), (2, hash!("we-agree"))], + update: chain_update![(0, hash!("_")), (1, hash!("fix")), (2, hash!("we-agree"))], exp: ExpectedResult::Ok { - changeset: &[(0, Some(hash!("fix")))], - init_changeset: &[(0, Some(hash!("fix"))), (1, Some(hash!("we-agree")))], + changeset: &[(1, Some(hash!("fix")))], + init_changeset: &[(0, Some(hash!("_"))), (1, Some(hash!("fix"))), (2, Some(hash!("we-agree")))], }, }, // B and C are in both chain and update @@ -320,6 +320,18 @@ fn update_local_chain() { ], }, }, + // Conflicting genesis with no point of agreement should fail. + // | 0 | 2 + // chain | _ B + // update | _' B' + TestLocalChain { + name: "conflicting genesis without agreement point", + chain: local_chain![(0, hash!("_")), (2, hash!("B"))], + update: chain_update![(0, hash!("_'")), (2, hash!("B'"))], + exp: ExpectedResult::Err(CannotConnectError { + try_include_height: 0, + }), + }, ] .into_iter() .for_each(TestLocalChain::run); -- 2.49.0