From 5ae5fe30ebd53d72fe567509506ae0cda7a3a244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 24 Mar 2023 09:23:36 +0800 Subject: [PATCH] [bdk_chain_redesign] Introduce `BlockAnchor` trait * Introduce `GraphedTx` struct to access transaction data of graphed transactions. * Ability to insert/access anchors and "seen at" values for graphed transactions. * `Additions` now records changes to anchors and last_seen_at. --- crates/bdk/src/wallet/mod.rs | 47 ++- crates/bdk/src/wallet/tx_builder.rs | 3 +- crates/chain/src/chain_data.rs | 10 +- crates/chain/src/chain_graph.rs | 107 +++--- crates/chain/src/keychain.rs | 40 +- crates/chain/src/keychain/persist.rs | 24 +- crates/chain/src/keychain/tracker.rs | 41 +- crates/chain/src/sparse_chain.rs | 4 +- crates/chain/src/tx_data_traits.rs | 20 +- crates/chain/src/tx_graph.rs | 360 +++++++++++++----- crates/chain/tests/test_chain_graph.rs | 63 ++- crates/chain/tests/test_keychain_tracker.rs | 14 +- crates/chain/tests/test_tx_graph.rs | 35 +- crates/electrum/src/lib.rs | 9 +- crates/esplora/src/async_ext.rs | 6 +- crates/esplora/src/blocking_ext.rs | 6 +- crates/file_store/src/file_store.rs | 32 +- crates/file_store/src/lib.rs | 10 +- .../keychain_tracker_electrum/src/main.rs | 2 +- .../keychain_tracker_esplora/src/main.rs | 2 +- .../keychain_tracker_example_cli/src/lib.rs | 47 ++- 21 files changed, 584 insertions(+), 298 deletions(-) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 67032cd3..65d3008b 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -23,7 +23,9 @@ pub use bdk_chain::keychain::Balance; use bdk_chain::{ chain_graph, keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker}, - sparse_chain, BlockId, ConfirmationTime, + sparse_chain, + tx_graph::GraphedTx, + BlockId, ConfirmationTime, }; use bitcoin::consensus::encode::serialize; use bitcoin::secp256k1::Secp256k1; @@ -83,19 +85,19 @@ const COINBASE_MATURITY: u32 = 100; pub struct Wallet { signers: Arc, change_signers: Arc, - keychain_tracker: KeychainTracker, - persist: persist::Persist, + keychain_tracker: KeychainTracker, + persist: persist::Persist, network: Network, secp: SecpCtx, } /// The update to a [`Wallet`] used in [`Wallet::apply_update`]. This is usually returned from blockchain data sources. /// The type parameter `T` indicates the kind of transaction contained in the update. It's usually a [`bitcoin::Transaction`]. -pub type Update = KeychainScan; +pub type Update = KeychainScan; /// Error indicating that something was wrong with an [`Update`]. pub type UpdateError = chain_graph::UpdateError; /// The changeset produced internally by applying an update -pub(crate) type ChangeSet = KeychainChangeSet; +pub(crate) type ChangeSet = KeychainChangeSet; /// The address index selection strategy to use to derived an address from the wallet's external /// descriptor. See [`Wallet::get_address`]. If you're unsure which one to use use `WalletIndex::New`. @@ -195,7 +197,7 @@ impl Wallet { network: Network, ) -> Result> where - D: persist::PersistBackend, + D: persist::PersistBackend, { let secp = Secp256k1::new(); @@ -257,7 +259,7 @@ impl Wallet { /// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo where - D: persist::PersistBackend, + D: persist::PersistBackend, { self._get_address(address_index, KeychainKind::External) } @@ -271,14 +273,14 @@ impl Wallet { /// be returned for any [`AddressIndex`]. pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo where - D: persist::PersistBackend, + D: persist::PersistBackend, { self._get_address(address_index, KeychainKind::Internal) } fn _get_address(&mut self, address_index: AddressIndex, keychain: KeychainKind) -> AddressInfo where - D: persist::PersistBackend, + D: persist::PersistBackend, { let keychain = self.map_keychain(keychain); let txout_index = &mut self.keychain_tracker.txout_index; @@ -453,7 +455,11 @@ impl Wallet { let fee = inputs.map(|inputs| inputs.saturating_sub(outputs)); Some(TransactionDetails { - transaction: if include_raw { Some(tx.clone()) } else { None }, + transaction: if include_raw { + Some(tx.tx.clone()) + } else { + None + }, txid, received, sent, @@ -518,7 +524,8 @@ impl Wallet { /// unconfirmed transactions last. pub fn transactions( &self, - ) -> impl DoubleEndedIterator + '_ { + ) -> impl DoubleEndedIterator)> + '_ + { self.keychain_tracker .chain_graph() .transactions_in_chain() @@ -613,7 +620,7 @@ impl Wallet { params: TxParams, ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> where - D: persist::PersistBackend, + D: persist::PersistBackend, { let external_descriptor = self .keychain_tracker @@ -1027,7 +1034,7 @@ impl Wallet { Some((ConfirmationTime::Confirmed { .. }, _tx)) => { return Err(Error::TransactionConfirmed) } - Some((_, tx)) => tx.clone(), + Some((_, tx)) => tx.tx.clone(), }; if !tx @@ -1085,7 +1092,7 @@ impl Wallet { outpoint: txin.previous_output, psbt_input: Box::new(psbt::Input { witness_utxo: Some(txout.clone()), - non_witness_utxo: Some(prev_tx.clone()), + non_witness_utxo: Some(prev_tx.tx.clone()), ..Default::default() }), }, @@ -1613,7 +1620,7 @@ impl Wallet { psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); } if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { - psbt_input.non_witness_utxo = Some(prev_tx.clone()); + psbt_input.non_witness_utxo = Some(prev_tx.tx.clone()); } } Ok(psbt_input) @@ -1687,7 +1694,7 @@ impl Wallet { /// [`commit`]: Self::commit pub fn apply_update(&mut self, update: Update) -> Result<(), UpdateError> where - D: persist::PersistBackend, + D: persist::PersistBackend, { let changeset = self.keychain_tracker.apply_update(update)?; self.persist.stage(changeset); @@ -1699,7 +1706,7 @@ impl Wallet { /// [`staged`]: Self::staged pub fn commit(&mut self) -> Result<(), D::WriteError> where - D: persist::PersistBackend, + D: persist::PersistBackend, { self.persist.commit() } @@ -1717,7 +1724,7 @@ impl Wallet { } /// Get a reference to the inner [`ChainGraph`](bdk_chain::chain_graph::ChainGraph). - pub fn as_chain_graph(&self) -> &bdk_chain::chain_graph::ChainGraph { + pub fn as_chain_graph(&self) -> &bdk_chain::chain_graph::ChainGraph { self.keychain_tracker.chain_graph() } } @@ -1728,8 +1735,8 @@ impl AsRef for Wallet { } } -impl AsRef> for Wallet { - fn as_ref(&self) -> &bdk_chain::chain_graph::ChainGraph { +impl AsRef> for Wallet { + fn as_ref(&self) -> &bdk_chain::chain_graph::ChainGraph { self.keychain_tracker.chain_graph() } } diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index dbd4811c..150d33aa 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -39,6 +39,7 @@ use crate::collections::BTreeMap; use crate::collections::HashSet; use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; +use bdk_chain::BlockId; use bdk_chain::ConfirmationTime; use core::cell::RefCell; use core::marker::PhantomData; @@ -526,7 +527,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> where - D: persist::PersistBackend, + D: persist::PersistBackend, { self.wallet .borrow_mut() diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 59444d7f..ec76dbb7 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -2,7 +2,7 @@ use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; use crate::{ sparse_chain::{self, ChainPosition}, - COINBASE_MATURITY, + BlockAnchor, COINBASE_MATURITY, }; /// Represents the height at which a transaction is confirmed. @@ -118,7 +118,7 @@ impl ConfirmationTime { } /// A reference to a block in the canonical chain. -#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] #[cfg_attr( feature = "serde", derive(serde::Deserialize, serde::Serialize), @@ -140,6 +140,12 @@ impl Default for BlockId { } } +impl BlockAnchor for BlockId { + fn anchor_block(&self) -> BlockId { + *self + } +} + impl From<(u32, BlockHash)> for BlockId { fn from((height, hash): (u32, BlockHash)) -> Self { Self { height, hash } diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index acf104e7..1a6ccb1e 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -2,8 +2,8 @@ use crate::{ collections::HashSet, sparse_chain::{self, ChainPosition, SparseChain}, - tx_graph::{self, TxGraph}, - BlockId, ForEachTxOut, FullTxOut, TxHeight, + tx_graph::{self, GraphedTx, TxGraph}, + BlockAnchor, BlockId, ForEachTxOut, FullTxOut, TxHeight, }; use alloc::{string::ToString, vec::Vec}; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; @@ -25,12 +25,12 @@ use core::fmt::Debug; /// `graph` but not the other way around. Transactions may fall out of the *chain* (via re-org or /// mempool eviction) but will remain in the *graph*. #[derive(Clone, Debug, PartialEq)] -pub struct ChainGraph

