diff --git a/crates/bdk/README.md b/crates/bdk/README.md index f911a4ea6d..72befa33f6 100644 --- a/crates/bdk/README.md +++ b/crates/bdk/README.md @@ -72,7 +72,7 @@ fn main() { let db = (); let descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; - let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet).expect("should create"); + let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet, None).expect("should create"); // get a new address (this increments revealed derivation index) println!("revealed address: {}", wallet.get_address(AddressIndex::New)); diff --git a/crates/bdk/examples/compiler.rs b/crates/bdk/examples/compiler.rs index f8918895ce..9445bcc94a 100644 --- a/crates/bdk/examples/compiler.rs +++ b/crates/bdk/examples/compiler.rs @@ -54,7 +54,8 @@ fn main() -> Result<(), Box> { info!("Compiled into following Descriptor: \n{}", descriptor); // Create a new wallet from this descriptor - let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?; + let mut wallet = + Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest, None)?; info!( "First derived address from the descriptor: \n{}", diff --git a/crates/bdk/src/descriptor/template.rs b/crates/bdk/src/descriptor/template.rs index c5e8b31c50..aef5cea1aa 100644 --- a/crates/bdk/src/descriptor/template.rs +++ b/crates/bdk/src/descriptor/template.rs @@ -79,7 +79,7 @@ impl IntoWalletDescriptor for T { /// /// let key = /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; -/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?; +/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet, None)?; /// /// assert_eq!( /// wallet.get_address(New).to_string(), @@ -107,7 +107,7 @@ impl> DescriptorTemplate for P2Pkh { /// /// let key = /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; -/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?; +/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet, None)?; /// /// assert_eq!( /// wallet.get_address(AddressIndex::New).to_string(), @@ -136,7 +136,7 @@ impl> DescriptorTemplate for P2Wpkh_P2Sh { /// /// let key = /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; -/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?; +/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet, None)?; /// /// assert_eq!( /// wallet.get_address(New).to_string(), @@ -164,7 +164,7 @@ impl> DescriptorTemplate for P2Wpkh { /// /// let key = /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; -/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?; +/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet, None)?; /// /// assert_eq!( /// wallet.get_address(New).to_string(), @@ -200,6 +200,7 @@ impl> DescriptorTemplate for P2TR { /// Bip44(key.clone(), KeychainKind::External), /// Some(Bip44(key, KeychainKind::Internal)), /// Network::Testnet, +/// None, /// )?; /// /// assert_eq!(wallet.get_address(New).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89"); @@ -238,6 +239,7 @@ impl> DescriptorTemplate for Bip44 { /// Bip44Public(key.clone(), fingerprint, KeychainKind::External), /// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)), /// Network::Testnet, +/// None, /// )?; /// /// assert_eq!(wallet.get_address(New).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR"); @@ -275,6 +277,7 @@ impl> DescriptorTemplate for Bip44Public { /// Bip49(key.clone(), KeychainKind::External), /// Some(Bip49(key, KeychainKind::Internal)), /// Network::Testnet, +/// None, /// )?; /// /// assert_eq!(wallet.get_address(New).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB"); @@ -313,6 +316,7 @@ impl> DescriptorTemplate for Bip49 { /// Bip49Public(key.clone(), fingerprint, KeychainKind::External), /// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)), /// Network::Testnet, +/// None, /// )?; /// /// assert_eq!(wallet.get_address(New).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt"); @@ -350,6 +354,7 @@ impl> DescriptorTemplate for Bip49Public { /// Bip84(key.clone(), KeychainKind::External), /// Some(Bip84(key, KeychainKind::Internal)), /// Network::Testnet, +/// None, /// )?; /// /// assert_eq!(wallet.get_address(New).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n"); @@ -388,6 +393,7 @@ impl> DescriptorTemplate for Bip84 { /// Bip84Public(key.clone(), fingerprint, KeychainKind::External), /// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)), /// Network::Testnet, +/// None, /// )?; /// /// assert_eq!(wallet.get_address(New).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7"); @@ -425,6 +431,7 @@ impl> DescriptorTemplate for Bip84Public { /// Bip86(key.clone(), KeychainKind::External), /// Some(Bip86(key, KeychainKind::Internal)), /// Network::Testnet, +/// None, /// )?; /// /// assert_eq!(wallet.get_address(New).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu"); @@ -463,6 +470,7 @@ impl> DescriptorTemplate for Bip86 { /// Bip86Public(key.clone(), fingerprint, KeychainKind::External), /// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)), /// Network::Testnet, +/// None, /// )?; /// /// assert_eq!(wallet.get_address(New).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37"); diff --git a/crates/bdk/src/error.rs b/crates/bdk/src/error.rs index fcb5a6f7b1..3bd3dc6d32 100644 --- a/crates/bdk/src/error.rs +++ b/crates/bdk/src/error.rs @@ -199,3 +199,12 @@ impl_error!(miniscript::Error, Miniscript); impl_error!(MiniscriptPsbtError, MiniscriptPsbt); impl_error!(bitcoin::bip32::Error, Bip32); impl_error!(bitcoin::psbt::Error, Psbt); + +impl From for Error { + fn from(e: crate::wallet::NewNoPersistError) -> Self { + match e { + wallet::NewNoPersistError::Descriptor(e) => Error::Descriptor(e), + unknown_network_err => Error::Generic(format!("{}", unknown_network_err)), + } + } +} diff --git a/crates/bdk/src/wallet/export.rs b/crates/bdk/src/wallet/export.rs index f2d656891e..9f8b1e6097 100644 --- a/crates/bdk/src/wallet/export.rs +++ b/crates/bdk/src/wallet/export.rs @@ -33,6 +33,7 @@ //! &import.descriptor(), //! import.change_descriptor().as_ref(), //! Network::Testnet, +//! None, //! )?; //! # Ok::<_, Box>(()) //! ``` @@ -46,6 +47,7 @@ //! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", //! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"), //! Network::Testnet, +//! None, //! )?; //! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap(); //! @@ -226,7 +228,8 @@ mod test { change_descriptor: Option<&str>, network: Network, ) -> Wallet<()> { - let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap(); + let mut wallet = + Wallet::new_no_persist(descriptor, change_descriptor, network, None).unwrap(); let transaction = Transaction { input: vec![], output: vec![], diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 659da90a26..132d388c7c 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -28,7 +28,6 @@ use bdk_chain::{ Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut, IndexedTxGraph, Persist, PersistBackend, }; -use bitcoin::consensus::encode::serialize; use bitcoin::psbt; use bitcoin::secp256k1::Secp256k1; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; @@ -36,6 +35,7 @@ use bitcoin::{ absolute, Address, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxOut, Txid, Weight, Witness, }; +use bitcoin::{consensus::encode::serialize, BlockHash}; use core::fmt; use core::ops::Deref; use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; @@ -218,26 +218,65 @@ impl Wallet { descriptor: E, change_descriptor: Option, network: Network, - ) -> Result { - Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { - NewError::Descriptor(e) => e, - NewError::Persist(_) => unreachable!("no persistence so it can't fail"), + custom_genesis_hash: Option, + ) -> Result { + Self::new( + descriptor, + change_descriptor, + (), + network, + custom_genesis_hash, + ) + .map_err(|e| match e { + NewError::Descriptor(e) => NewNoPersistError::Descriptor(e), + NewError::Persist(_) | NewError::InvalidPersistenceGenesis => { + unreachable!("no persistence so it can't fail") + } + NewError::UnknownNetwork => NewNoPersistError::UnknownNetwork, }) } } +/// Error returned from [`Wallet::new_no_persist`] +#[derive(Debug)] +pub enum NewNoPersistError { + /// There was problem with the descriptors passed in + Descriptor(crate::descriptor::DescriptorError), + /// We cannot determine the genesis hash from the network. + UnknownNetwork, +} + +impl fmt::Display for NewNoPersistError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NewNoPersistError::Descriptor(e) => e.fmt(f), + NewNoPersistError::UnknownNetwork => write!( + f, + "unknown network - genesis block hash needs to be provided explicitly" + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for NewNoPersistError {} + #[derive(Debug)] /// Error returned from [`Wallet::new`] -pub enum NewError

