]> Untitled Git - bdk/commitdiff
fix(chain)!: make genesis immutable in `merge_chains`
author志宇 <hello@evanlinjin.me>
Wed, 18 Feb 2026 17:33:29 +0000 (17:33 +0000)
committer志宇 <hello@evanlinjin.me>
Wed, 22 Apr 2026 04:48:19 +0000 (04:48 +0000)
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>
crates/chain/src/local_chain.rs
crates/chain/tests/test_local_chain.rs

index 537e910ece8bdef410f57c30f381376648f47f2e..56cde203e3b4ce6807939e8ff53ac2cae2952a39 100644 (file)
@@ -497,7 +497,7 @@ impl<D> FromIterator<(u32, D)> for ChangeSet<D> {
 /// 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());
index 109ac5f520373d1bca016266fbfb9394306e1dc0..a9af0248b8be35dc0734ce56be73029972d29ba7 100644 (file)
@@ -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);