]> Untitled Git - bdk/commitdiff
fix(chain): prevent `merge_chains` from replacing the genesis block
authorElias Rohrer <dev@tnull.de>
Wed, 15 Apr 2026 12:41:54 +0000 (14:41 +0200)
committerElias Rohrer <dev@tnull.de>
Tue, 21 Apr 2026 09:07:35 +0000 (11:07 +0200)
Other code paths (`disconnect_from`, `CheckPoint::insert`) already
protect height 0 from modification, but `merge_chains` allowed an
update chain with a different genesis hash to silently replace the
wallet's existing genesis block. Return `CannotConnectError` when the
update attempts to change the block hash at height 0.

Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
crates/chain/src/local_chain.rs
crates/chain/tests/test_local_chain.rs

index 0ab676e8d46d42e2ff818ce057a6946527228dfb..e3aa74a07a2c1a8f3f99c4b24dae0a12814ffac5 100644 (file)
@@ -648,6 +648,12 @@ fn merge_chains(
                         }
                     }
                 } else {
+                    // The genesis block must never be replaced.
+                    if u.height() == 0 {
+                        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(), Some(u.hash()));
index 8adbce4af4c0039815e3f86ef49166ab68567bd0..cc157c5d8235cdf8e79b483286de59c0cd7c4f03 100644 (file)
@@ -171,11 +171,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!("genesis")), (1, hash!("im-wrong")), (2, hash!("we-agree"))],
+            update: chain_update![(0, hash!("genesis")), (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!("genesis"))), (1, Some(hash!("fix"))), (2, Some(hash!("we-agree")))],
             },
         },
         // B and C are in both chain and update
@@ -315,6 +315,18 @@ fn update_local_chain() {
                 ],
             },
         },
+        // Reject update that replaces the genesis block
+        //        | 0 | 1 | 2
+        // chain  | A   B   C
+        // update | A'  B'  C'
+        TestLocalChain {
+            name: "reject genesis block replacement",
+            chain: local_chain![(0, hash!("A")), (1, hash!("B")), (2, hash!("C"))],
+            update: chain_update![(0, hash!("A'")), (1, hash!("B'")), (2, hash!("C'"))],
+            exp: ExpectedResult::Err(CannotConnectError {
+                try_include_height: 0,
+            }),
+        },
     ]
     .into_iter()
     .for_each(TestLocalChain::run);