{ +pub enum NewError { /// There was problem with the descriptors passed in Descriptor(crate::descriptor::DescriptorError), /// We were unable to load the wallet's data from the persistence backend - Persist(P), + Persist(PE), + /// We cannot determine the genesis hash from the network + UnknownNetwork, + /// The genesis block hash is either missing from persistence or has an unexpected value + InvalidPersistenceGenesis, } -impl

fmt::Display for NewError

+impl fmt::Display for NewError where - P: fmt::Display, + PE: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -245,10 +284,18 @@ where NewError::Persist(e) => { write!(f, "failed to load wallet from persistence backend: {}", e) } + NewError::UnknownNetwork => write!( + f, + "unknown network - genesis block hash needs to be provided explicitly" + ), + NewError::InvalidPersistenceGenesis => write!(f, "the genesis block hash is either missing from persistence or has an unexpected value"), } } } +#[cfg(feature = "std")] +impl std::error::Error for NewError where PE: core::fmt::Display + core::fmt::Debug {} + /// An error that may occur when inserting a transaction into [`Wallet`]. #[derive(Debug)] pub enum InsertTxError { @@ -256,15 +303,12 @@ pub enum InsertTxError { /// confirmation height that is greater than the internal chain tip. ConfirmationHeightCannotBeGreaterThanTip { /// The internal chain's tip height. - tip_height: Option, + tip_height: u32, /// The introduced transaction's confirmation height. tx_height: u32, }, } -#[cfg(feature = "std")] -impl std::error::Error for NewError

