diff --git a/src/descriptor/error.rs b/src/descriptor/error.rs index 93a41919..3220c4dc 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 1eb8fbc3..3f273c99 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 3a5e7fde..07dcb78c 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 a27d21d8..1aa8d754 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();