{ +pub struct ChainGraph { chain: SparseChain

, - graph: TxGraph, + graph: TxGraph, } -impl

Default for ChainGraph

{ +impl Default for ChainGraph { fn default() -> Self { Self { chain: Default::default(), @@ -39,38 +39,39 @@ impl

Default for ChainGraph

{ } } -impl

AsRef> for ChainGraph

{ +impl AsRef> for ChainGraph { fn as_ref(&self) -> &SparseChain

{ &self.chain } } -impl

AsRef for ChainGraph

{ - fn as_ref(&self) -> &TxGraph { +impl AsRef> for ChainGraph { + fn as_ref(&self) -> &TxGraph { &self.graph } } -impl

AsRef> for ChainGraph

{ - fn as_ref(&self) -> &ChainGraph

{ +impl AsRef> for ChainGraph { + fn as_ref(&self) -> &ChainGraph { self } } -impl

ChainGraph

{ +impl ChainGraph { /// Returns a reference to the internal [`SparseChain`]. pub fn chain(&self) -> &SparseChain

{ &self.chain } /// Returns a reference to the internal [`TxGraph`]. - pub fn graph(&self) -> &TxGraph { + pub fn graph(&self) -> &TxGraph { &self.graph } } -impl

ChainGraph

+impl ChainGraph where + A: BlockAnchor, P: ChainPosition, { /// Create a new chain graph from a `chain` and a `graph`. @@ -81,12 +82,14 @@ where /// transaction in `graph`. /// 2. The `chain` has two transactions that are allegedly in it, but they conflict in the `graph` /// (so could not possibly be in the same chain). - pub fn new(chain: SparseChain

, graph: TxGraph) -> Result> { + pub fn new(chain: SparseChain

, graph: TxGraph) -> Result> { let mut missing = HashSet::default(); for (pos, txid) in chain.txids() { - if let Some(tx) = graph.get_tx(*txid) { + if let Some(graphed_tx) = graph.get_tx(*txid) { let conflict = graph - .walk_conflicts(tx, |_, txid| Some((chain.tx_position(txid)?.clone(), txid))) + .walk_conflicts(graphed_tx.tx, |_, txid| { + Some((chain.tx_position(txid)?.clone(), txid)) + }) .next(); if let Some((conflict_pos, conflict)) = conflict { return Err(NewError::Conflict { @@ -126,7 +129,7 @@ where &self, update: SparseChain

, new_txs: impl IntoIterator, - ) -> Result, NewError

> { + ) -> Result, NewError

> { let mut inflated_chain = SparseChain::default(); let mut inflated_graph = TxGraph::default(); @@ -143,7 +146,7 @@ where match self.chain.tx_position(*txid) { Some(original_pos) => { if original_pos != pos { - let tx = self + let graphed_tx = self .graph .get_tx(*txid) .expect("tx must exist as it is referenced in sparsechain") @@ -151,7 +154,7 @@ where let _ = inflated_chain .insert_tx(*txid, pos.clone()) .expect("must insert since this was already in update"); - let _ = inflated_graph.insert_tx(tx); + let _ = inflated_graph.insert_tx(graphed_tx.tx.clone()); } } None => { @@ -185,7 +188,7 @@ where /// Determines the changes required to invalidate checkpoints `from_height` (inclusive) and /// above. Displaced transactions will have their positions moved to [`TxHeight::Unconfirmed`]. - pub fn invalidate_checkpoints_preview(&self, from_height: u32) -> ChangeSet

{ + pub fn invalidate_checkpoints_preview(&self, from_height: u32) -> ChangeSet { ChangeSet { chain: self.chain.invalidate_checkpoints_preview(from_height), ..Default::default() @@ -197,9 +200,9 @@ where /// /// This is equivalent to calling [`Self::invalidate_checkpoints_preview`] and /// [`Self::apply_changeset`] in sequence. - pub fn invalidate_checkpoints(&mut self, from_height: u32) -> ChangeSet

+ pub fn invalidate_checkpoints(&mut self, from_height: u32) -> ChangeSet where - ChangeSet

: Clone, + ChangeSet: Clone, { let changeset = self.invalidate_checkpoints_preview(from_height); self.apply_changeset(changeset.clone()); @@ -210,10 +213,10 @@ where /// /// This does not necessarily mean that it is *confirmed* in the blockchain; it might just be in /// the unconfirmed transaction list within the [`SparseChain`]. - pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, &Transaction)> { + pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, GraphedTx<'_, Transaction, A>)> { let position = self.chain.tx_position(txid)?; - let full_tx = self.graph.get_tx(txid).expect("must exist"); - Some((position, full_tx)) + let graphed_tx = self.graph.get_tx(txid).expect("must exist"); + Some((position, graphed_tx)) } /// Determines the changes required to insert a transaction into the inner [`ChainGraph`] and @@ -225,7 +228,7 @@ where &self, tx: Transaction, pos: P, - ) -> Result, InsertTxError

> { + ) -> Result, InsertTxError

> { let mut changeset = ChangeSet { chain: self.chain.insert_tx_preview(tx.txid(), pos)?, graph: self.graph.insert_tx_preview(tx), @@ -238,14 +241,18 @@ where /// /// This is equivalent to calling [`Self::insert_tx_preview`] and [`Self::apply_changeset`] in /// sequence. - pub fn insert_tx(&mut self, tx: Transaction, pos: P) -> Result, InsertTxError

> { + pub fn insert_tx( + &mut self, + tx: Transaction, + pos: P, + ) -> Result, InsertTxError

> { let changeset = self.insert_tx_preview(tx, pos)?; self.apply_changeset(changeset.clone()); Ok(changeset) } /// Determines the changes required to insert a [`TxOut`] into the internal [`TxGraph`]. - pub fn insert_txout_preview(&self, outpoint: OutPoint, txout: TxOut) -> ChangeSet

{ + pub fn insert_txout_preview(&self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { ChangeSet { chain: Default::default(), graph: self.graph.insert_txout_preview(outpoint, txout), @@ -256,7 +263,7 @@ where /// /// This is equivalent to calling [`Self::insert_txout_preview`] and [`Self::apply_changeset`] /// in sequence. - pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet

{ + pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { let changeset = self.insert_txout_preview(outpoint, txout); self.apply_changeset(changeset.clone()); changeset @@ -269,7 +276,7 @@ where pub fn insert_checkpoint_preview( &self, block_id: BlockId, - ) -> Result, InsertCheckpointError> { + ) -> Result, InsertCheckpointError> { self.chain .insert_checkpoint_preview(block_id) .map(|chain_changeset| ChangeSet { @@ -285,7 +292,7 @@ where pub fn insert_checkpoint( &mut self, block_id: BlockId, - ) -> Result, InsertCheckpointError> { + ) -> Result, InsertCheckpointError> { let changeset = self.insert_checkpoint_preview(block_id)?; self.apply_changeset(changeset.clone()); Ok(changeset) @@ -294,8 +301,8 @@ where /// Calculates the difference between self and `update` in the form of a [`ChangeSet`]. pub fn determine_changeset( &self, - update: &ChainGraph

, - ) -> Result, UpdateError

> { + update: &ChainGraph, + ) -> Result, UpdateError

> { let chain_changeset = self .chain .determine_changeset(&update.chain) @@ -330,7 +337,10 @@ where /// /// **WARNING:** If there are any missing full txs, conflict resolution will not be complete. In /// debug mode, this will result in panic. - fn fix_conflicts(&self, changeset: &mut ChangeSet

) -> Result<(), UnresolvableConflict

> { + fn fix_conflicts( + &self, + changeset: &mut ChangeSet, + ) -> Result<(), UnresolvableConflict

> { let mut chain_conflicts = vec![]; for (&txid, pos_change) in &changeset.chain.txids { @@ -346,7 +356,7 @@ where None => continue, }; - let mut full_tx = self.graph.get_tx(txid); + let mut full_tx = self.graph.get_tx(txid).map(|tx| tx.tx); if full_tx.is_none() { full_tx = changeset.graph.tx.iter().find(|tx| tx.txid() == txid) @@ -406,14 +416,17 @@ where /// /// **Warning** this method assumes that the changeset is correctly formed. If it is not, the /// chain graph may behave incorrectly in the future and panic unexpectedly. - pub fn apply_changeset(&mut self, changeset: ChangeSet

) { + pub fn apply_changeset(&mut self, changeset: ChangeSet) { self.chain.apply_changeset(changeset.chain); self.graph.apply_additions(changeset.graph); } /// Applies the `update` chain graph. Note this is shorthand for calling /// [`Self::determine_changeset()`] and [`Self::apply_changeset()`] in sequence. - pub fn apply_update(&mut self, update: ChainGraph

) -> Result, UpdateError

> { + pub fn apply_update( + &mut self, + update: ChainGraph, + ) -> Result, UpdateError

> { let changeset = self.determine_changeset(&update)?; self.apply_changeset(changeset.clone()); Ok(changeset) @@ -426,7 +439,9 @@ where /// Iterate over the full transactions and their position in the chain ordered by their position /// in ascending order. - pub fn transactions_in_chain(&self) -> impl DoubleEndedIterator { + pub fn transactions_in_chain( + &self, + ) -> impl DoubleEndedIterator)> { self.chain .txids() .map(move |(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist"))) @@ -457,18 +472,18 @@ where serde( crate = "serde_crate", bound( - deserialize = "P: serde::Deserialize<'de>", - serialize = "P: serde::Serialize" + deserialize = "A: Ord + serde::Deserialize<'de>, P: serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize, P: serde::Serialize" ) ) )] #[must_use] -pub struct ChangeSet

{ +pub struct ChangeSet { pub chain: sparse_chain::ChangeSet

, - pub graph: tx_graph::Additions, + pub graph: tx_graph::Additions, } -impl

ChangeSet

{ +impl ChangeSet { /// Returns `true` if this [`ChangeSet`] records no changes. pub fn is_empty(&self) -> bool { self.chain.is_empty() && self.graph.is_empty() @@ -484,7 +499,7 @@ impl

ChangeSet

{ /// Appends the changes in `other` into self such that applying `self` afterward has the same /// effect as sequentially applying the original `self` and `other`. - pub fn append(&mut self, other: ChangeSet

) + pub fn append(&mut self, other: ChangeSet) where P: ChainPosition, { @@ -493,7 +508,7 @@ impl

ChangeSet

{ } } -impl

Default for ChangeSet

{ +impl Default for ChangeSet { fn default() -> Self { Self { chain: Default::default(), @@ -508,7 +523,7 @@ impl

ForEachTxOut for ChainGraph

{ } } -impl

ForEachTxOut for ChangeSet

{ +impl ForEachTxOut for ChangeSet { fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { self.graph.for_each_txout(f) } diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 32176936..92d72841 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -99,14 +99,14 @@ impl AsRef> for DerivationAdditions { #[derive(Clone, Debug, PartialEq)] /// An update that includes the last active indexes of each keychain. -pub struct KeychainScan { +pub struct KeychainScan { /// The update data in the form of a chain that could be applied - pub update: ChainGraph

, + pub update: ChainGraph, /// The last active indexes of each keychain pub last_active_indices: BTreeMap, } -impl Default for KeychainScan { +impl Default for KeychainScan { fn default() -> Self { Self { update: Default::default(), @@ -115,8 +115,8 @@ impl Default for KeychainScan { } } -impl From> for KeychainScan { - fn from(update: ChainGraph

) -> Self { +impl From> for KeychainScan { + fn from(update: ChainGraph) -> Self { KeychainScan { update, last_active_indices: Default::default(), @@ -134,20 +134,20 @@ impl From> for KeychainScan { serde( crate = "serde_crate", bound( - deserialize = "K: Ord + serde::Deserialize<'de>, P: serde::Deserialize<'de>", - serialize = "K: Ord + serde::Serialize, P: serde::Serialize" + deserialize = "K: Ord + serde::Deserialize<'de>, A: Ord + serde::Deserialize<'de>, P: serde::Deserialize<'de>", + serialize = "K: Ord + serde::Serialize, A: Ord + serde::Serialize, P: serde::Serialize" ) ) )] #[must_use] -pub struct KeychainChangeSet { +pub struct KeychainChangeSet { /// The changes in local keychain derivation indices pub derivation_indices: DerivationAdditions, /// The changes that have occurred in the blockchain - pub chain_graph: chain_graph::ChangeSet

, + pub chain_graph: chain_graph::ChangeSet, } -impl Default for KeychainChangeSet { +impl Default for KeychainChangeSet { fn default() -> Self { Self { chain_graph: Default::default(), @@ -156,7 +156,7 @@ impl Default for KeychainChangeSet { } } -impl KeychainChangeSet { +impl KeychainChangeSet { /// Returns whether the [`KeychainChangeSet`] is empty (no changes recorded). pub fn is_empty(&self) -> bool { self.chain_graph.is_empty() && self.derivation_indices.is_empty() @@ -167,7 +167,7 @@ impl KeychainChangeSet { /// /// Note the derivation indices cannot be decreased, so `other` will only change the derivation /// index for a keychain, if it's value is higher than the one in `self`. - pub fn append(&mut self, other: KeychainChangeSet) + pub fn append(&mut self, other: KeychainChangeSet) where K: Ord, P: ChainPosition, @@ -177,8 +177,8 @@ impl KeychainChangeSet { } } -impl From> for KeychainChangeSet { - fn from(changeset: chain_graph::ChangeSet

) -> Self { +impl From> for KeychainChangeSet { + fn from(changeset: chain_graph::ChangeSet) -> Self { Self { chain_graph: changeset, ..Default::default() @@ -186,7 +186,7 @@ impl From> for KeychainChangeSet { } } -impl From> for KeychainChangeSet { +impl From> for KeychainChangeSet { fn from(additions: DerivationAdditions) -> Self { Self { derivation_indices: additions, @@ -195,13 +195,13 @@ impl From> for KeychainChangeSet { } } -impl AsRef for KeychainScan { - fn as_ref(&self) -> &TxGraph { +impl AsRef> for KeychainScan { + fn as_ref(&self) -> &TxGraph { self.update.graph() } } -impl ForEachTxOut for KeychainChangeSet { +impl ForEachTxOut for KeychainChangeSet { fn for_each_txout(&self, f: impl FnMut((bitcoin::OutPoint, &bitcoin::TxOut))) { self.chain_graph.for_each_txout(f) } @@ -287,12 +287,12 @@ mod test { rhs_di.insert(Keychain::Four, 4); let mut lhs = KeychainChangeSet { derivation_indices: DerivationAdditions(lhs_di), - chain_graph: chain_graph::ChangeSet::::default(), + chain_graph: chain_graph::ChangeSet::<(), TxHeight>::default(), }; let rhs = KeychainChangeSet { derivation_indices: DerivationAdditions(rhs_di), - chain_graph: chain_graph::ChangeSet::::default(), + chain_graph: chain_graph::ChangeSet::<(), TxHeight>::default(), }; lhs.append(rhs); diff --git a/crates/chain/src/keychain/persist.rs b/crates/chain/src/keychain/persist.rs index 1a3ffab0..f0bc8d11 100644 --- a/crates/chain/src/keychain/persist.rs +++ b/crates/chain/src/keychain/persist.rs @@ -18,12 +18,12 @@ use crate::{keychain, sparse_chain::ChainPosition}; /// /// [`KeychainTracker`]: keychain::KeychainTracker #[derive(Debug)] -pub struct Persist { +pub struct Persist { backend: B, - stage: keychain::KeychainChangeSet, + stage: keychain::KeychainChangeSet, } -impl Persist { +impl Persist { /// Create a new `Persist` from a [`PersistBackend`]. pub fn new(backend: B) -> Self { Self { @@ -35,7 +35,7 @@ impl Persist { /// Stage a `changeset` to later persistence with [`commit`]. /// /// [`commit`]: Self::commit - pub fn stage(&mut self, changeset: keychain::KeychainChangeSet) + pub fn stage(&mut self, changeset: keychain::KeychainChangeSet) where K: Ord, P: ChainPosition, @@ -44,7 +44,7 @@ impl Persist { } /// Get the changes that haven't been committed yet - pub fn staged(&self) -> &keychain::KeychainChangeSet { + pub fn staged(&self) -> &keychain::KeychainChangeSet { &self.stage } @@ -53,7 +53,7 @@ impl Persist { /// Returns a backend-defined error if this fails. pub fn commit(&mut self) -> Result<(), B::WriteError> where - B: PersistBackend, + B: PersistBackend, { self.backend.append_changeset(&self.stage)?; self.stage = Default::default(); @@ -62,7 +62,7 @@ impl Persist { } /// A persistence backend for [`Persist`]. -pub trait PersistBackend { +pub trait PersistBackend { /// The error the backend returns when it fails to write. type WriteError: core::fmt::Debug; @@ -79,29 +79,29 @@ pub trait PersistBackend { /// [`load_into_keychain_tracker`]: Self::load_into_keychain_tracker fn append_changeset( &mut self, - changeset: &keychain::KeychainChangeSet, + changeset: &keychain::KeychainChangeSet, ) -> Result<(), Self::WriteError>; /// Applies all the changesets the backend has received to `tracker`. fn load_into_keychain_tracker( &mut self, - tracker: &mut keychain::KeychainTracker, + tracker: &mut keychain::KeychainTracker, ) -> Result<(), Self::LoadError>; } -impl PersistBackend for () { +impl PersistBackend for () { type WriteError = (); type LoadError = (); fn append_changeset( &mut self, - _changeset: &keychain::KeychainChangeSet, + _changeset: &keychain::KeychainChangeSet, ) -> Result<(), Self::WriteError> { Ok(()) } fn load_into_keychain_tracker( &mut self, - _tracker: &mut keychain::KeychainTracker, + _tracker: &mut keychain::KeychainTracker, ) -> Result<(), Self::LoadError> { Ok(()) } diff --git a/crates/chain/src/keychain/tracker.rs b/crates/chain/src/keychain/tracker.rs index fff5ee2b..db4e8d89 100644 --- a/crates/chain/src/keychain/tracker.rs +++ b/crates/chain/src/keychain/tracker.rs @@ -17,15 +17,16 @@ use super::{Balance, DerivationAdditions}; /// The [`KeychainTracker`] atomically updates its [`KeychainTxOutIndex`] whenever new chain data is /// incorporated into its internal [`ChainGraph`]. #[derive(Clone, Debug)] -pub struct KeychainTracker { +pub struct KeychainTracker { /// Index between script pubkeys to transaction outputs pub txout_index: KeychainTxOutIndex, - chain_graph: ChainGraph

, + chain_graph: ChainGraph, } -impl KeychainTracker +impl KeychainTracker where P: sparse_chain::ChainPosition, + A: crate::BlockAnchor, K: Ord + Clone + core::fmt::Debug, { /// Add a keychain to the tracker's `txout_index` with a descriptor to derive addresses. @@ -64,8 +65,8 @@ where /// [`KeychainTxOutIndex`]. pub fn determine_changeset( &self, - scan: &KeychainScan, - ) -> Result, chain_graph::UpdateError

> { + scan: &KeychainScan, + ) -> Result, chain_graph::UpdateError

> { // TODO: `KeychainTxOutIndex::determine_additions` let mut derivation_indices = scan.last_active_indices.clone(); derivation_indices.retain(|keychain, index| { @@ -89,8 +90,8 @@ where /// [`apply_changeset`]: Self::apply_changeset pub fn apply_update( &mut self, - scan: KeychainScan, - ) -> Result, chain_graph::UpdateError

> { + scan: KeychainScan, + ) -> Result, chain_graph::UpdateError

> { let changeset = self.determine_changeset(&scan)?; self.apply_changeset(changeset.clone()); Ok(changeset) @@ -100,7 +101,7 @@ where /// /// Internally, this calls [`KeychainTxOutIndex::apply_additions`] and /// [`ChainGraph::apply_changeset`] in sequence. - pub fn apply_changeset(&mut self, changeset: KeychainChangeSet) { + pub fn apply_changeset(&mut self, changeset: KeychainChangeSet) { let KeychainChangeSet { derivation_indices, chain_graph, @@ -132,12 +133,12 @@ where } /// Returns a reference to the internal [`ChainGraph`]. - pub fn chain_graph(&self) -> &ChainGraph

{ + pub fn chain_graph(&self) -> &ChainGraph { &self.chain_graph } /// Returns a reference to the internal [`TxGraph`] (which is part of the [`ChainGraph`]). - pub fn graph(&self) -> &TxGraph { + pub fn graph(&self) -> &TxGraph { self.chain_graph().graph() } @@ -159,7 +160,7 @@ where pub fn insert_checkpoint_preview( &self, block_id: BlockId, - ) -> Result, chain_graph::InsertCheckpointError> { + ) -> Result, chain_graph::InsertCheckpointError> { Ok(KeychainChangeSet { chain_graph: self.chain_graph.insert_checkpoint_preview(block_id)?, ..Default::default() @@ -176,7 +177,7 @@ where pub fn insert_checkpoint( &mut self, block_id: BlockId, - ) -> Result, chain_graph::InsertCheckpointError> { + ) -> Result, chain_graph::InsertCheckpointError> { let changeset = self.insert_checkpoint_preview(block_id)?; self.apply_changeset(changeset.clone()); Ok(changeset) @@ -191,7 +192,7 @@ where &self, tx: Transaction, pos: P, - ) -> Result, chain_graph::InsertTxError

> { + ) -> Result, chain_graph::InsertTxError

> { Ok(KeychainChangeSet { chain_graph: self.chain_graph.insert_tx_preview(tx, pos)?, ..Default::default() @@ -209,7 +210,7 @@ where &mut self, tx: Transaction, pos: P, - ) -> Result, chain_graph::InsertTxError

> { + ) -> Result, chain_graph::InsertTxError

> { let changeset = self.insert_tx_preview(tx, pos)?; self.apply_changeset(changeset.clone()); Ok(changeset) @@ -280,7 +281,7 @@ where } } -impl Default for KeychainTracker { +impl Default for KeychainTracker { fn default() -> Self { Self { txout_index: Default::default(), @@ -289,20 +290,20 @@ impl Default for KeychainTracker { } } -impl AsRef> for KeychainTracker { +impl AsRef> for KeychainTracker { fn as_ref(&self) -> &SparseChain

{ self.chain_graph.chain() } } -impl AsRef for KeychainTracker { - fn as_ref(&self) -> &TxGraph { +impl AsRef> for KeychainTracker { + fn as_ref(&self) -> &TxGraph { self.chain_graph.graph() } } -impl AsRef> for KeychainTracker { - fn as_ref(&self) -> &ChainGraph

{ +impl AsRef> for KeychainTracker { + fn as_ref(&self) -> &ChainGraph { &self.chain_graph } } diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index b9c1e24b..a449638d 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -899,7 +899,7 @@ impl SparseChain

{ /// Attempt to retrieve a [`FullTxOut`] of the given `outpoint`. /// /// This will return `Some` only if the output's transaction is in both `self` and `graph`. - pub fn full_txout(&self, graph: &TxGraph, outpoint: OutPoint) -> Option> { + pub fn full_txout(&self, graph: &TxGraph, outpoint: OutPoint) -> Option> { let chain_pos = self.tx_position(outpoint.txid)?; let tx = graph.get_tx(outpoint.txid)?; @@ -972,7 +972,7 @@ impl SparseChain

{ /// /// Note that the transaction including `outpoint` does not need to be in the `graph` or the /// `chain` for this to return `Some`. - pub fn spent_by(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<(&P, Txid)> { + pub fn spent_by(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<(&P, Txid)> { graph .outspends(outpoint) .iter() diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 432592b8..9b9facab 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,4 +1,6 @@ -use bitcoin::{Block, OutPoint, Transaction, TxOut}; +use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; + +use crate::BlockId; /// Trait to do something with every txout contained in a structure. /// @@ -31,3 +33,19 @@ impl ForEachTxOut for Transaction { } } } + +/// Trait that "anchors" blockchain data in a specific block of height and hash. +/// +/// This trait is typically associated with blockchain data such as transactions. +pub trait BlockAnchor: + core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash + Send + Sync + 'static +{ + /// Returns the [`BlockId`] that the associated blockchain data is "anchored" in. + fn anchor_block(&self) -> BlockId; +} + +impl BlockAnchor for (u32, BlockHash) { + fn anchor_block(&self) -> BlockId { + (*self).into() + } +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 3326ac4a..824b68e2 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -15,12 +15,13 @@ //! of the changes to [`TxGraph`]. //! //! ``` +//! # use bdk_chain::BlockId; //! # use bdk_chain::tx_graph::TxGraph; //! # use bdk_chain::example_utils::*; //! # use bitcoin::Transaction; //! # let tx_a = tx_from_hex(RAW_TX_1); //! # let tx_b = tx_from_hex(RAW_TX_2); -//! let mut graph = TxGraph::default(); +//! let mut graph = TxGraph::::default(); //! //! // preview a transaction insertion (not actually inserted) //! let additions = graph.insert_tx_preview(tx_a); @@ -34,12 +35,13 @@ //! A [`TxGraph`] can also be updated with another [`TxGraph`]. //! //! ``` +//! # use bdk_chain::BlockId; //! # use bdk_chain::tx_graph::TxGraph; //! # use bdk_chain::example_utils::*; //! # use bitcoin::Transaction; //! # let tx_a = tx_from_hex(RAW_TX_1); //! # let tx_b = tx_from_hex(RAW_TX_2); -//! let mut graph = TxGraph::default(); +//! let mut graph = TxGraph::::default(); //! let update = TxGraph::new(vec![tx_a, tx_b]); //! //! // preview additions as the result of the update @@ -52,28 +54,76 @@ //! let additions = graph.apply_update(update); //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, ForEachTxOut}; + +use crate::{collections::*, BlockAnchor, BlockId, ForEachTxOut}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; -use core::ops::RangeInclusive; +use core::ops::{Deref, RangeInclusive}; /// A graph of transactions and spends. /// /// See the [module-level documentation] for more. /// /// [module-level documentation]: crate::tx_graph -#[derive(Clone, Debug, PartialEq, Default)] -pub struct TxGraph { - txs: HashMap, +#[derive(Clone, Debug, PartialEq)] +pub struct TxGraph { + // all transactions that the graph is aware of in format: `(tx_node, tx_anchors, tx_last_seen)` + txs: HashMap, u64)>, spends: BTreeMap>, + anchors: BTreeSet<(A, Txid)>, // This atrocity exists so that `TxGraph::outspends()` can return a reference. // FIXME: This can be removed once `HashSet::new` is a const fn. empty_outspends: HashSet, } -/// Node of a [`TxGraph`]. This can either be a whole transaction, or a partial transaction (where -/// we only have select outputs). +impl Default for TxGraph { + fn default() -> Self { + Self { + txs: Default::default(), + spends: Default::default(), + anchors: Default::default(), + empty_outspends: Default::default(), + } + } +} + +/// An outward-facing view of a transaction that resides in a [`TxGraph`]. +#[derive(Clone, Debug, PartialEq)] +pub struct GraphedTx<'a, T, A> { + /// Txid of the transaction. + pub txid: Txid, + /// A partial or full representation of the transaction. + pub tx: &'a T, + /// The blocks that the transaction is "anchored" in. + pub anchors: &'a BTreeSet, + /// The last-seen unix timestamp of the transaction. + pub last_seen: u64, +} + +impl<'a, T, A> Deref for GraphedTx<'a, T, A> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.tx + } +} + +impl<'a, A> GraphedTx<'a, Transaction, A> { + pub fn from_tx(tx: &'a Transaction, anchors: &'a BTreeSet) -> Self { + Self { + txid: tx.txid(), + tx, + anchors, + last_seen: 0, + } + } +} + +/// Internal representation of a transaction node of a [`TxGraph`]. +/// +/// This can either be a whole transaction, or a partial transaction (where we only have select +/// outputs). #[derive(Clone, Debug, PartialEq)] enum TxNode { Whole(Transaction), @@ -86,10 +136,10 @@ impl Default for TxNode { } } -impl TxGraph { +impl TxGraph { /// Iterate over all tx outputs known by [`TxGraph`]. pub fn all_txouts(&self) -> impl Iterator { - self.txs.iter().flat_map(|(txid, tx)| match tx { + self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx { TxNode::Whole(tx) => tx .output .iter() @@ -104,11 +154,18 @@ impl TxGraph { } /// Iterate over all full transactions in the graph. - pub fn full_transactions(&self) -> impl Iterator { - self.txs.iter().filter_map(|(_, tx)| match tx { - TxNode::Whole(tx) => Some(tx), - TxNode::Partial(_) => None, - }) + pub fn full_transactions(&self) -> impl Iterator> { + self.txs + .iter() + .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { + TxNode::Whole(tx) => Some(GraphedTx { + txid, + tx, + anchors, + last_seen: *last_seen, + }), + TxNode::Partial(_) => None, + }) } /// Get a transaction by txid. This only returns `Some` for full transactions. @@ -116,16 +173,21 @@ impl TxGraph { /// Refer to [`get_txout`] for getting a specific [`TxOut`]. /// /// [`get_txout`]: Self::get_txout - pub fn get_tx(&self, txid: Txid) -> Option<&Transaction> { - match self.txs.get(&txid)? { - TxNode::Whole(tx) => Some(tx), - TxNode::Partial(_) => None, + pub fn get_tx(&self, txid: Txid) -> Option> { + match &self.txs.get(&txid)? { + (TxNode::Whole(tx), anchors, last_seen) => Some(GraphedTx { + txid, + tx, + anchors, + last_seen: *last_seen, + }), + _ => None, } } /// Obtains a single tx output (if any) at the specified outpoint. pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> { - match self.txs.get(&outpoint.txid)? { + match &self.txs.get(&outpoint.txid)?.0 { TxNode::Whole(tx) => tx.output.get(outpoint.vout as usize), TxNode::Partial(txouts) => txouts.get(&outpoint.vout), } @@ -133,7 +195,7 @@ impl TxGraph { /// Returns a [`BTreeMap`] of vout to output of the provided `txid`. pub fn txouts(&self, txid: Txid) -> Option> { - Some(match self.txs.get(&txid)? { + Some(match &self.txs.get(&txid)?.0 { TxNode::Whole(tx) => tx .output .iter() @@ -178,7 +240,7 @@ impl TxGraph { } } -impl TxGraph { +impl TxGraph { /// Construct a new [`TxGraph`] from a list of transactions. pub fn new(txs: impl IntoIterator) -> Self { let mut new = Self::default(); @@ -187,11 +249,12 @@ impl TxGraph { } new } + /// Inserts the given [`TxOut`] at [`OutPoint`]. /// /// Note this will ignore the action if we already have the full transaction that the txout is /// alleged to be on (even if it doesn't match it!). - pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> Additions { + pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> Additions { let additions = self.insert_txout_preview(outpoint, txout); self.apply_additions(additions.clone()); additions @@ -200,25 +263,52 @@ impl TxGraph { /// Inserts the given transaction into [`TxGraph`]. /// /// The [`Additions`] returned will be empty if `tx` already exists. - pub fn insert_tx(&mut self, tx: Transaction) -> Additions { + pub fn insert_tx(&mut self, tx: Transaction) -> Additions { let additions = self.insert_tx_preview(tx); self.apply_additions(additions.clone()); additions } + /// Inserts the given `anchor` into [`TxGraph`]. + /// + /// This is equivalent to calling [`insert_anchor_preview`] and [`apply_additions`] in sequence. + /// The [`Additions`] returned will be empty if graph already knows that `txid` exists in + /// `anchor`. + /// + /// [`insert_anchor_preview`]: Self::insert_anchor_preview + /// [`apply_additions`]: Self::apply_additions + pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> Additions { + let additions = self.insert_anchor_preview(txid, anchor); + self.apply_additions(additions.clone()); + additions + } + + /// Inserts the given `seen_at` into [`TxGraph`]. + /// + /// This is equivalent to calling [`insert_seen_at_preview`] and [`apply_additions`] in + /// sequence. + /// + /// [`insert_seen_at_preview`]: Self::insert_seen_at_preview + /// [`apply_additions`]: Self::apply_additions + pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> Additions { + let additions = self.insert_seen_at_preview(txid, seen_at); + self.apply_additions(additions.clone()); + additions + } + /// Extends this graph with another so that `self` becomes the union of the two sets of /// transactions. /// /// The returned [`Additions`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). - pub fn apply_update(&mut self, update: TxGraph) -> Additions { + pub fn apply_update(&mut self, update: TxGraph) -> Additions { let additions = self.determine_additions(&update); self.apply_additions(additions.clone()); additions } /// Applies [`Additions`] to [`TxGraph`]. - pub fn apply_additions(&mut self, additions: Additions) { + pub fn apply_additions(&mut self, additions: Additions) { for tx in additions.tx { let txid = tx.txid(); @@ -232,12 +322,21 @@ impl TxGraph { self.spends.entry(outpoint).or_default().insert(txid); }); - if let Some(TxNode::Whole(old_tx)) = self.txs.insert(txid, TxNode::Whole(tx)) { - debug_assert_eq!( - old_tx.txid(), - txid, - "old tx of the same txid should not be different." - ); + match self.txs.get_mut(&txid) { + Some((tx_node @ TxNode::Partial(_), _, _)) => { + *tx_node = TxNode::Whole(tx); + } + Some((TxNode::Whole(tx), _, _)) => { + debug_assert_eq!( + tx.txid(), + txid, + "tx should produce txid that is same as key" + ); + } + None => { + self.txs + .insert(txid, (TxNode::Whole(tx), BTreeSet::new(), 0)); + } } } @@ -245,47 +344,75 @@ impl TxGraph { let tx_entry = self .txs .entry(outpoint.txid) - .or_insert_with(TxNode::default); + .or_insert_with(Default::default); match tx_entry { - TxNode::Whole(_) => { /* do nothing since we already have full tx */ } - TxNode::Partial(txouts) => { + (TxNode::Whole(_), _, _) => { /* do nothing since we already have full tx */ } + (TxNode::Partial(txouts), _, _) => { txouts.insert(outpoint.vout, txout); } } } + + for (anchor, txid) in additions.anchors { + if self.anchors.insert((anchor.clone(), txid)) { + let (_, anchors, _) = self.txs.entry(txid).or_insert_with(Default::default); + anchors.insert(anchor); + } + } + + for (txid, new_last_seen) in additions.last_seen { + let (_, _, last_seen) = self.txs.entry(txid).or_insert_with(Default::default); + if new_last_seen > *last_seen { + *last_seen = new_last_seen; + } + } } /// Previews the resultant [`Additions`] when [`Self`] is updated against the `update` graph. /// /// The [`Additions`] would be the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). - pub fn determine_additions(&self, update: &TxGraph) -> Additions { + pub fn determine_additions(&self, update: &TxGraph) -> Additions { let mut additions = Additions::default(); - for (&txid, update_tx) in &update.txs { - if self.get_tx(txid).is_some() { - continue; - } - - match update_tx { - TxNode::Whole(tx) => { - if matches!(self.txs.get(&txid), None | Some(TxNode::Partial(_))) { - additions.tx.insert(tx.clone()); - } + for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs { + let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) { + (None, TxNode::Whole(update_tx)) => { + additions.tx.insert(update_tx.clone()); + 0 } - TxNode::Partial(partial) => { - for (&vout, update_txout) in partial { - let outpoint = OutPoint::new(txid, vout); - - if self.get_txout(outpoint) != Some(update_txout) { - additions.txout.insert(outpoint, update_txout.clone()); - } - } + (None, TxNode::Partial(update_txos)) => { + additions.txout.extend( + update_txos + .iter() + .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), + ); + 0 } + (Some((TxNode::Whole(_), _, last_seen)), _) => *last_seen, + (Some((TxNode::Partial(_), _, last_seen)), TxNode::Whole(update_tx)) => { + additions.tx.insert(update_tx.clone()); + *last_seen + } + (Some((TxNode::Partial(txos), _, last_seen)), TxNode::Partial(update_txos)) => { + additions.txout.extend( + update_txos + .iter() + .filter(|(vout, _)| !txos.contains_key(*vout)) + .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), + ); + *last_seen + } + }; + + if *update_last_seen > prev_last_seen { + additions.last_seen.insert(txid, *update_last_seen); } } + additions.anchors = update.anchors.difference(&self.anchors).cloned().collect(); + additions } @@ -293,9 +420,11 @@ impl TxGraph { /// mutate [`Self`]. /// /// The [`Additions`] result will be empty if `tx` already exists in `self`. - pub fn insert_tx_preview(&self, tx: Transaction) -> Additions { + pub fn insert_tx_preview(&self, tx: Transaction) -> Additions { let mut update = Self::default(); - update.txs.insert(tx.txid(), TxNode::Whole(tx)); + update + .txs + .insert(tx.txid(), (TxNode::Whole(tx), BTreeSet::new(), 0)); self.determine_additions(&update) } @@ -304,17 +433,38 @@ impl TxGraph { /// /// The [`Additions`] result will be empty if the `outpoint` (or a full transaction containing /// the `outpoint`) already existed in `self`. - pub fn insert_txout_preview(&self, outpoint: OutPoint, txout: TxOut) -> Additions { + pub fn insert_txout_preview(&self, outpoint: OutPoint, txout: TxOut) -> Additions { let mut update = Self::default(); update.txs.insert( outpoint.txid, - TxNode::Partial([(outpoint.vout, txout)].into()), + ( + TxNode::Partial([(outpoint.vout, txout)].into()), + BTreeSet::new(), + 0, + ), ); self.determine_additions(&update) } + + /// Returns the resultant [`Additions`] if the `txid` is set in `anchor`. + pub fn insert_anchor_preview(&self, txid: Txid, anchor: A) -> Additions { + let mut update = Self::default(); + update.anchors.insert((anchor, txid)); + self.determine_additions(&update) + } + + /// Returns the resultant [`Additions`] if the `txid` is set to `seen_at`. + /// + /// Note that [`TxGraph`] only keeps track of the lastest `seen_at`. + pub fn insert_seen_at_preview(&self, txid: Txid, seen_at: u64) -> Additions { + let mut update = Self::default(); + let (_, _, update_last_seen) = update.txs.entry(txid).or_default(); + *update_last_seen = seen_at; + self.determine_additions(&update) + } } -impl TxGraph { +impl TxGraph { /// The transactions spending from this output. /// /// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in @@ -344,11 +494,20 @@ impl TxGraph { } /// Iterate over all partial transactions (outputs only) in the graph. - pub fn partial_transactions(&self) -> impl Iterator)> { - self.txs.iter().filter_map(|(txid, tx)| match tx { - TxNode::Whole(_) => None, - TxNode::Partial(partial) => Some((*txid, partial)), - }) + pub fn partial_transactions( + &self, + ) -> impl Iterator, A>> { + self.txs + .iter() + .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { + TxNode::Whole(_) => None, + TxNode::Partial(partial) => Some(GraphedTx { + txid, + tx: partial, + anchors, + last_seen: *last_seen, + }), + }) } /// Creates an iterator that filters and maps descendants from the starting `txid`. @@ -361,7 +520,7 @@ impl TxGraph { /// /// The supplied closure returns an `Option`, allowing the caller to map each node it vists /// and decide whether to visit descendants. - pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants + pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants where F: FnMut(usize, Txid) -> Option + 'g, { @@ -372,7 +531,11 @@ impl TxGraph { /// descendants of directly-conflicting transactions, which are also considered conflicts). /// /// Refer to [`Self::walk_descendants`] for `walk_map` usage. - pub fn walk_conflicts<'g, F, O>(&'g self, tx: &'g Transaction, walk_map: F) -> TxDescendants + pub fn walk_conflicts<'g, F, O>( + &'g self, + tx: &'g Transaction, + walk_map: F, + ) -> TxDescendants where F: FnMut(usize, Txid) -> Option + 'g, { @@ -413,19 +576,38 @@ impl TxGraph { /// Refer to [module-level documentation] for more. /// /// [module-level documentation]: crate::tx_graph -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq)] #[cfg_attr( feature = "serde", derive(serde::Deserialize, serde::Serialize), - serde(crate = "serde_crate") + serde( + crate = "serde_crate", + bound( + deserialize = "A: Ord + serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize", + ) + ) )] #[must_use] -pub struct Additions { +pub struct Additions { pub tx: BTreeSet, pub txout: BTreeMap, + pub anchors: BTreeSet<(A, Txid)>, + pub last_seen: BTreeMap, } -impl Additions { +impl Default for Additions { + fn default() -> Self { + Self { + tx: Default::default(), + txout: Default::default(), + anchors: Default::default(), + last_seen: Default::default(), + } + } +} + +impl Additions { /// Returns true if the [`Additions`] is empty (no transactions or txouts). pub fn is_empty(&self) -> bool { self.tx.is_empty() && self.txout.is_empty() @@ -446,25 +628,25 @@ impl Additions { /// Appends the changes in `other` into self such that applying `self` afterward has the same /// effect as sequentially applying the original `self` and `other`. - pub fn append(&mut self, mut other: Additions) { + pub fn append(&mut self, mut other: Additions) { self.tx.append(&mut other.tx); self.txout.append(&mut other.txout); } } -impl AsRef for TxGraph { - fn as_ref(&self) -> &TxGraph { +impl AsRef> for TxGraph { + fn as_ref(&self) -> &TxGraph { self } } -impl ForEachTxOut for Additions { +impl ForEachTxOut for Additions { fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { self.txouts().for_each(f) } } -impl ForEachTxOut for TxGraph { +impl ForEachTxOut for TxGraph { fn for_each_txout(&self, f: impl FnMut((OutPoint, &TxOut))) { self.all_txouts().for_each(f) } @@ -475,17 +657,17 @@ impl ForEachTxOut for TxGraph { /// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`]. /// /// [`walk_descendants`]: TxGraph::walk_descendants -pub struct TxDescendants<'g, F> { - graph: &'g TxGraph, +pub struct TxDescendants<'g, A, F> { + graph: &'g TxGraph, visited: HashSet, stack: Vec<(usize, Txid)>, filter_map: F, } -impl<'g, F> TxDescendants<'g, F> { +impl<'g, A, F> TxDescendants<'g, A, F> { /// Creates a `TxDescendants` that includes the starting `txid` when iterating. #[allow(unused)] - pub(crate) fn new_include_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { + pub(crate) fn new_include_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { Self { graph, visited: Default::default(), @@ -495,7 +677,7 @@ impl<'g, F> TxDescendants<'g, F> { } /// Creates a `TxDescendants` that excludes the starting `txid` when iterating. - pub(crate) fn new_exclude_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { + pub(crate) fn new_exclude_root(graph: &'g TxGraph, txid: Txid, filter_map: F) -> Self { let mut descendants = Self { graph, visited: Default::default(), @@ -508,7 +690,11 @@ impl<'g, F> TxDescendants<'g, F> { /// Creates a `TxDescendants` from multiple starting transactions that include the starting /// `txid`s when iterating. - pub(crate) fn from_multiple_include_root(graph: &'g TxGraph, txids: I, filter_map: F) -> Self + pub(crate) fn from_multiple_include_root( + graph: &'g TxGraph, + txids: I, + filter_map: F, + ) -> Self where I: IntoIterator, { @@ -523,7 +709,11 @@ impl<'g, F> TxDescendants<'g, F> { /// Creates a `TxDescendants` from multiple starting transactions that excludes the starting /// `txid`s when iterating. #[allow(unused)] - pub(crate) fn from_multiple_exclude_root(graph: &'g TxGraph, txids: I, filter_map: F) -> Self + pub(crate) fn from_multiple_exclude_root( + graph: &'g TxGraph, + txids: I, + filter_map: F, + ) -> Self where I: IntoIterator, { @@ -540,7 +730,7 @@ impl<'g, F> TxDescendants<'g, F> { } } -impl<'g, F> TxDescendants<'g, F> { +impl<'g, A, F> TxDescendants<'g, A, F> { fn populate_stack(&mut self, depth: usize, txid: Txid) { let spend_paths = self .graph @@ -552,7 +742,7 @@ impl<'g, F> TxDescendants<'g, F> { } } -impl<'g, F, O> Iterator for TxDescendants<'g, F> +impl<'g, A, F, O> Iterator for TxDescendants<'g, A, F> where F: FnMut(usize, Txid) -> Option, { diff --git a/crates/chain/tests/test_chain_graph.rs b/crates/chain/tests/test_chain_graph.rs index 68f50b8f..cd2a2894 100644 --- a/crates/chain/tests/test_chain_graph.rs +++ b/crates/chain/tests/test_chain_graph.rs @@ -1,14 +1,18 @@ #[macro_use] mod common; +use std::collections::BTreeSet; + use bdk_chain::{ chain_graph::*, collections::HashSet, sparse_chain, - tx_graph::{self, TxGraph}, + tx_graph::{self, GraphedTx, TxGraph}, BlockId, TxHeight, }; -use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness}; +use bitcoin::{ + BlockHash, OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness, +}; #[test] fn test_spent_by() { @@ -43,7 +47,7 @@ fn test_spent_by() { output: vec![], }; - let mut cg1 = ChainGraph::default(); + let mut cg1 = ChainGraph::<(u32, BlockHash), _>::default(); let _ = cg1 .insert_tx(tx1, TxHeight::Unconfirmed) .expect("should insert"); @@ -124,7 +128,7 @@ fn update_evicts_conflicting_tx() { cg }; - let changeset = ChangeSet:: { + let changeset = ChangeSet::<(u32, BlockHash), TxHeight> { chain: sparse_chain::ChangeSet { checkpoints: Default::default(), txids: [ @@ -133,9 +137,10 @@ fn update_evicts_conflicting_tx() { ] .into(), }, - graph: tx_graph::Additions { + graph: tx_graph::Additions::<(u32, BlockHash)> { tx: [tx_b2.clone()].into(), txout: [].into(), + ..Default::default() }, }; assert_eq!( @@ -149,7 +154,7 @@ fn update_evicts_conflicting_tx() { { let cg1 = { - let mut cg = ChainGraph::default(); + let mut cg = ChainGraph::<(u32, BlockHash), _>::default(); let _ = cg.insert_checkpoint(cp_a).expect("should insert cp"); let _ = cg.insert_checkpoint(cp_b).expect("should insert cp"); let _ = cg @@ -203,7 +208,7 @@ fn update_evicts_conflicting_tx() { cg }; - let changeset = ChangeSet:: { + let changeset = ChangeSet::<(u32, BlockHash), TxHeight> { chain: sparse_chain::ChangeSet { checkpoints: [(1, Some(h!("B'")))].into(), txids: [ @@ -212,9 +217,10 @@ fn update_evicts_conflicting_tx() { ] .into(), }, - graph: tx_graph::Additions { + graph: tx_graph::Additions::<(u32, BlockHash)> { tx: [tx_b2].into(), txout: [].into(), + ..Default::default() }, }; assert_eq!( @@ -250,7 +256,7 @@ fn chain_graph_new_missing() { (tx_b.txid(), TxHeight::Confirmed(0)) ] ); - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<(u32, BlockHash)>::default(); let mut expected_missing = HashSet::new(); expected_missing.insert(tx_a.txid()); @@ -287,7 +293,7 @@ fn chain_graph_new_missing() { let new_graph = ChainGraph::new(update.clone(), graph.clone()).unwrap(); let expected_graph = { - let mut cg = ChainGraph::::default(); + let mut cg = ChainGraph::<(u32, BlockHash), TxHeight>::default(); let _ = cg .insert_checkpoint(update.latest_checkpoint().unwrap()) .unwrap(); @@ -342,7 +348,7 @@ fn chain_graph_new_conflicts() { ] ); - let graph = TxGraph::new([tx_a, tx_b, tx_b2]); + let graph = TxGraph::<(u32, BlockHash)>::new([tx_a, tx_b, tx_b2]); assert!(matches!( ChainGraph::new(chain, graph), @@ -352,7 +358,7 @@ fn chain_graph_new_conflicts() { #[test] fn test_get_tx_in_chain() { - let mut cg = ChainGraph::default(); + let mut cg = ChainGraph::<(u32, BlockHash), _>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -363,13 +369,21 @@ fn test_get_tx_in_chain() { let _ = cg.insert_tx(tx.clone(), TxHeight::Unconfirmed).unwrap(); assert_eq!( cg.get_tx_in_chain(tx.txid()), - Some((&TxHeight::Unconfirmed, &tx)) + Some(( + &TxHeight::Unconfirmed, + GraphedTx { + txid: tx.txid(), + tx: &tx, + anchors: &BTreeSet::new(), + last_seen: 0 + } + )) ); } #[test] fn test_iterate_transactions() { - let mut cg = ChainGraph::default(); + let mut cg = ChainGraph::::default(); let txs = (0..3) .map(|i| Transaction { version: i, @@ -395,9 +409,18 @@ fn test_iterate_transactions() { assert_eq!( cg.transactions_in_chain().collect::>(), vec![ - (&TxHeight::Confirmed(0), &txs[2]), - (&TxHeight::Confirmed(1), &txs[0]), - (&TxHeight::Unconfirmed, &txs[1]), + ( + &TxHeight::Confirmed(0), + GraphedTx::from_tx(&txs[2], &BTreeSet::new()) + ), + ( + &TxHeight::Confirmed(1), + GraphedTx::from_tx(&txs[0], &BTreeSet::new()) + ), + ( + &TxHeight::Unconfirmed, + GraphedTx::from_tx(&txs[1], &BTreeSet::new()) + ), ] ); } @@ -457,7 +480,7 @@ fn test_apply_changes_reintroduce_tx() { // block1, block2a, tx1, tx2a let mut cg = { - let mut cg = ChainGraph::default(); + let mut cg = ChainGraph::<(u32, BlockHash), _>::default(); let _ = cg.insert_checkpoint(block1).unwrap(); let _ = cg.insert_checkpoint(block2a).unwrap(); let _ = cg.insert_tx(tx1, TxHeight::Confirmed(1)).unwrap(); @@ -613,7 +636,7 @@ fn test_evict_descendants() { let txid_conflict = tx_conflict.txid(); let cg = { - let mut cg = ChainGraph::::default(); + let mut cg = ChainGraph::<(u32, BlockHash), TxHeight>::default(); let _ = cg.insert_checkpoint(block_1); let _ = cg.insert_checkpoint(block_2a); let _ = cg.insert_tx(tx_1, TxHeight::Confirmed(1)); @@ -625,7 +648,7 @@ fn test_evict_descendants() { }; let update = { - let mut cg = ChainGraph::::default(); + let mut cg = ChainGraph::<(u32, BlockHash), TxHeight>::default(); let _ = cg.insert_checkpoint(block_1); let _ = cg.insert_checkpoint(block_2b); let _ = cg.insert_tx(tx_conflict.clone(), TxHeight::Confirmed(2)); diff --git a/crates/chain/tests/test_keychain_tracker.rs b/crates/chain/tests/test_keychain_tracker.rs index 3bf0a1d5..1c5e0795 100644 --- a/crates/chain/tests/test_keychain_tracker.rs +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -1,19 +1,22 @@ #![cfg(feature = "miniscript")] #[macro_use] mod common; +use std::collections::BTreeSet; + use bdk_chain::{ keychain::{Balance, KeychainTracker}, miniscript::{ bitcoin::{secp256k1::Secp256k1, OutPoint, PackedLockTime, Transaction, TxOut}, Descriptor, }, + tx_graph::GraphedTx, BlockId, ConfirmationTime, TxHeight, }; -use bitcoin::TxIn; +use bitcoin::{BlockHash, TxIn}; #[test] fn test_insert_tx() { - let mut tracker = KeychainTracker::default(); + let mut tracker = KeychainTracker::<_, BlockId, _>::default(); let secp = Secp256k1::new(); let (descriptor, _) = Descriptor::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap(); tracker.add_keychain((), descriptor.clone()); @@ -40,7 +43,10 @@ fn test_insert_tx() { .chain_graph() .transactions_in_chain() .collect::>(), - vec![(&ConfirmationTime::Unconfirmed, &tx)] + vec![( + &ConfirmationTime::Unconfirmed, + GraphedTx::from_tx(&tx, &BTreeSet::new()) + )] ); assert_eq!( @@ -66,7 +72,7 @@ fn test_balance() { One, Two, } - let mut tracker = KeychainTracker::::default(); + let mut tracker = KeychainTracker::::default(); let one = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#rg247h69").unwrap(); let two = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1/*)#ju05rz2a").unwrap(); tracker.add_keychain(Keychain::One, one); diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 04974bf3..2550d556 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,9 +2,12 @@ mod common; use bdk_chain::{ collections::*, - tx_graph::{Additions, TxGraph}, + tx_graph::{Additions, GraphedTx, TxGraph}, + BlockId, +}; +use bitcoin::{ + hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid, }; -use bitcoin::{hashes::Hash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid}; use core::iter; #[test] @@ -35,7 +38,7 @@ fn insert_txouts() { )]; let mut graph = { - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<(u32, BlockHash)>::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -69,6 +72,7 @@ fn insert_txouts() { Additions { tx: [].into(), txout: update_ops.into(), + ..Default::default() } ); @@ -90,7 +94,7 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() { output: vec![], }; - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<(u32, BlockHash)>::default(); let _ = graph.insert_tx(tx); assert!(graph.outspends(OutPoint::null()).is_empty()); assert!(graph.tx_outspends(Txid::all_zeros()).next().is_none()); @@ -120,8 +124,8 @@ fn insert_tx_graph_keeps_track_of_spend() { output: vec![], }; - let mut graph1 = TxGraph::default(); - let mut graph2 = TxGraph::default(); + let mut graph1 = TxGraph::<(u32, BlockHash)>::default(); + let mut graph2 = TxGraph::<(u32, BlockHash)>::default(); // insert in different order let _ = graph1.insert_tx(tx1.clone()); @@ -149,14 +153,17 @@ fn insert_tx_can_retrieve_full_tx_from_graph() { output: vec![TxOut::default()], }; - let mut graph = TxGraph::default(); + let mut graph = TxGraph::::default(); let _ = graph.insert_tx(tx.clone()); - assert_eq!(graph.get_tx(tx.txid()), Some(&tx)); + assert_eq!( + graph.get_tx(tx.txid()), + Some(GraphedTx::from_tx(&tx, &BTreeSet::new())) + ); } #[test] fn insert_tx_displaces_txouts() { - let mut tx_graph = TxGraph::default(); + let mut tx_graph = TxGraph::<(u32, BlockHash)>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -212,7 +219,7 @@ fn insert_tx_displaces_txouts() { #[test] fn insert_txout_does_not_displace_tx() { - let mut tx_graph = TxGraph::default(); + let mut tx_graph = TxGraph::<(u32, BlockHash)>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -268,7 +275,7 @@ fn insert_txout_does_not_displace_tx() { #[test] fn test_calculate_fee() { - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<(u32, BlockHash)>::default(); let intx1 = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -362,7 +369,7 @@ fn test_calculate_fee_on_coinbase() { output: vec![TxOut::default()], }; - let graph = TxGraph::default(); + let graph = TxGraph::<(u32, BlockHash)>::default(); assert_eq!(graph.calculate_fee(&tx), Some(0)); } @@ -404,7 +411,7 @@ fn test_conflicting_descendants() { let txid_a = tx_a.txid(); let txid_b = tx_b.txid(); - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<(u32, BlockHash)>::default(); let _ = graph.insert_tx(tx_a); let _ = graph.insert_tx(tx_b); @@ -480,7 +487,7 @@ fn test_descendants_no_repeat() { }) .collect::>(); - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<(u32, BlockHash)>::default(); let mut expected_txids = BTreeSet::new(); // these are NOT descendants of `tx_a` diff --git a/crates/electrum/src/lib.rs b/crates/electrum/src/lib.rs index bddbd8f2..d062cfdc 100644 --- a/crates/electrum/src/lib.rs +++ b/crates/electrum/src/lib.rs @@ -32,7 +32,7 @@ use bdk_chain::{ keychain::KeychainScan, sparse_chain::{self, ChainPosition, SparseChain}, tx_graph::TxGraph, - BlockId, ConfirmationTime, TxHeight, + BlockAnchor, BlockId, ConfirmationTime, TxHeight, }; pub use electrum_client; use electrum_client::{Client, ElectrumApi, Error}; @@ -243,13 +243,14 @@ impl ElectrumUpdate { /// `tracker`. /// /// This will fail if there are missing full transactions not provided via `new_txs`. - pub fn into_keychain_scan( + pub fn into_keychain_scan( self, new_txs: Vec, chain_graph: &CG, - ) -> Result, chain_graph::NewError

> + ) -> Result, chain_graph::NewError

> where - CG: AsRef>, + CG: AsRef>, + A: BlockAnchor, { Ok(KeychainScan { update: chain_graph diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 266fd30b..420f1197 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -48,7 +48,7 @@ pub trait EsploraAsyncExt { outpoints: impl IntoIterator + Send> + Send, stop_gap: usize, parallel_requests: usize, - ) -> Result, Error>; + ) -> Result, Error>; /// Convenience method to call [`scan`] without requiring a keychain. /// @@ -61,7 +61,7 @@ pub trait EsploraAsyncExt { txids: impl IntoIterator + Send> + Send, outpoints: impl IntoIterator + Send> + Send, parallel_requests: usize, - ) -> Result, Error> { + ) -> Result, Error> { let wallet_scan = self .scan( local_chain, @@ -100,7 +100,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { outpoints: impl IntoIterator + Send> + Send, stop_gap: usize, parallel_requests: usize, - ) -> Result, Error> { + ) -> Result, Error> { let txids = txids.into_iter(); let outpoints = outpoints.into_iter(); let parallel_requests = parallel_requests.max(1); diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index c22668a5..d4a511ac 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -38,7 +38,7 @@ pub trait EsploraExt { outpoints: impl IntoIterator, stop_gap: usize, parallel_requests: usize, - ) -> Result, Error>; + ) -> Result, Error>; /// Convenience method to call [`scan`] without requiring a keychain. /// @@ -51,7 +51,7 @@ pub trait EsploraExt { txids: impl IntoIterator, outpoints: impl IntoIterator, parallel_requests: usize, - ) -> Result, Error> { + ) -> Result, Error> { let wallet_scan = self.scan( local_chain, [( @@ -81,7 +81,7 @@ impl EsploraExt for esplora_client::BlockingClient { outpoints: impl IntoIterator, stop_gap: usize, parallel_requests: usize, - ) -> Result, Error> { + ) -> Result, Error> { let parallel_requests = parallel_requests.max(1); let mut scan = KeychainScan::default(); let update = &mut scan.update; diff --git a/crates/file_store/src/file_store.rs b/crates/file_store/src/file_store.rs index 824e3ccc..ba0dc21d 100644 --- a/crates/file_store/src/file_store.rs +++ b/crates/file_store/src/file_store.rs @@ -4,7 +4,7 @@ //! [`KeychainChangeSet`]s which can be used to restore a [`KeychainTracker`]. use bdk_chain::{ keychain::{KeychainChangeSet, KeychainTracker}, - sparse_chain, + sparse_chain, BlockAnchor, }; use bincode::{DefaultOptions, Options}; use core::marker::PhantomData; @@ -23,20 +23,21 @@ const MAGIC_BYTES: [u8; MAGIC_BYTES_LEN] = [98, 100, 107, 102, 115, 48, 48, 48, /// Persists an append only list of `KeychainChangeSet` to a single file. /// [`KeychainChangeSet`] record the changes made to a [`KeychainTracker`]. #[derive(Debug)] -pub struct KeychainStore { +pub struct KeychainStore { db_file: File, - changeset_type_params: core::marker::PhantomData<(K, P)>, + changeset_type_params: core::marker::PhantomData<(K, A, P)>, } fn bincode() -> impl bincode::Options { DefaultOptions::new().with_varint_encoding() } -impl KeychainStore +impl KeychainStore where K: Ord + Clone + core::fmt::Debug, + A: BlockAnchor, P: sparse_chain::ChainPosition, - KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, + KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, { /// Creates a new store from a [`File`]. /// @@ -85,7 +86,9 @@ where /// **WARNING**: This method changes the write position in the underlying file. You should /// always iterate over all entries until `None` is returned if you want your next write to go /// at the end; otherwise, you will write over existing entries. - pub fn iter_changesets(&mut self) -> Result>, io::Error> { + pub fn iter_changesets( + &mut self, + ) -> Result>, io::Error> { self.db_file .seek(io::SeekFrom::Start(MAGIC_BYTES_LEN as _))?; @@ -104,7 +107,7 @@ where /// /// **WARNING**: This method changes the write position of the underlying file. The next /// changeset will be written over the erroring entry (or the end of the file if none existed). - pub fn aggregate_changeset(&mut self) -> (KeychainChangeSet, Result<(), IterError>) { + pub fn aggregate_changeset(&mut self) -> (KeychainChangeSet, Result<(), IterError>) { let mut changeset = KeychainChangeSet::default(); let result = (|| { let iter_changeset = self.iter_changesets()?; @@ -124,7 +127,7 @@ where /// changeset will be written over the erroring entry (or the end of the file if none existed). pub fn load_into_keychain_tracker( &mut self, - tracker: &mut KeychainTracker, + tracker: &mut KeychainTracker, ) -> Result<(), IterError> { for changeset in self.iter_changesets()? { tracker.apply_changeset(changeset?) @@ -138,7 +141,7 @@ where /// directly after the appended changeset. pub fn append_changeset( &mut self, - changeset: &KeychainChangeSet, + changeset: &KeychainChangeSet, ) -> Result<(), io::Error> { if changeset.is_empty() { return Ok(()); @@ -288,7 +291,7 @@ mod test { use super::*; use bdk_chain::{ keychain::{DerivationAdditions, KeychainChangeSet}, - TxHeight, + BlockId, TxHeight, }; use std::{ io::{Read, Write}, @@ -332,7 +335,7 @@ mod test { file.write_all(&MAGIC_BYTES[..MAGIC_BYTES_LEN - 1]) .expect("should write"); - match KeychainStore::::new(file.reopen().unwrap()) { + match KeychainStore::::new(file.reopen().unwrap()) { Err(FileError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof), unexpected => panic!("unexpected result: {:?}", unexpected), }; @@ -346,7 +349,7 @@ mod test { file.write_all(invalid_magic_bytes.as_bytes()) .expect("should write"); - match KeychainStore::::new(file.reopen().unwrap()) { + match KeychainStore::::new(file.reopen().unwrap()) { Err(FileError::InvalidMagicBytes(b)) => { assert_eq!(b, invalid_magic_bytes.as_bytes()) } @@ -370,8 +373,9 @@ mod test { let mut file = NamedTempFile::new().unwrap(); file.write_all(&data).expect("should write"); - let mut store = KeychainStore::::new(file.reopen().unwrap()) - .expect("should open"); + let mut store = + KeychainStore::::new(file.reopen().unwrap()) + .expect("should open"); match store.iter_changesets().expect("seek should succeed").next() { Some(Err(IterError::Bincode(_))) => {} unexpected_res => panic!("unexpected result: {:?}", unexpected_res), diff --git a/crates/file_store/src/lib.rs b/crates/file_store/src/lib.rs index e3347419..a9673be9 100644 --- a/crates/file_store/src/lib.rs +++ b/crates/file_store/src/lib.rs @@ -3,14 +3,16 @@ mod file_store; use bdk_chain::{ keychain::{KeychainChangeSet, KeychainTracker, PersistBackend}, sparse_chain::ChainPosition, + BlockAnchor, }; pub use file_store::*; -impl PersistBackend for KeychainStore +impl PersistBackend for KeychainStore where K: Ord + Clone + core::fmt::Debug, + A: BlockAnchor, P: ChainPosition, - KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, + KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, { type WriteError = std::io::Error; @@ -18,14 +20,14 @@ where fn append_changeset( &mut self, - changeset: &KeychainChangeSet, + changeset: &KeychainChangeSet, ) -> Result<(), Self::WriteError> { KeychainStore::append_changeset(self, changeset) } fn load_into_keychain_tracker( &mut self, - tracker: &mut KeychainTracker, + tracker: &mut KeychainTracker, ) -> Result<(), Self::LoadError> { KeychainStore::load_into_keychain_tracker(self, tracker) } diff --git a/example-crates/keychain_tracker_electrum/src/main.rs b/example-crates/keychain_tracker_electrum/src/main.rs index c8b9e068..08f29ceb 100644 --- a/example-crates/keychain_tracker_electrum/src/main.rs +++ b/example-crates/keychain_tracker_electrum/src/main.rs @@ -48,7 +48,7 @@ pub struct ScanOptions { } fn main() -> anyhow::Result<()> { - let (args, keymap, tracker, db) = cli::init::()?; + let (args, keymap, tracker, db) = cli::init::()?; let electrum_url = match args.network { Network::Bitcoin => "ssl://electrum.blockstream.info:50002", diff --git a/example-crates/keychain_tracker_esplora/src/main.rs b/example-crates/keychain_tracker_esplora/src/main.rs index cae5e960..04d121d2 100644 --- a/example-crates/keychain_tracker_esplora/src/main.rs +++ b/example-crates/keychain_tracker_esplora/src/main.rs @@ -49,7 +49,7 @@ pub struct ScanOptions { } fn main() -> anyhow::Result<()> { - let (args, keymap, keychain_tracker, db) = cli::init::()?; + let (args, keymap, keychain_tracker, db) = cli::init::()?; let esplora_url = match args.network { Network::Bitcoin => "https://mempool.space/api", Network::Testnet => "https://mempool.space/testnet/api", diff --git a/example-crates/keychain_tracker_example_cli/src/lib.rs b/example-crates/keychain_tracker_example_cli/src/lib.rs index df42df1a..e118cbf4 100644 --- a/example-crates/keychain_tracker_example_cli/src/lib.rs +++ b/example-crates/keychain_tracker_example_cli/src/lib.rs @@ -13,7 +13,7 @@ use bdk_chain::{ Descriptor, DescriptorPublicKey, }, sparse_chain::{self, ChainPosition}, - DescriptorExt, FullTxOut, + BlockAnchor, DescriptorExt, FullTxOut, }; use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; use bdk_file_store::KeychainStore; @@ -179,15 +179,16 @@ pub struct AddrsOutput { used: bool, } -pub fn run_address_cmd

( - tracker: &Mutex>, - db: &Mutex>, +pub fn run_address_cmd( + tracker: &Mutex>, + db: &Mutex>, addr_cmd: AddressCmd, network: Network, ) -> Result<()> where + A: bdk_chain::BlockAnchor, P: bdk_chain::sparse_chain::ChainPosition, - KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, + KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, { let mut tracker = tracker.lock().unwrap(); let txout_index = &mut tracker.txout_index; @@ -241,7 +242,9 @@ where } } -pub fn run_balance_cmd(tracker: &Mutex>) { +pub fn run_balance_cmd( + tracker: &Mutex>, +) { let tracker = tracker.lock().unwrap(); let (confirmed, unconfirmed) = tracker @@ -258,9 +261,9 @@ pub fn run_balance_cmd(tracker: &Mutex( +pub fn run_txo_cmd( txout_cmd: TxOutCmd, - tracker: &Mutex>, + tracker: &Mutex>, network: Network, ) { match txout_cmd { @@ -313,11 +316,11 @@ pub fn run_txo_cmd( } #[allow(clippy::type_complexity)] // FIXME -pub fn create_tx( +pub fn create_tx( value: u64, address: Address, coin_select: CoinSelectionAlgo, - keychain_tracker: &mut KeychainTracker, + keychain_tracker: &mut KeychainTracker, keymap: &HashMap, ) -> Result<( Transaction, @@ -526,19 +529,20 @@ pub fn create_tx( Ok((transaction, change_info)) } -pub fn handle_commands( +pub fn handle_commands( command: Commands, broadcast: impl FnOnce(&Transaction) -> Result<()>, // we Mutex around these not because we need them for a simple CLI app but to demonstrate how // all the stuff we're doing can be made thread-safe and not keep locks up over an IO bound. - tracker: &Mutex>, - store: &Mutex>, + tracker: &Mutex>, + store: &Mutex>, network: Network, keymap: &HashMap, ) -> Result<()> where + A: BlockAnchor, P: ChainPosition, - KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, + KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, { match command { // TODO: Make these functions return stuffs @@ -619,17 +623,18 @@ where } #[allow(clippy::type_complexity)] // FIXME -pub fn init() -> anyhow::Result<( +pub fn init() -> anyhow::Result<( Args, KeyMap, // These don't need to have mutexes around them, but we want the cli example code to make it obvious how they // are thread-safe, forcing the example developers to show where they would lock and unlock things. - Mutex>, - Mutex>, + Mutex>, + Mutex>, )> where + A: BlockAnchor, P: sparse_chain::ChainPosition, - KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, + KeychainChangeSet: serde::Serialize + serde::de::DeserializeOwned, { let args = Args::::parse(); let secp = Secp256k1::default(); @@ -655,7 +660,7 @@ where .add_keychain(Keychain::Internal, internal_descriptor); }; - let mut db = KeychainStore::::new_from_path(args.db_path.as_path())?; + let mut db = KeychainStore::::new_from_path(args.db_path.as_path())?; if let Err(e) = db.load_into_keychain_tracker(&mut tracker) { match tracker.chain().latest_checkpoint() { @@ -669,8 +674,8 @@ where Ok((args, keymap, Mutex::new(tracker), Mutex::new(db))) } -pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, P: ChainPosition>( - tracker: &'a KeychainTracker, +pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, A: BlockAnchor, P: ChainPosition>( + tracker: &'a KeychainTracker, assets: &'a bdk_tmp_plan::Assets, ) -> impl Iterator, FullTxOut

)> + 'a { tracker