{} - impl Wallet { /// Create a wallet from a `descriptor` (and an optional `change_descriptor`) and load related /// transaction data from `db`. @@ -273,12 +317,16 @@ impl Wallet { change_descriptor: Option, mut db: D, network: Network, + custom_genesis_hash: Option, ) -> Result> where D: PersistBackend, { let secp = Secp256k1::new(); - let mut chain = LocalChain::default(); + let genesis_hash = custom_genesis_hash + .or_else(|| bdk_chain::genesis::from_network(network)) + .ok_or(NewError::UnknownNetwork)?; + let mut chain = LocalChain::from_genesis_hash(genesis_hash); let mut indexed_graph = IndexedTxGraph::>::default(); @@ -310,7 +358,9 @@ impl Wallet { }; let changeset = db.load_from_persistence().map_err(NewError::Persist)?; - chain.apply_changeset(&changeset.chain); + chain + .apply_changeset(&changeset.chain) + .map_err(|_| NewError::InvalidPersistenceGenesis)?; indexed_graph.apply_changeset(changeset.indexed_tx_graph); let persist = Persist::new(db); @@ -437,7 +487,7 @@ impl Wallet { .graph() .filter_chain_unspents( &self.chain, - self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), + self.chain.tip().block_id(), self.indexed_graph.index.outpoints().iter().cloned(), ) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) @@ -449,7 +499,7 @@ impl Wallet { } /// Returns the latest checkpoint. - pub fn latest_checkpoint(&self) -> Option { + pub fn latest_checkpoint(&self) -> CheckPoint { self.chain.tip() } @@ -487,7 +537,7 @@ impl Wallet { .graph() .filter_chain_unspents( &self.chain, - self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), + self.chain.tip().block_id(), core::iter::once((spk_i, op)), ) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) @@ -660,7 +710,7 @@ impl Wallet { Some(CanonicalTx { chain_position: graph.get_chain_position( &self.chain, - self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), + self.chain.tip().block_id(), txid, )?, tx_node: graph.get_tx_node(txid)?, @@ -677,7 +727,7 @@ impl Wallet { pub fn insert_checkpoint( &mut self, block_id: BlockId, - ) -> Result + ) -> Result where D: PersistBackend, { @@ -721,7 +771,7 @@ impl Wallet { .range(height..) .next() .ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip { - tip_height: self.chain.tip().map(|b| b.height()), + tip_height: self.chain.tip().height(), tx_height: height, }) .map(|(&anchor_height, &hash)| ConfirmationTimeAnchor { @@ -757,10 +807,9 @@ impl Wallet { pub fn transactions( &self, ) -> impl Iterator> + '_ { - self.indexed_graph.graph().list_chain_txs( - &self.chain, - self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), - ) + self.indexed_graph + .graph() + .list_chain_txs(&self.chain, self.chain.tip().block_id()) } /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature @@ -768,7 +817,7 @@ impl Wallet { pub fn get_balance(&self) -> Balance { self.indexed_graph.graph().balance( &self.chain, - self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(), + self.chain.tip().block_id(), self.indexed_graph.index.outpoints().iter().cloned(), |&(k, _), _| k == KeychainKind::Internal, ) @@ -798,7 +847,7 @@ impl Wallet { /// ``` /// # use bdk::{Wallet, KeychainKind}; /// # use bdk::bitcoin::Network; - /// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet)?; + /// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet, None)?; /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* /// println!("secret_key: {}", secret_key); @@ -936,14 +985,14 @@ impl Wallet { _ => 1, }; - // We use a match here instead of a map_or_else as it's way more readable :) + // We use a match here instead of a unwrap_or_else as it's way more readable :) let current_height = match params.current_height { // If they didn't tell us the current height, we assume it's the latest sync height. - None => self - .chain - .tip() - .map(|cp| absolute::LockTime::from_height(cp.height()).expect("Invalid height")), - h => h, + None => { + let tip_height = self.chain.tip().height(); + absolute::LockTime::from_height(tip_height).expect("invalid height") + } + Some(h) => h, }; let lock_time = match params.locktime { @@ -952,7 +1001,7 @@ impl Wallet { // Fee sniping can be partially prevented by setting the timelock // to current_height. If we don't know the current_height, // we default to 0. - let fee_sniping_height = current_height.unwrap_or(absolute::LockTime::ZERO); + let fee_sniping_height = current_height; // We choose the biggest between the required nlocktime and the fee sniping // height @@ -1106,7 +1155,7 @@ impl Wallet { params.drain_wallet, params.manually_selected_only, params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee - current_height.map(absolute::LockTime::to_consensus_u32), + Some(current_height.to_consensus_u32()), ); // get drain script @@ -1248,7 +1297,7 @@ impl Wallet { ) -> Result, Error> { let graph = self.indexed_graph.graph(); let txout_index = &self.indexed_graph.index; - let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(); + let chain_tip = self.chain.tip().block_id(); let mut tx = graph .get_tx(txid) @@ -1483,7 +1532,7 @@ impl Wallet { psbt: &mut psbt::PartiallySignedTransaction, sign_options: SignOptions, ) -> Result { - let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(); + let chain_tip = self.chain.tip().block_id(); let tx = &psbt.unsigned_tx; let mut finished = true; @@ -1506,7 +1555,7 @@ impl Wallet { }); let current_height = sign_options .assume_height - .or(self.chain.tip().map(|b| b.height())); + .unwrap_or_else(|| self.chain.tip().height()); debug!( "Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}", @@ -1543,8 +1592,8 @@ impl Wallet { &mut tmp_input, ( PsbtInputSatisfier::new(psbt, n), - After::new(current_height, false), - Older::new(current_height, confirmation_height, false), + After::new(Some(current_height), false), + Older::new(Some(current_height), confirmation_height, false), ), ) { Ok(_) => { @@ -1652,7 +1701,7 @@ impl Wallet { must_only_use_confirmed_tx: bool, current_height: Option, ) -> (Vec, Vec) { - let chain_tip = self.chain.tip().map(|cp| cp.block_id()).unwrap_or_default(); + let chain_tip = self.chain.tip().block_id(); // must_spend <- manually selected utxos // may_spend <- all other available utxos let mut may_spend = self.get_available_utxos(); @@ -2055,6 +2104,7 @@ macro_rules! doctest_wallet { descriptor, Some(change_descriptor), Network::Regtest, + None, ) .unwrap(); let address = wallet.get_address(AddressIndex::New).address; diff --git a/crates/bdk/src/wallet/signer.rs b/crates/bdk/src/wallet/signer.rs index 68b2ecb154..58526e14f6 100644 --- a/crates/bdk/src/wallet/signer.rs +++ b/crates/bdk/src/wallet/signer.rs @@ -69,7 +69,7 @@ //! let custom_signer = CustomSigner::connect(); //! //! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; -//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?; +//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet, None)?; //! wallet.add_signer( //! KeychainKind::External, //! SignerOrdering(200), diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs index ee8ed74e15..0715f8e48e 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -16,7 +16,7 @@ pub fn get_funded_wallet_with_change( descriptor: &str, change: Option<&str>, ) -> (Wallet, bitcoin::Txid) { - let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap(); + let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest, None).unwrap(); let change_address = wallet.get_address(AddressIndex::New).address; let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5") .expect("address") diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index aad8c2db25..96e9245fa7 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -42,14 +42,14 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> } fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { - let height = match wallet.latest_checkpoint() { - Some(cp) => ConfirmationTime::Confirmed { - height: cp.height(), - time: 0, - }, - None => ConfirmationTime::Unconfirmed { last_seen: 0 }, + let latest_cp = wallet.latest_checkpoint(); + let height = latest_cp.height(); + let anchor = if height == 0 { + ConfirmationTime::Unconfirmed { last_seen: 0 } + } else { + ConfirmationTime::Confirmed { height, time: 0 } }; - receive_output(wallet, value, height) + receive_output(wallet, value, anchor) } // The satisfaction size of a P2WPKH is 112 WU = @@ -277,7 +277,7 @@ fn test_create_tx_fee_sniping_locktime_last_sync() { // If there's no current_height we're left with using the last sync height assert_eq!( psbt.unsigned_tx.lock_time.to_consensus_u32(), - wallet.latest_checkpoint().unwrap().height() + wallet.latest_checkpoint().height() ); } @@ -936,7 +936,7 @@ fn test_create_tx_policy_path_required() { #[test] fn test_create_tx_policy_path_no_csv() { let descriptors = get_test_wpkh(); - let mut wallet = Wallet::new_no_persist(descriptors, None, Network::Regtest).unwrap(); + let mut wallet = Wallet::new_no_persist(descriptors, None, Network::Regtest, None).unwrap(); let tx = Transaction { version: 0, @@ -1615,7 +1615,7 @@ fn test_bump_fee_drain_wallet() { .insert_tx( tx.clone(), ConfirmationTime::Confirmed { - height: wallet.latest_checkpoint().unwrap().height(), + height: wallet.latest_checkpoint().height(), time: 42_000, }, ) @@ -2464,7 +2464,7 @@ fn test_sign_nonstandard_sighash() { #[test] fn test_unused_address() { let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); + None, Network::Testnet, None).unwrap(); assert_eq!( wallet.get_address(LastUnused).to_string(), @@ -2479,7 +2479,7 @@ fn test_unused_address() { #[test] fn test_next_unused_address() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; - let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet).unwrap(); + let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet, None).unwrap(); assert_eq!(wallet.derivation_index(KeychainKind::External), None); assert_eq!( @@ -2506,7 +2506,7 @@ fn test_next_unused_address() { #[test] fn test_peek_address_at_index() { let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); + None, Network::Testnet, None).unwrap(); assert_eq!( wallet.get_address(Peek(1)).to_string(), @@ -2538,7 +2538,7 @@ fn test_peek_address_at_index() { #[test] fn test_peek_address_at_index_not_derivable() { let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)", - None, Network::Testnet).unwrap(); + None, Network::Testnet, None).unwrap(); assert_eq!( wallet.get_address(Peek(1)).to_string(), @@ -2559,7 +2559,7 @@ fn test_peek_address_at_index_not_derivable() { #[test] fn test_returns_index_and_address() { let mut wallet = Wallet::new_no_persist("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", - None, Network::Testnet).unwrap(); + None, Network::Testnet, None).unwrap(); // new index 0 assert_eq!( @@ -2629,6 +2629,7 @@ fn test_get_address() { Bip84(key, KeychainKind::External), Some(Bip84(key, KeychainKind::Internal)), Network::Regtest, + None, ) .unwrap(); @@ -2654,8 +2655,13 @@ fn test_get_address() { } ); - let mut wallet = - Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); + let mut wallet = Wallet::new_no_persist( + Bip84(key, KeychainKind::External), + None, + Network::Regtest, + None, + ) + .unwrap(); assert_eq!( wallet.get_internal_address(AddressIndex::New), @@ -2676,8 +2682,13 @@ fn test_get_address_no_reuse_single_descriptor() { use std::collections::HashSet; let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let mut wallet = - Wallet::new_no_persist(Bip84(key, KeychainKind::External), None, Network::Regtest).unwrap(); + let mut wallet = Wallet::new_no_persist( + Bip84(key, KeychainKind::External), + None, + Network::Regtest, + None, + ) + .unwrap(); let mut used_set = HashSet::new(); @@ -3143,7 +3154,8 @@ fn test_taproot_sign_derive_index_from_psbt() { // re-create the wallet with an empty db let wallet_empty = - Wallet::new_no_persist(get_test_tr_single_sig_xprv(), None, Network::Regtest).unwrap(); + Wallet::new_no_persist(get_test_tr_single_sig_xprv(), None, Network::Regtest, None) + .unwrap(); // signing with an empty db means that we will only look at the psbt to infer the // derivation index @@ -3243,7 +3255,7 @@ fn test_taproot_sign_non_default_sighash() { #[test] fn test_spend_coinbase() { let descriptor = get_test_wpkh(); - let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Regtest).unwrap(); + let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Regtest, None).unwrap(); let confirmation_height = 5; wallet diff --git a/crates/bitcoind_rpc/src/lib.rs b/crates/bitcoind_rpc/src/lib.rs index a5016ce6fb..07f6ea3d4c 100644 --- a/crates/bitcoind_rpc/src/lib.rs +++ b/crates/bitcoind_rpc/src/lib.rs @@ -25,7 +25,7 @@ pub struct Emitter<'c, C> { /// The checkpoint of the last-emitted block that is in the best chain. If it is later found /// that the block is no longer in the best chain, it will be popped off from here. - last_cp: Option, + last_cp: CheckPoint, /// The block result returned from rpc of the last-emitted block. As this result contains the /// next block's block hash (which we use to fetch the next block), we set this to `None` @@ -43,29 +43,12 @@ pub struct Emitter<'c, C> { } impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> { - /// Construct a new [`Emitter`] with the given RPC `client` and `start_height`. - /// - /// `start_height` is the block height to start emitting blocks from. - pub fn from_height(client: &'c C, start_height: u32) -> Self { + /// TODO + pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self { Self { client, start_height, - last_cp: None, - last_block: None, - last_mempool_time: 0, - last_mempool_tip: None, - } - } - - /// Construct a new [`Emitter`] with the given RPC `client` and `checkpoint`. - /// - /// `checkpoint` is used to find the latest block which is still part of the best chain. The - /// [`Emitter`] will emit blocks starting right above this block. - pub fn from_checkpoint(client: &'c C, checkpoint: CheckPoint) -> Self { - Self { - client, - start_height: 0, - last_cp: Some(checkpoint), + last_cp, last_block: None, last_mempool_time: 0, last_mempool_tip: None, @@ -134,7 +117,7 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> { .collect::, _>>()?; self.last_mempool_time = latest_time; - self.last_mempool_tip = self.last_cp.as_ref().map(|cp| cp.height()); + self.last_mempool_tip = Some(self.last_cp.height()); Ok(txs_to_emit) } @@ -156,7 +139,8 @@ enum PollResponse { /// Fetched block is not in the best chain. BlockNotInBestChain, AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint), - AgreementPointNotFound, + /// Force the genesis checkpoint down the receiver's throat. + AgreementPointNotFound(BlockHash), } fn poll_once(emitter: &Emitter) -> Result @@ -166,45 +150,50 @@ where let client = emitter.client; if let Some(last_res) = &emitter.last_block { - assert!( - emitter.last_cp.is_some(), - "must not have block result without last cp" - ); - - let next_hash = match last_res.nextblockhash { - None => return Ok(PollResponse::NoMoreBlocks), - Some(next_hash) => next_hash, + let next_hash = if last_res.height < emitter.start_height as _ { + // enforce start height + let next_hash = client.get_block_hash(emitter.start_height as _)?; + // make sure last emission is still in best chain + if client.get_block_hash(last_res.height as _)? != last_res.hash { + return Ok(PollResponse::BlockNotInBestChain); + } + next_hash + } else { + match last_res.nextblockhash { + None => return Ok(PollResponse::NoMoreBlocks), + Some(next_hash) => next_hash, + } }; let res = client.get_block_info(&next_hash)?; if res.confirmations < 0 { return Ok(PollResponse::BlockNotInBestChain); } - return Ok(PollResponse::Block(res)); - } - - if emitter.last_cp.is_none() { - let hash = client.get_block_hash(emitter.start_height as _)?; - let res = client.get_block_info(&hash)?; - if res.confirmations < 0 { - return Ok(PollResponse::BlockNotInBestChain); - } return Ok(PollResponse::Block(res)); } - for cp in emitter.last_cp.iter().flat_map(CheckPoint::iter) { - let res = client.get_block_info(&cp.hash())?; - if res.confirmations < 0 { - // block is not in best chain - continue; - } + for cp in emitter.last_cp.iter() { + let res = match client.get_block_info(&cp.hash()) { + // block not in best chain + Ok(res) if res.confirmations < 0 => continue, + Ok(res) => res, + Err(e) if e.is_not_found_error() => { + if cp.height() > 0 { + continue; + } + // if we can't find genesis block, we can't create an update that connects + break; + } + Err(e) => return Err(e), + }; // agreement point found return Ok(PollResponse::AgreementFound(res, cp)); } - Ok(PollResponse::AgreementPointNotFound) + let genesis_hash = client.get_block_hash(0)?; + Ok(PollResponse::AgreementPointNotFound(genesis_hash)) } fn poll( @@ -222,25 +211,12 @@ where let hash = res.hash; let item = get_item(&hash)?; - let this_id = BlockId { height, hash }; - let prev_id = res.previousblockhash.map(|prev_hash| BlockId { - height: height - 1, - hash: prev_hash, - }); - - match (&mut emitter.last_cp, prev_id) { - (Some(cp), _) => *cp = cp.clone().push(this_id).expect("must push"), - (last_cp, None) => *last_cp = Some(CheckPoint::new(this_id)), - // When the receiver constructs a local_chain update from a block, the previous - // checkpoint is also included in the update. We need to reflect this state in - // `Emitter::last_cp` as well. - (last_cp, Some(prev_id)) => { - *last_cp = Some(CheckPoint::new(prev_id).push(this_id).expect("must push")) - } - } - + emitter.last_cp = emitter + .last_cp + .clone() + .push(BlockId { height, hash }) + .expect("must push"); emitter.last_block = Some(res); - return Ok(Some((height, item))); } PollResponse::NoMoreBlocks => { @@ -254,9 +230,6 @@ where PollResponse::AgreementFound(res, cp) => { let agreement_h = res.height as u32; - // get rid of evicted blocks - emitter.last_cp = Some(cp); - // The tip during the last mempool emission needs to in the best chain, we reduce // it if it is not. if let Some(h) = emitter.last_mempool_tip.as_mut() { @@ -264,15 +237,17 @@ where *h = agreement_h; } } + + // get rid of evicted blocks + emitter.last_cp = cp; emitter.last_block = Some(res); continue; } - PollResponse::AgreementPointNotFound => { - // We want to clear `last_cp` and set `start_height` to the first checkpoint's - // height. This way, the first checkpoint in `LocalChain` can be replaced. - if let Some(last_cp) = emitter.last_cp.take() { - emitter.start_height = last_cp.height(); - } + PollResponse::AgreementPointNotFound(genesis_hash) => { + emitter.last_cp = CheckPoint::new(BlockId { + height: 0, + hash: genesis_hash, + }); emitter.last_block = None; continue; } diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index f0bbd3d158..22df5100b5 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -188,8 +188,8 @@ fn block_to_chain_update(block: &bitcoin::Block, height: u32) -> local_chain::Up #[test] pub fn test_sync_local_chain() -> anyhow::Result<()> { let env = TestEnv::new()?; - let mut local_chain = LocalChain::default(); - let mut emitter = Emitter::from_height(&env.client, 0); + let mut local_chain = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?); + let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0); // mine some blocks and returned the actual block hashes let exp_hashes = { @@ -296,7 +296,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> { env.mine_blocks(101, None)?; println!("mined blocks!"); - let mut chain = LocalChain::default(); + let mut chain = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?); let mut indexed_tx_graph = IndexedTxGraph::::new({ let mut index = SpkTxOutIndex::::default(); index.insert_spk(0, addr_0.script_pubkey()); @@ -305,7 +305,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> { index }); - let emitter = &mut Emitter::from_height(&env.client, 0); + let emitter = &mut Emitter::new(&env.client, chain.tip(), 0); while let Some((height, block)) = emitter.next_block()? { let _ = chain.apply_update(block_to_chain_update(&block, height))?; @@ -393,7 +393,14 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> { const CHAIN_TIP_HEIGHT: usize = 110; let env = TestEnv::new()?; - let mut emitter = Emitter::from_height(&env.client, EMITTER_START_HEIGHT as _); + let mut emitter = Emitter::new( + &env.client, + CheckPoint::new(BlockId { + height: 0, + hash: env.client.get_block_hash(0)?, + }), + EMITTER_START_HEIGHT as _, + ); env.mine_blocks(CHAIN_TIP_HEIGHT, None)?; while emitter.next_header()?.is_some() {} @@ -442,9 +449,7 @@ fn get_balance( recv_chain: &LocalChain, recv_graph: &IndexedTxGraph>, ) -> anyhow::Result { - let chain_tip = recv_chain - .tip() - .map_or(BlockId::default(), |cp| cp.block_id()); + let chain_tip = recv_chain.tip().block_id(); let outpoints = recv_graph.index.outpoints().clone(); let balance = recv_graph .graph() @@ -461,7 +466,14 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { const SEND_AMOUNT: Amount = Amount::from_sat(10_000); let env = TestEnv::new()?; - let mut emitter = Emitter::from_height(&env.client, 0); + let mut emitter = Emitter::new( + &env.client, + CheckPoint::new(BlockId { + height: 0, + hash: env.client.get_block_hash(0)?, + }), + 0, + ); // setup addresses let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked(); @@ -469,7 +481,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?; // setup receiver - let mut recv_chain = LocalChain::default(); + let mut recv_chain = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?); let mut recv_graph = IndexedTxGraph::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); @@ -542,7 +554,14 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> { const MEMPOOL_TX_COUNT: usize = 2; let env = TestEnv::new()?; - let mut emitter = Emitter::from_height(&env.client, 0); + let mut emitter = Emitter::new( + &env.client, + CheckPoint::new(BlockId { + height: 0, + hash: env.client.get_block_hash(0)?, + }), + 0, + ); // mine blocks and sync up emitter let addr = env.client.get_new_address(None, None)?.assume_checked(); @@ -597,7 +616,14 @@ fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<() const MEMPOOL_TX_COUNT: usize = 21; let env = TestEnv::new()?; - let mut emitter = Emitter::from_height(&env.client, 0); + let mut emitter = Emitter::new( + &env.client, + CheckPoint::new(BlockId { + height: 0, + hash: env.client.get_block_hash(0)?, + }), + 0, + ); // mine blocks to get initial balance, sync emitter up to tip let addr = env.client.get_new_address(None, None)?.assume_checked(); @@ -674,7 +700,14 @@ fn mempool_during_reorg() -> anyhow::Result<()> { const PREMINE_COUNT: usize = 101; let env = TestEnv::new()?; - let mut emitter = Emitter::from_height(&env.client, 0); + let mut emitter = Emitter::new( + &env.client, + CheckPoint::new(BlockId { + height: 0, + hash: env.client.get_block_hash(0)?, + }), + 0, + ); // mine blocks to get initial balance let addr = env.client.get_new_address(None, None)?.assume_checked(); @@ -789,7 +822,14 @@ fn no_agreement_point() -> anyhow::Result<()> { let env = TestEnv::new()?; // start height is 99 - let mut emitter = Emitter::from_height(&env.client, (PREMINE_COUNT - 2) as u32); + let mut emitter = Emitter::new( + &env.client, + CheckPoint::new(BlockId { + height: 0, + hash: env.client.get_block_hash(0)?, + }), + (PREMINE_COUNT - 2) as u32, + ); // mine 101 blocks env.mine_blocks(PREMINE_COUNT, None)?; diff --git a/crates/chain/src/chain_oracle.rs b/crates/chain/src/chain_oracle.rs index e736be0354..038025619f 100644 --- a/crates/chain/src/chain_oracle.rs +++ b/crates/chain/src/chain_oracle.rs @@ -21,5 +21,5 @@ pub trait ChainOracle { ) -> Result, Self::Error>; /// Get the best chain's chain tip. - fn get_chain_tip(&self) -> Result, Self::Error>; + fn get_chain_tip(&self) -> Result; } diff --git a/crates/chain/src/genesis.rs b/crates/chain/src/genesis.rs new file mode 100644 index 0000000000..6af35fa815 --- /dev/null +++ b/crates/chain/src/genesis.rs @@ -0,0 +1,54 @@ +//! Bitcoin genesis hashes. +//! +//! This is blatantly copied from +//! https://github.com/cloudhead/nakamoto/blob/master/common/src/block/genesis.rs + +use bitcoin::{hashes::Hash, BlockHash, Network}; + +#[rustfmt::skip] +/// Bitcoin mainnet genesis hash. +pub const MAINNET: [u8; 32] = [ + 0x6f, 0xe2, 0x8c, 0x0a, 0xb6, 0xf1, 0xb3, 0x72, + 0xc1, 0xa6, 0xa2, 0x46, 0xae, 0x63, 0xf7, 0x4f, + 0x93, 0x1e, 0x83, 0x65, 0xe1, 0x5a, 0x08, 0x9c, + 0x68, 0xd6, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +/// Bitcoin testnet genesis hash. +pub const TESTNET: [u8; 32] = [ + 0x43, 0x49, 0x7f, 0xd7, 0xf8, 0x26, 0x95, 0x71, + 0x08, 0xf4, 0xa3, 0x0f, 0xd9, 0xce, 0xc3, 0xae, + 0xba, 0x79, 0x97, 0x20, 0x84, 0xe9, 0x0e, 0xad, + 0x01, 0xea, 0x33, 0x09, 0x00, 0x00, 0x00, 0x00, +]; + +#[rustfmt::skip] +/// Bitcoin regtest genesis hash. +pub const REGTEST: [u8; 32] = [ + 0x06, 0x22, 0x6e, 0x46, 0x11, 0x1a, 0x0b, 0x59, + 0xca, 0xaf, 0x12, 0x60, 0x43, 0xeb, 0x5b, 0xbf, + 0x28, 0xc3, 0x4f, 0x3a, 0x5e, 0x33, 0x2a, 0x1f, + 0xc7, 0xb2, 0xb7, 0x3c, 0xf1, 0x88, 0x91, 0x0f, +]; + +#[rustfmt::skip] +/// Bitcoin signet genesis hash. +pub const SIGNET: [u8; 32] = [ + 0xf6, 0x1e, 0xee, 0x3b, 0x63, 0xa3, 0x80, 0xa4, + 0x77, 0xa0, 0x63, 0xaf, 0x32, 0xb2, 0xbb, 0xc9, + 0x7c, 0x9f, 0xf9, 0xf0, 0x1f, 0x2c, 0x42, 0x25, + 0xe9, 0x73, 0x98, 0x81, 0x08, 0x00, 0x00, 0x00 +]; + +/// Get the associated genesis [`BlockHash`] from the given `network`. +pub fn from_network(network: Network) -> Option { + let raw_hash = match network { + Network::Bitcoin => MAINNET, + Network::Testnet => TESTNET, + Network::Signet => SIGNET, + Network::Regtest => REGTEST, + _ => return None, + }; + Some(BlockHash::from_byte_array(raw_hash)) +} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index ed167ebf6c..bc004ec1b1 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -38,6 +38,7 @@ mod chain_oracle; pub use chain_oracle::*; mod persist; pub use persist::*; +pub mod genesis; #[doc(hidden)] pub mod example_utils; diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 094b77424d..c01898ff4a 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -179,9 +179,9 @@ pub struct Update { } /// This is a local implementation of [`ChainOracle`]. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct LocalChain { - tip: Option, + tip: CheckPoint, index: BTreeMap, } @@ -197,12 +197,6 @@ impl From for BTreeMap { } } -impl From> for LocalChain { - fn from(value: BTreeMap) -> Self { - Self::from_blocks(value) - } -} - impl ChainOracle for LocalChain { type Error = Infallible; @@ -225,39 +219,68 @@ impl ChainOracle for LocalChain { ) } - fn get_chain_tip(&self) -> Result, Self::Error> { - Ok(self.tip.as_ref().map(|tip| tip.block_id())) + fn get_chain_tip(&self) -> Result { + Ok(self.tip.block_id()) } } impl LocalChain { + /// Get the genesis hash. + pub fn genesis_hash(&self) -> BlockHash { + self.index.get(&0).copied().expect("must have genesis hash") + } + + /// Construct [`LocalChain`] from genesis `hash`. + pub fn from_genesis_hash(hash: BlockHash) -> Self { + let height = 0; + Self { + tip: CheckPoint::new(BlockId { height, hash }), + index: core::iter::once((height, hash)).collect(), + } + } + /// Construct a [`LocalChain`] from an initial `changeset`. - pub fn from_changeset(changeset: ChangeSet) -> Self { - let mut chain = Self::default(); - chain.apply_changeset(&changeset); + pub fn from_changeset(changeset: ChangeSet) -> Result { + let genesis_entry = changeset.get(&0).copied().flatten(); + let genesis_hash = match genesis_entry { + Some(hash) => hash, + None => return Err(MissingGenesisError), + }; + + let mut chain = Self::from_genesis_hash(genesis_hash); + chain.apply_changeset(&changeset)?; debug_assert!(chain._check_index_is_consistent_with_tip()); debug_assert!(chain._check_changeset_is_applied(&changeset)); - chain + Ok(chain) } /// Construct a [`LocalChain`] from a given `checkpoint` tip. - pub fn from_tip(tip: CheckPoint) -> Self { + pub fn from_tip(tip: CheckPoint) -> Result { let mut chain = Self { - tip: Some(tip), - ..Default::default() + tip, + index: BTreeMap::new(), }; chain.reindex(0); + + if chain.index.get(&0).copied().is_none() { + return Err(MissingGenesisError); + } + debug_assert!(chain._check_index_is_consistent_with_tip()); - chain + Ok(chain) } /// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`]. /// /// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are /// all of the same chain. - pub fn from_blocks(blocks: BTreeMap) -> Self { + pub fn from_blocks(blocks: BTreeMap) -> Result { + if !blocks.contains_key(&0) { + return Err(MissingGenesisError); + } + let mut tip: Option = None; for block in &blocks { @@ -272,25 +295,20 @@ impl LocalChain { } } - let chain = Self { index: blocks, tip }; + let chain = Self { + index: blocks, + tip: tip.expect("already checked to have genesis"), + }; debug_assert!(chain._check_index_is_consistent_with_tip()); - - chain + Ok(chain) } /// Get the highest checkpoint. - pub fn tip(&self) -> Option { + pub fn tip(&self) -> CheckPoint { self.tip.clone() } - /// Returns whether the [`LocalChain`] is empty (has no checkpoints). - pub fn is_empty(&self) -> bool { - let res = self.tip.is_none(); - debug_assert_eq!(res, self.index.is_empty()); - res - } - /// Applies the given `update` to the chain. /// /// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`. @@ -312,34 +330,58 @@ impl LocalChain { /// /// [module-level documentation]: crate::local_chain pub fn apply_update(&mut self, update: Update) -> Result { - match self.tip() { - Some(original_tip) => { - let changeset = merge_chains( - original_tip, - update.tip.clone(), - update.introduce_older_blocks, - )?; - self.apply_changeset(&changeset); - - // return early as `apply_changeset` already calls `check_consistency` - Ok(changeset) - } - None => { - *self = Self::from_tip(update.tip); - let changeset = self.initial_changeset(); - - debug_assert!(self._check_index_is_consistent_with_tip()); - debug_assert!(self._check_changeset_is_applied(&changeset)); - Ok(changeset) - } + // match self.tip() { + // Some(original_tip) => { + // let changeset = merge_chains( + // original_tip: self.tip.clone(), + // update.tip.clone(), + // update.introduce_older_blocks, + // )?; + // self.apply_changeset(&changeset); + // + // // return early as `apply_changeset` already calls `check_consistency` + // Ok(changeset) + // } + // None => { + // *self = Self::from_tip(update.tip); + // let changeset = self.initial_changeset(); + // + // debug_assert!(self._check_index_is_consistent_with_tip()); + // debug_assert!(self._check_changeset_is_applied(&changeset)); + // Ok(changeset) + // } + // } + + let mut changeset = merge_chains( + self.tip.clone(), + update.tip.clone(), + update.introduce_older_blocks, + )?; + self.apply_changeset(&changeset) + .map_err(|_| CannotConnectError { + try_include_height: 0, + })?; + + // Append genesis to `changeset` if original chain has length of 1 and update chain + // has length > 1. TODO: Explain why + let self_only_genesis = self.index.len() == 1; + let update_extends_chain = update.tip.prev().is_some(); + if self_only_genesis && update_extends_chain { + changeset.insert(0, self.index.get(&0).copied()); } + + // return early as `apply_changeset` already calls `check_consistency` + Ok(changeset) } /// Apply the given `changeset`. - pub fn apply_changeset(&mut self, changeset: &ChangeSet) { + pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> { if let Some(start_height) = changeset.keys().next().cloned() { + // changes after point of agreement let mut extension = BTreeMap::default(); + // point of agreement let mut base: Option = None; + for cp in self.iter_checkpoints() { if cp.height() >= start_height { extension.insert(cp.height(), cp.hash()); @@ -359,12 +401,12 @@ impl LocalChain { } }; } + let new_tip = match base { - Some(base) => Some( - base.extend(extension.into_iter().map(BlockId::from)) - .expect("extension is strictly greater than base"), - ), - None => LocalChain::from_blocks(extension).tip(), + Some(base) => base + .extend(extension.into_iter().map(BlockId::from)) + .expect("extension is strictly greater than base"), + None => LocalChain::from_blocks(extension)?.tip(), }; self.tip = new_tip; self.reindex(start_height); @@ -372,6 +414,8 @@ impl LocalChain { debug_assert!(self._check_index_is_consistent_with_tip()); debug_assert!(self._check_changeset_is_applied(changeset)); } + + Ok(()) } /// Insert a [`BlockId`]. @@ -379,13 +423,13 @@ impl LocalChain { /// # Errors /// /// Replacing the block hash of an existing checkpoint will result in an error. - pub fn insert_block(&mut self, block_id: BlockId) -> Result { + pub fn insert_block(&mut self, block_id: BlockId) -> Result { if let Some(&original_hash) = self.index.get(&block_id.height) { if original_hash != block_id.hash { - return Err(InsertBlockError { + return Err(AlterCheckPointError { height: block_id.height, original_hash, - update_hash: block_id.hash, + update_hash: Some(block_id.hash), }); } else { return Ok(ChangeSet::default()); @@ -394,7 +438,12 @@ impl LocalChain { let mut changeset = ChangeSet::default(); changeset.insert(block_id.height, Some(block_id.hash)); - self.apply_changeset(&changeset); + self.apply_changeset(&changeset) + .map_err(|_| AlterCheckPointError { + height: 0, + original_hash: self.genesis_hash(), + update_hash: changeset.get(&0).cloned().flatten(), + })?; Ok(changeset) } @@ -418,7 +467,7 @@ impl LocalChain { /// Iterate over checkpoints in descending height order. pub fn iter_checkpoints(&self) -> CheckPointIter { CheckPointIter { - current: self.tip.as_ref().map(|tip| tip.0.clone()), + current: Some(self.tip.0.clone()), } } @@ -431,7 +480,6 @@ impl LocalChain { let tip_history = self .tip .iter() - .flat_map(CheckPoint::iter) .map(|cp| (cp.height(), cp.hash())) .collect::>(); self.index == tip_history @@ -447,29 +495,52 @@ impl LocalChain { } } -/// Represents a failure when trying to insert a checkpoint into [`LocalChain`]. +/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint. #[derive(Clone, Debug, PartialEq)] -pub struct InsertBlockError { - /// The checkpoints' height. - pub height: u32, - /// Original checkpoint's block hash. - pub original_hash: BlockHash, - /// Update checkpoint's block hash. - pub update_hash: BlockHash, -} +pub struct MissingGenesisError; -impl core::fmt::Display for InsertBlockError { +impl core::fmt::Display for MissingGenesisError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!( f, - "failed to insert block at height {} as block hashes conflict: original={}, update={}", - self.height, self.original_hash, self.update_hash + "cannot construct `LocalChain` without a genesis checkpoint" ) } } #[cfg(feature = "std")] -impl std::error::Error for InsertBlockError {} +impl std::error::Error for MissingGenesisError {} + +/// Represents a failure when trying to insert/remove a checkpoint to/from [`LocalChain`]. +#[derive(Clone, Debug, PartialEq)] +pub struct AlterCheckPointError { + /// The checkpoint's height. + pub height: u32, + /// The original checkpoint's block hash which cannot be replaced/removed. + pub original_hash: BlockHash, + /// The attempted update to the `original_block` hash. + pub update_hash: Option, +} + +impl core::fmt::Display for AlterCheckPointError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self.update_hash { + Some(update_hash) => write!( + f, + "failed to insert block at height {}: original={} update={}", + self.height, self.original_hash, update_hash + ), + None => write!( + f, + "failed to remove block at height {}: original={}", + self.height, self.original_hash + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for AlterCheckPointError {} /// Occurs when an update does not have a common checkpoint with the original chain. #[derive(Clone, Debug, PartialEq)] diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs index d3db819103..cfb8b7c449 100644 --- a/crates/chain/tests/common/mod.rs +++ b/crates/chain/tests/common/mod.rs @@ -23,6 +23,7 @@ macro_rules! local_chain { [ $(($height:expr, $block_hash:expr)), * ] => {{ #[allow(unused_mut)] bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect()) + .expect("chain must have genesis block") }}; } @@ -32,8 +33,8 @@ macro_rules! chain_update { #[allow(unused_mut)] bdk_chain::local_chain::Update { tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect()) - .tip() - .expect("must have tip"), + .expect("chain must have genesis block") + .tip(), introduce_older_blocks: true, } }}; diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 3dc22ef5ba..ec250c95c3 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -1,7 +1,7 @@ #[macro_use] mod common; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use bdk_chain::{ indexed_tx_graph::{self, IndexedTxGraph}, @@ -9,9 +9,7 @@ use bdk_chain::{ local_chain::LocalChain, tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor, }; -use bitcoin::{ - secp256k1::Secp256k1, BlockHash, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, -}; +use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut}; use miniscript::Descriptor; /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented @@ -112,11 +110,8 @@ fn insert_relevant_txs() { fn test_list_owned_txouts() { // Create Local chains - let local_chain = LocalChain::from( - (0..150) - .map(|i| (i as u32, h!("random"))) - .collect::>(), - ); + let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect()) + .expect("must have genesis hash"); // Initiate IndexedTxGraph diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs index 9ea8b7f3a6..f76ea0ff65 100644 --- a/crates/chain/tests/test_local_chain.rs +++ b/crates/chain/tests/test_local_chain.rs @@ -1,4 +1,6 @@ -use bdk_chain::local_chain::{CannotConnectError, ChangeSet, InsertBlockError, LocalChain, Update}; +use bdk_chain::local_chain::{ + AlterCheckPointError, CannotConnectError, ChangeSet, LocalChain, Update, +}; use bitcoin::BlockHash; #[macro_use] @@ -68,7 +70,7 @@ fn update_local_chain() { [ TestLocalChain { name: "add first tip", - chain: local_chain![], + chain: local_chain![(0, h!("A"))], update: chain_update![(0, h!("A"))], exp: ExpectedResult::Ok { changeset: &[(0, Some(h!("A")))], @@ -294,44 +296,44 @@ fn local_chain_insert_block() { struct TestCase { original: LocalChain, insert: (u32, BlockHash), - expected_result: Result, + expected_result: Result, expected_final: LocalChain, } let test_cases = [ TestCase { - original: local_chain![], + original: local_chain![(0, h!("_"))], insert: (5, h!("block5")), expected_result: Ok([(5, Some(h!("block5")))].into()), - expected_final: local_chain![(5, h!("block5"))], + expected_final: local_chain![(0, h!("_")), (5, h!("block5"))], }, TestCase { - original: local_chain![(3, h!("A"))], + original: local_chain![(0, h!("_")), (3, h!("A"))], insert: (4, h!("B")), expected_result: Ok([(4, Some(h!("B")))].into()), - expected_final: local_chain![(3, h!("A")), (4, h!("B"))], + expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))], }, TestCase { - original: local_chain![(4, h!("B"))], + original: local_chain![(0, h!("_")), (4, h!("B"))], insert: (3, h!("A")), expected_result: Ok([(3, Some(h!("A")))].into()), - expected_final: local_chain![(3, h!("A")), (4, h!("B"))], + expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))], }, TestCase { - original: local_chain![(2, h!("K"))], + original: local_chain![(0, h!("_")), (2, h!("K"))], insert: (2, h!("K")), expected_result: Ok([].into()), - expected_final: local_chain![(2, h!("K"))], + expected_final: local_chain![(0, h!("_")), (2, h!("K"))], }, TestCase { - original: local_chain![(2, h!("K"))], + original: local_chain![(0, h!("_")), (2, h!("K"))], insert: (2, h!("J")), - expected_result: Err(InsertBlockError { + expected_result: Err(AlterCheckPointError { height: 2, original_hash: h!("K"), - update_hash: h!("J"), + update_hash: Some(h!("J")), }), - expected_final: local_chain![(2, h!("K"))], + expected_final: local_chain![(0, h!("_")), (2, h!("K"))], }, ]; diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index a0efd1004c..32105e0000 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -511,11 +511,13 @@ fn test_calculate_fee_on_coinbase() { // where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc. #[test] fn test_walk_ancestors() { - let local_chain: LocalChain = (0..=20) - .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) - .collect::>() - .into(); - let tip = local_chain.tip().expect("must have tip"); + let local_chain = LocalChain::from_blocks( + (0..=20) + .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) + .collect(), + ) + .expect("must contain genesis hash"); + let tip = local_chain.tip(); let tx_a0 = Transaction { input: vec![TxIn { @@ -839,11 +841,13 @@ fn test_descendants_no_repeat() { #[test] fn test_chain_spends() { - let local_chain: LocalChain = (0..=100) - .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) - .collect::>() - .into(); - let tip = local_chain.tip().expect("must have tip"); + let local_chain = LocalChain::from_blocks( + (0..=100) + .map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes()))) + .collect(), + ) + .expect("must have genesis hash"); + let tip = local_chain.tip(); // The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx. // The parent tx is confirmed at block 95. @@ -1078,7 +1082,7 @@ fn test_missing_blocks() { g }, chain: { - let mut c = LocalChain::default(); + let mut c = LocalChain::from_genesis_hash(h!("genesis")); for (height, hash) in chain { let _ = c.insert_block(BlockId { height: *height, diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 1794d8452f..a1f1fe6798 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -39,10 +39,7 @@ fn test_tx_conflict_handling() { (5, h!("F")), (6, h!("G")) ); - let chain_tip = local_chain - .tip() - .map(|cp| cp.block_id()) - .unwrap_or_default(); + let chain_tip = local_chain.tip().block_id(); let scenarios = [ Scenario { diff --git a/crates/electrum/src/electrum_ext.rs b/crates/electrum/src/electrum_ext.rs index 7ac16a046f..a5eda2ac68 100644 --- a/crates/electrum/src/electrum_ext.rs +++ b/crates/electrum/src/electrum_ext.rs @@ -148,7 +148,7 @@ pub trait ElectrumExt { /// single batch request. fn scan( &self, - prev_tip: Option, + prev_tip: CheckPoint, keychain_spks: BTreeMap>, txids: impl IntoIterator, outpoints: impl IntoIterator, @@ -161,7 +161,7 @@ pub trait ElectrumExt { /// [`scan`]: ElectrumExt::scan fn scan_without_keychain( &self, - prev_tip: Option, + prev_tip: CheckPoint, misc_spks: impl IntoIterator, txids: impl IntoIterator, outpoints: impl IntoIterator, @@ -188,7 +188,7 @@ pub trait ElectrumExt { impl ElectrumExt for Client { fn scan( &self, - prev_tip: Option, + prev_tip: CheckPoint, keychain_spks: BTreeMap>, txids: impl IntoIterator, outpoints: impl IntoIterator, @@ -289,17 +289,15 @@ impl ElectrumExt for Client { /// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. fn construct_update_tip( client: &Client, - prev_tip: Option, + prev_tip: CheckPoint, ) -> Result<(CheckPoint, Option), Error> { let HeaderNotification { height, .. } = client.block_headers_subscribe()?; let new_tip_height = height as u32; // If electrum returns a tip height that is lower than our previous tip, then checkpoints do // not need updating. We just return the previous tip and use that as the point of agreement. - if let Some(prev_tip) = prev_tip.as_ref() { - if new_tip_height < prev_tip.height() { - return Ok((prev_tip.clone(), Some(prev_tip.height()))); - } + if new_tip_height < prev_tip.height() { + return Ok((prev_tip.clone(), Some(prev_tip.height()))); } // Atomically fetch the latest `ASSUME_FINAL_DEPTH` count of blocks from Electrum. We use this @@ -317,7 +315,7 @@ fn construct_update_tip( // Find the "point of agreement" (if any). let agreement_cp = { let mut agreement_cp = Option::::None; - for cp in prev_tip.iter().flat_map(CheckPoint::iter) { + for cp in prev_tip.iter() { let cp_block = cp.block_id(); let hash = match new_blocks.get(&cp_block.height) { Some(&hash) => hash, diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 279c8e962c..02a9b77edd 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -32,7 +32,7 @@ pub trait EsploraAsyncExt { #[allow(clippy::result_large_err)] async fn update_local_chain( &self, - local_tip: Option, + local_tip: CheckPoint, request_heights: impl IntoIterator + Send> + Send, ) -> Result; @@ -95,7 +95,7 @@ pub trait EsploraAsyncExt { impl EsploraAsyncExt for esplora_client::AsyncClient { async fn update_local_chain( &self, - local_tip: Option, + local_tip: CheckPoint, request_heights: impl IntoIterator + Send> + Send, ) -> Result { let request_heights = request_heights.into_iter().collect::>(); @@ -129,41 +129,39 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { let earliest_agreement_cp = { let mut earliest_agreement_cp = Option::::None; - if let Some(local_tip) = local_tip { - let local_tip_height = local_tip.height(); - for local_cp in local_tip.iter() { - let local_block = local_cp.block_id(); + let local_tip_height = local_tip.height(); + for local_cp in local_tip.iter() { + let local_block = local_cp.block_id(); - // the updated hash (block hash at this height after the update), can either be: - // 1. a block that already existed in `fetched_blocks` - // 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH - // 3. otherwise we can freshly fetch the block from remote, which is safe as it - // is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the - // remote tip - let updated_hash = match fetched_blocks.entry(local_block.height) { - btree_map::Entry::Occupied(entry) => *entry.get(), - btree_map::Entry::Vacant(entry) => *entry.insert( - if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH { - local_block.hash - } else { - self.get_block_hash(local_block.height).await? - }, - ), - }; + // the updated hash (block hash at this height after the update), can either be: + // 1. a block that already existed in `fetched_blocks` + // 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH + // 3. otherwise we can freshly fetch the block from remote, which is safe as it + // is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the + // remote tip + let updated_hash = match fetched_blocks.entry(local_block.height) { + btree_map::Entry::Occupied(entry) => *entry.get(), + btree_map::Entry::Vacant(entry) => *entry.insert( + if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH { + local_block.hash + } else { + self.get_block_hash(local_block.height).await? + }, + ), + }; - // since we may introduce blocks below the point of agreement, we cannot break - // here unconditionally - we only break if we guarantee there are no new heights - // below our current local checkpoint - if local_block.hash == updated_hash { - earliest_agreement_cp = Some(local_cp); + // since we may introduce blocks below the point of agreement, we cannot break + // here unconditionally - we only break if we guarantee there are no new heights + // below our current local checkpoint + if local_block.hash == updated_hash { + earliest_agreement_cp = Some(local_cp); - let first_new_height = *fetched_blocks - .keys() - .next() - .expect("must have at least one new block"); - if first_new_height >= local_block.height { - break; - } + let first_new_height = *fetched_blocks + .keys() + .next() + .expect("must have at least one new block"); + if first_new_height >= local_block.height { + break; } } } diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 17c364ca9c..f7cecdc9c3 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -30,7 +30,7 @@ pub trait EsploraExt { #[allow(clippy::result_large_err)] fn update_local_chain( &self, - local_tip: Option, + local_tip: CheckPoint, request_heights: impl IntoIterator, ) -> Result; @@ -87,7 +87,7 @@ pub trait EsploraExt { impl EsploraExt for esplora_client::BlockingClient { fn update_local_chain( &self, - local_tip: Option, + local_tip: CheckPoint, request_heights: impl IntoIterator, ) -> Result { let request_heights = request_heights.into_iter().collect::>(); @@ -120,41 +120,39 @@ impl EsploraExt for esplora_client::BlockingClient { let earliest_agreement_cp = { let mut earliest_agreement_cp = Option::::None; - if let Some(local_tip) = local_tip { - let local_tip_height = local_tip.height(); - for local_cp in local_tip.iter() { - let local_block = local_cp.block_id(); + let local_tip_height = local_tip.height(); + for local_cp in local_tip.iter() { + let local_block = local_cp.block_id(); - // the updated hash (block hash at this height after the update), can either be: - // 1. a block that already existed in `fetched_blocks` - // 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH - // 3. otherwise we can freshly fetch the block from remote, which is safe as it - // is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the - // remote tip - let updated_hash = match fetched_blocks.entry(local_block.height) { - btree_map::Entry::Occupied(entry) => *entry.get(), - btree_map::Entry::Vacant(entry) => *entry.insert( - if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH { - local_block.hash - } else { - self.get_block_hash(local_block.height)? - }, - ), - }; + // the updated hash (block hash at this height after the update), can either be: + // 1. a block that already existed in `fetched_blocks` + // 2. a block that exists locally and at least has a depth of ASSUME_FINAL_DEPTH + // 3. otherwise we can freshly fetch the block from remote, which is safe as it + // is guaranteed that this would be at or below ASSUME_FINAL_DEPTH from the + // remote tip + let updated_hash = match fetched_blocks.entry(local_block.height) { + btree_map::Entry::Occupied(entry) => *entry.get(), + btree_map::Entry::Vacant(entry) => *entry.insert( + if local_tip_height - local_block.height >= ASSUME_FINAL_DEPTH { + local_block.hash + } else { + self.get_block_hash(local_block.height)? + }, + ), + }; - // since we may introduce blocks below the point of agreement, we cannot break - // here unconditionally - we only break if we guarantee there are no new heights - // below our current local checkpoint - if local_block.hash == updated_hash { - earliest_agreement_cp = Some(local_cp); + // since we may introduce blocks below the point of agreement, we cannot break + // here unconditionally - we only break if we guarantee there are no new heights + // below our current local checkpoint + if local_block.hash == updated_hash { + earliest_agreement_cp = Some(local_cp); - let first_new_height = *fetched_blocks - .keys() - .next() - .expect("must have at least one new block"); - if first_new_height >= local_block.height { - break; - } + let first_new_height = *fetched_blocks + .keys() + .next() + .expect("must have at least one new block"); + if first_new_height >= local_block.height { + break; } } } diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index 32735022db..9f086f83f3 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -131,7 +131,7 @@ fn main() -> anyhow::Result<()> { start.elapsed().as_secs_f32() ); - let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)); + let chain = Mutex::new(LocalChain::from_changeset(init_changeset.0)?); println!( "[{:>10}s] loaded local chain from changeset", start.elapsed().as_secs_f32() @@ -170,10 +170,7 @@ fn main() -> anyhow::Result<()> { let chain_tip = chain.lock().unwrap().tip(); let rpc_client = rpc_args.new_client()?; - let mut emitter = match chain_tip { - Some(cp) => Emitter::from_checkpoint(&rpc_client, cp), - None => Emitter::from_height(&rpc_client, fallback_height), - }; + let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height); let mut last_db_commit = Instant::now(); let mut last_print = Instant::now(); @@ -205,23 +202,22 @@ fn main() -> anyhow::Result<()> { // print synced-to height and current balance in intervals if last_print.elapsed() >= STDOUT_PRINT_DELAY { last_print = Instant::now(); - if let Some(synced_to) = chain.tip() { - let balance = { - graph.graph().balance( - &*chain, - synced_to.block_id(), - graph.index.outpoints().iter().cloned(), - |(k, _), _| k == &Keychain::Internal, - ) - }; - println!( - "[{:>10}s] synced to {} @ {} | total: {} sats", - start.elapsed().as_secs_f32(), - synced_to.hash(), - synced_to.height(), - balance.total() - ); - } + let synced_to = chain.tip(); + let balance = { + graph.graph().balance( + &*chain, + synced_to.block_id(), + graph.index.outpoints().iter().cloned(), + |(k, _), _| k == &Keychain::Internal, + ) + }; + println!( + "[{:>10}s] synced to {} @ {} | total: {} sats", + start.elapsed().as_secs_f32(), + synced_to.hash(), + synced_to.height(), + balance.total() + ); } } @@ -253,10 +249,7 @@ fn main() -> anyhow::Result<()> { let (tx, rx) = std::sync::mpsc::sync_channel::(CHANNEL_BOUND); let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> { let rpc_client = rpc_args.new_client()?; - let mut emitter = match last_cp { - Some(cp) => Emitter::from_checkpoint(&rpc_client, cp), - None => Emitter::from_height(&rpc_client, fallback_height), - }; + let mut emitter = Emitter::new(&rpc_client, last_cp, fallback_height); let mut block_count = rpc_client.get_block_count()? as u32; tx.send(Emission::Tip(block_count))?; @@ -335,24 +328,23 @@ fn main() -> anyhow::Result<()> { if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY { last_print = Some(Instant::now()); - if let Some(synced_to) = chain.tip() { - let balance = { - graph.graph().balance( - &*chain, - synced_to.block_id(), - graph.index.outpoints().iter().cloned(), - |(k, _), _| k == &Keychain::Internal, - ) - }; - println!( - "[{:>10}s] synced to {} @ {} / {} | total: {} sats", - start.elapsed().as_secs_f32(), - synced_to.hash(), - synced_to.height(), - tip_height, - balance.total() - ); - } + let synced_to = chain.tip(); + let balance = { + graph.graph().balance( + &*chain, + synced_to.block_id(), + graph.index.outpoints().iter().cloned(), + |(k, _), _| k == &Keychain::Internal, + ) + }; + println!( + "[{:>10}s] synced to {} @ {} / {} | total: {} sats", + start.elapsed().as_secs_f32(), + synced_to.hash(), + synced_to.height(), + tip_height, + balance.total() + ); } } diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 9e572a8929..3649980346 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -315,10 +315,8 @@ where version: 0x02, // because the temporary planning module does not support timelocks, we can use the chain // tip as the `lock_time` for anti-fee-sniping purposes - lock_time: chain - .get_chain_tip()? - .and_then(|block_id| absolute::LockTime::from_height(block_id.height).ok()) - .unwrap_or(absolute::LockTime::ZERO), + lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height) + .expect("invalid height"), input: selected_txos .iter() .map(|(_, utxo)| TxIn { @@ -404,7 +402,7 @@ pub fn planned_utxos, ) -> Result, FullTxOut)>, O::Error> { - let chain_tip = chain.get_chain_tip()?.unwrap_or_default(); + let chain_tip = chain.get_chain_tip()?; let outpoints = graph.index.outpoints().iter().cloned(); graph .graph() @@ -509,7 +507,7 @@ where let balance = graph.graph().try_balance( chain, - chain.get_chain_tip()?.unwrap_or_default(), + chain.get_chain_tip()?, graph.index.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, )?; @@ -539,7 +537,7 @@ where Commands::TxOut { txout_cmd } => { let graph = &*graph.lock().unwrap(); let chain = &*chain.lock().unwrap(); - let chain_tip = chain.get_chain_tip()?.unwrap_or_default(); + let chain_tip = chain.get_chain_tip()?; let outpoints = graph.index.outpoints().iter().cloned(); match txout_cmd { diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index be5ffc7bc8..a96378f647 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -112,7 +112,7 @@ fn main() -> anyhow::Result<()> { graph }); - let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)); + let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)?); let electrum_cmd = match &args.command { example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd, @@ -193,7 +193,7 @@ fn main() -> anyhow::Result<()> { // Get a short lock on the tracker to get the spks we're interested in let graph = graph.lock().unwrap(); let chain = chain.lock().unwrap(); - let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default(); + let chain_tip = chain.tip().block_id(); if !(all_spks || unused_spks || utxos || unconfirmed) { unused_spks = true; diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index d2ba62d0bb..5d4f612971 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -8,7 +8,7 @@ use bdk_chain::{ bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid}, indexed_tx_graph::{self, IndexedTxGraph}, keychain, - local_chain::{self, CheckPoint, LocalChain}, + local_chain::{self, LocalChain}, Append, ConfirmationTimeAnchor, }; @@ -102,6 +102,10 @@ fn main() -> anyhow::Result<()> { let (args, keymap, index, db, init_changeset) = example_cli::init::(DB_MAGIC, DB_PATH)?; + let genesis_hash = bdk_chain::genesis::from_network(args.network).ok_or(anyhow::anyhow!( + "cannot determine genesis hash from network" + ))?; + let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset; // Contruct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in @@ -113,8 +117,8 @@ fn main() -> anyhow::Result<()> { graph }); let chain = Mutex::new({ - let mut chain = LocalChain::default(); - chain.apply_changeset(&init_chain_changeset); + let mut chain = LocalChain::from_genesis_hash(genesis_hash); + chain.apply_changeset(&init_chain_changeset)?; chain }); @@ -234,7 +238,7 @@ fn main() -> anyhow::Result<()> { { let graph = graph.lock().unwrap(); let chain = chain.lock().unwrap(); - let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default(); + let chain_tip = chain.tip().block_id(); if *all_spks { let all_spks = graph @@ -332,7 +336,7 @@ fn main() -> anyhow::Result<()> { (missing_block_heights, tip) }; - println!("prev tip: {}", tip.as_ref().map_or(0, CheckPoint::height)); + println!("prev tip: {}", tip.height()); println!("missing block heights: {:?}", missing_block_heights); // Here, we actually fetch the missing blocks and create a `local_chain::Update`. diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index a6d7ca5206..8b26ddd0c3 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -27,6 +27,7 @@ fn main() -> Result<(), Box> { Some(internal_descriptor), db, Network::Testnet, + None, )?; let address = wallet.get_address(bdk::wallet::AddressIndex::New); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index ff1bbfb6d9..5b165c16b0 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -25,6 +25,7 @@ async fn main() -> Result<(), Box> { Some(internal_descriptor), db, Network::Testnet, + None, )?; let address = wallet.get_address(AddressIndex::New); diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 71554b0a81..a3e94bcb3e 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -24,6 +24,7 @@ fn main() -> Result<(), Box> { Some(internal_descriptor), db, Network::Testnet, + None, )?; let address = wallet.get_address(AddressIndex::New);