From cd4e5258515266a7582b7c34dec5ee41b35fb19b Mon Sep 17 00:00:00 2001 From: busayo-OD Date: Tue, 28 Jul 2026 16:02:39 +0100 Subject: [PATCH] fix(wallet): validate network and genesis hash consistency Reject wallets configured for Network::Bitcoin with a non-mainnet genesis hash during wallet creation and loading. Introduce dedicated error variants to report these validation failures through the existing create and load APIs, and add regression tests covering both entry points. Signed-off-by: busayo-OD --- src/descriptor/error.rs | 15 +++++++++++ src/wallet/error.rs | 14 +++++++++++ src/wallet/mod.rs | 27 +++++++++++++++++--- tests/wallet.rs | 55 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/src/descriptor/error.rs b/src/descriptor/error.rs index 93a419190..3220c4dc7 100644 --- a/src/descriptor/error.rs +++ b/src/descriptor/error.rs @@ -10,6 +10,7 @@ // licenses. //! Descriptor errors +use bitcoin::{BlockHash, Network}; use core::fmt; /// Errors related to the parsing and usage of descriptors @@ -44,6 +45,13 @@ pub enum Error { Hex(bitcoin::hex::HexToBytesError), /// The provided wallet descriptors are identical ExternalAndInternalAreTheSame, + /// The provided genesis hash does not match the expected mainnet genesis hash + GenesisHashMismatch { + /// The configured network + network: Network, + /// The genesis hash that was provided + genesis_hash: BlockHash, + }, } impl From for Error { @@ -84,6 +92,13 @@ impl fmt::Display for Error { Self::ExternalAndInternalAreTheSame => { write!(f, "External and internal descriptors are the same") } + Self::GenesisHashMismatch { + network, + genesis_hash, + } => write!( + f, + "Genesis hash {genesis_hash} does not match expected genesis hash for network {network}" + ), } } } diff --git a/src/wallet/error.rs b/src/wallet/error.rs index 1eb8fbc37..3f273c99d 100644 --- a/src/wallet/error.rs +++ b/src/wallet/error.rs @@ -38,6 +38,13 @@ pub enum LoadError { MissingGenesis, /// Data loaded from persistence is missing descriptor. MissingDescriptor(KeychainKind), + /// The network's mainnet genesis hash does not match the loaded genesis hash. + GenesisNetworkMismatch { + /// The loaded network. + network: Network, + /// The loaded genesis hash. + genesis_hash: BlockHash, + }, /// Data loaded is unexpected. Mismatch(LoadMismatch), } @@ -51,6 +58,13 @@ impl fmt::Display for LoadError { LoadError::MissingDescriptor(k) => { write!(f, "loaded data is missing descriptor for {k} keychain") } + LoadError::GenesisNetworkMismatch { + network, + genesis_hash, + } => write!( + f, + "network {network} is mainnet but loaded genesis hash {genesis_hash} does not match" + ), LoadError::Mismatch(e) => write!(f, "{e}"), } } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 3a5e7fde0..07dcb78cc 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -37,7 +37,7 @@ use bitcoin::{ Address, Amount, Block, FeeRate, Network, NetworkKind, OutPoint, Psbt, ScriptBuf, Sequence, SignedAmount, Transaction, TxOut, Txid, Weight, Witness, absolute, consensus::encode::serialize, - constants::genesis_block, + constants::{ChainHash, genesis_block}, psbt, secp256k1::Secp256k1, sighash::{EcdsaSighashType, TapSighashType}, @@ -339,9 +339,20 @@ impl Wallet { let secp = SecpCtx::new(); let network = params.network; let network_kind = NetworkKind::from(network); - let genesis_hash = params - .genesis_hash - .unwrap_or(genesis_block(network).block_hash()); + let genesis_hash = match params.genesis_hash { + Some(hash) => { + if network_kind.is_mainnet() + && ChainHash::from_genesis_block_hash(hash) != ChainHash::BITCOIN + { + return Err(DescriptorError::GenesisHashMismatch { + network, + genesis_hash: hash, + }); + } + hash + } + None => genesis_block(network).block_hash(), + }; let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); let (descriptor, mut descriptor_keymap) = (params.descriptor)(&secp, network_kind)?; @@ -475,6 +486,14 @@ impl Wallet { })); } } + if network_kind.is_mainnet() + && ChainHash::from_genesis_block_hash(chain.genesis_hash()) != ChainHash::BITCOIN + { + return Err(LoadError::GenesisNetworkMismatch { + network, + genesis_hash: chain.genesis_hash(), + }); + } if let Some(exp_genesis_hash) = params.check_genesis_hash { if chain.genesis_hash() != exp_genesis_hash { return Err(LoadError::Mismatch(LoadMismatch::Genesis { diff --git a/tests/wallet.rs b/tests/wallet.rs index a27d21d8b..1aa8d7548 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use std::sync::Arc; use assert_matches::assert_matches; -use bdk_chain::{BlockId, CanonicalizationParams, ConfirmationBlockTime}; +use bdk_chain::{BlockId, CanonicalizationParams, ConfirmationBlockTime, local_chain}; use bdk_wallet::KeychainKind; use bdk_wallet::coin_selection; use bdk_wallet::coin_selection::InsufficientFunds; @@ -12,8 +12,8 @@ use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::signer::{SignOptions, SignerError, SignersContainer}; use bdk_wallet::test_utils::*; use bdk_wallet::{ - AddressInfo, Balance, FinalizeInputOutcome, IndexOutOfBoundsError, PersistedWallet, Update, - Wallet, WalletTx, + AddressInfo, Balance, ChangeSet, FinalizeInputOutcome, IndexOutOfBoundsError, LoadError, + LoadParams, PersistedWallet, Update, Wallet, WalletTx, }; use bitcoin::constants::COINBASE_MATURITY; use bitcoin::hashes::Hash; @@ -53,6 +53,55 @@ fn test_error_external_and_internal_are_the_same() { ); } +#[test] +fn test_error_create_mainnet_network_with_non_mainnet_genesis_hash() { + let external_desc = "tr(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)"; + let internal_desc = "tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55)"; + let testnet_genesis_hash = bitcoin::constants::genesis_block(Network::Testnet).block_hash(); + + let err = Wallet::create(external_desc, internal_desc) + .network(Network::Bitcoin) + .genesis_hash(testnet_genesis_hash) + .create_wallet_no_persist(); + + assert!( + matches!( + err, + Err(DescriptorError::GenesisHashMismatch { + network: Network::Bitcoin, + genesis_hash, + }) if genesis_hash == testnet_genesis_hash + ), + "expected wallet creation to reject a mismatched mainnet network and genesis hash, got {err:?}", + ); +} + +#[test] +fn test_error_load_mainnet_network_with_non_mainnet_genesis_hash() { + let testnet_genesis_hash = bitcoin::constants::genesis_block(Network::Testnet).block_hash(); + + let changeset = ChangeSet { + network: Some(Network::Bitcoin), + local_chain: local_chain::ChangeSet { + blocks: [(0, Some(testnet_genesis_hash))].into(), + }, + ..Default::default() + }; + + let err = Wallet::load_with_params(changeset, LoadParams::default()); + + assert!( + matches!( + err, + Err(LoadError::GenesisNetworkMismatch { + network: Network::Bitcoin, + genesis_hash, + }) if genesis_hash == testnet_genesis_hash + ), + "expected wallet loading to reject a mismatched mainnet network and genesis hash, got {err:?}" + ); +} + #[test] fn test_descriptor_checksum() { let (wallet, _) = get_funded_wallet_wpkh();