From: Elias Rohrer Date: Wed, 15 Apr 2026 12:41:54 +0000 (+0200) Subject: fix(chain): prevent `merge_chains` from replacing the genesis block X-Git-Tag: electrum-0.24.0~2^2 X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/example_cli/struct.EncoderStringWriter.html?a=commitdiff_plain;h=be8e156115e21750158d44394341f1d41dfbf90f;p=bdk fix(chain): prevent `merge_chains` from replacing the genesis block 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 --- diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 0ab676e8..e3aa74a0 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -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())); diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs index 8adbce4a..cc157c5d 100644 --- a/crates/chain/tests/test_local_chain.rs +++ b/crates/chain/tests/test_local_chain.rs @@ -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);