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 01/48] [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 From 61a8606fbcaec933f915c4f0600cd6f5e35636e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 24 Mar 2023 15:47:39 +0800 Subject: [PATCH 02/48] [bdk_chain_redesign] Introduce `ChainOracle` and `TxIndex` traits The chain oracle keeps track of the best chain, while the transaction index indexes transaction data in relation to script pubkeys. This commit also includes initial work on `IndexedTxGraph`. --- crates/chain/src/chain_data.rs | 9 ++ crates/chain/src/keychain.rs | 8 +- crates/chain/src/keychain/txout_index.rs | 18 ++- crates/chain/src/sparse_chain.rs | 10 +- crates/chain/src/spk_txout_index.rs | 21 ++- crates/chain/src/tx_data_traits.rs | 72 +++++++++ crates/chain/src/tx_graph.rs | 182 ++++++++++++++++++++++- 7 files changed, 315 insertions(+), 5 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index ec76dbb7..147ce240 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -5,6 +5,15 @@ use crate::{ BlockAnchor, COINBASE_MATURITY, }; +/// Represents an observation of some chain data. +#[derive(Debug, Clone, Copy)] +pub enum Observation { + /// The chain data is seen in a block identified by `A`. + InBlock(A), + /// The chain data is seen at this given unix timestamp. + SeenAt(u64), +} + /// Represents the height at which a transaction is confirmed. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr( diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 92d72841..dd419db5 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -19,7 +19,7 @@ use crate::{ collections::BTreeMap, sparse_chain::ChainPosition, tx_graph::TxGraph, - ForEachTxOut, + ForEachTxOut, TxIndexAdditions, }; #[cfg(feature = "miniscript")] @@ -85,6 +85,12 @@ impl DerivationAdditions { } } +impl TxIndexAdditions for DerivationAdditions { + fn append_additions(&mut self, other: Self) { + self.append(other) + } +} + impl Default for DerivationAdditions { fn default() -> Self { Self(Default::default()) diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index feb71edb..b60e0584 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,7 +1,7 @@ use crate::{ collections::*, miniscript::{Descriptor, DescriptorPublicKey}, - ForEachTxOut, SpkTxOutIndex, + ForEachTxOut, SpkTxOutIndex, TxIndex, }; use alloc::{borrow::Cow, vec::Vec}; use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut}; @@ -88,6 +88,22 @@ impl Deref for KeychainTxOutIndex { } } +impl TxIndex for KeychainTxOutIndex { + type Additions = DerivationAdditions; + + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { + self.scan_txout(outpoint, txout) + } + + fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::Additions { + self.scan(tx) + } + + fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { + self.is_relevant(tx) + } +} + impl KeychainTxOutIndex { /// Scans an object for relevant outpoints, which are stored and indexed internally. /// diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index a449638d..eb6e3e2a 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -311,7 +311,7 @@ use core::{ ops::{Bound, RangeBounds}, }; -use crate::{collections::*, tx_graph::TxGraph, BlockId, FullTxOut, TxHeight}; +use crate::{collections::*, tx_graph::TxGraph, BlockId, ChainOracle, FullTxOut, TxHeight}; use bitcoin::{hashes::Hash, BlockHash, OutPoint, Txid}; /// This is a non-monotone structure that tracks relevant [`Txid`]s that are ordered by chain @@ -456,6 +456,14 @@ impl core::fmt::Display for UpdateError

{ #[cfg(feature = "std")] impl std::error::Error for UpdateError

{} +impl ChainOracle for SparseChain

{ + type Error = (); + + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { + Ok(self.checkpoint_at(height).map(|b| b.hash)) + } +} + impl SparseChain

{ /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they /// are in the same chain. diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 7f46604f..3ce6c06c 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -2,7 +2,7 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - ForEachTxOut, + ForEachTxOut, TxIndex, }; use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid}; @@ -52,6 +52,25 @@ impl Default for SpkTxOutIndex { } } +impl TxIndex for SpkTxOutIndex { + type Additions = BTreeSet; + + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { + self.scan_txout(outpoint, txout) + .cloned() + .into_iter() + .collect() + } + + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions { + self.scan(tx) + } + + fn is_tx_relevant(&self, tx: &Transaction) -> bool { + self.is_relevant(tx) + } +} + /// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a /// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a /// reference out of the `ForEachTxOut` closure during scanning. diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 9b9facab..43ce487e 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,3 +1,4 @@ +use alloc::collections::BTreeSet; use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; use crate::BlockId; @@ -44,8 +45,79 @@ pub trait BlockAnchor: fn anchor_block(&self) -> BlockId; } +impl BlockAnchor for &'static A { + fn anchor_block(&self) -> BlockId { + ::anchor_block(self) + } +} + impl BlockAnchor for (u32, BlockHash) { fn anchor_block(&self) -> BlockId { (*self).into() } } + +/// Represents a service that tracks the best chain history. +pub trait ChainOracle { + /// Error type. + type Error: core::fmt::Debug; + + /// Returns the block hash (if any) of the given `height`. + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error>; + + /// Determines whether the block of [`BlockId`] exists in the best chain. + fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { + Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash)) + } +} + +impl ChainOracle for &C { + type Error = C::Error; + + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { + ::get_block_in_best_chain(self, height) + } + + fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { + ::is_block_in_best_chain(self, block_id) + } +} + +/// Represents changes to a [`TxIndex`] implementation. +pub trait TxIndexAdditions: Default { + /// Append `other` on top of `self`. + fn append_additions(&mut self, other: Self); +} + +impl TxIndexAdditions for BTreeSet { + fn append_additions(&mut self, mut other: Self) { + self.append(&mut other); + } +} + +/// Represents an index of transaction data. +pub trait TxIndex { + /// The resultant "additions" when new transaction data is indexed. + type Additions: TxIndexAdditions; + + /// Scan and index the given `outpoint` and `txout`. + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions; + + /// Scan and index the given transaction. + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions { + let txid = tx.txid(); + tx.output + .iter() + .enumerate() + .map(|(vout, txout)| self.index_txout(OutPoint::new(txid, vout as _), txout)) + .reduce(|mut acc, other| { + acc.append_additions(other); + acc + }) + .unwrap_or_default() + } + + /// A transaction is relevant if it contains a txout with a script_pubkey that we own, or if it + /// spends an already-indexed outpoint that we have previously indexed. + fn is_tx_relevant(&self, tx: &Transaction) -> bool; +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 824b68e2..daa7e1ba 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,10 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, BlockAnchor, BlockId, ForEachTxOut}; +use crate::{ + collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, Observation, TxIndex, + TxIndexAdditions, +}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::ops::{Deref, RangeInclusive}; @@ -209,6 +212,12 @@ impl TxGraph { }) } + pub fn get_anchors_and_last_seen(&self, txid: Txid) -> Option<(&BTreeSet, u64)> { + self.txs + .get(&txid) + .map(|(_, anchors, last_seen)| (anchors, *last_seen)) + } + /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. /// Returns `Some(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as /// the full transactions or individual txouts). If the returned value is negative, then the @@ -462,6 +471,75 @@ impl TxGraph { *update_last_seen = seen_at; self.determine_additions(&update) } + + /// Determines whether a transaction of `txid` is in the best chain. + /// + /// TODO: Also return conflicting tx list, ordered by last_seen. + pub fn is_txid_in_best_chain(&self, chain: C, txid: Txid) -> Result + where + C: ChainOracle, + { + let (tx_node, anchors, &last_seen) = match self.txs.get(&txid) { + Some((tx, anchors, last_seen)) if !(anchors.is_empty() && *last_seen == 0) => { + (tx, anchors, last_seen) + } + _ => return Ok(false), + }; + + for block_id in anchors.iter().map(A::anchor_block) { + if chain.is_block_in_best_chain(block_id)? { + return Ok(true); + } + } + + // The tx is not anchored to a block which is in the best chain, let's check whether we can + // ignore it by checking conflicts! + let tx = match tx_node { + TxNode::Whole(tx) => tx, + TxNode::Partial(_) => { + // [TODO] Unfortunately, we can't iterate over conflicts of partial txs right now! + // [TODO] So we just assume the partial tx does not exist in the best chain :/ + return Ok(false); + } + }; + + // [TODO] Is this logic correct? I do not think so, but it should be good enough for now! + let mut latest_last_seen = 0_u64; + for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx(txid)) { + for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) { + if chain.is_block_in_best_chain(block_id)? { + // conflicting tx is in best chain, so the current tx cannot be in best chain! + return Ok(false); + } + } + if conflicting_tx.last_seen > latest_last_seen { + latest_last_seen = conflicting_tx.last_seen; + } + } + if last_seen >= latest_last_seen { + Ok(true) + } else { + Ok(false) + } + } + + /// Return true if `outpoint` exists in best chain and is unspent. + pub fn is_unspent(&self, chain: C, outpoint: OutPoint) -> Result + where + C: ChainOracle, + { + if !self.is_txid_in_best_chain(&chain, outpoint.txid)? { + return Ok(false); + } + if let Some(spends) = self.spends.get(&outpoint) { + for &txid in spends { + if self.is_txid_in_best_chain(&chain, txid)? { + return Ok(false); + } + } + } + Ok(true) + } } impl TxGraph { @@ -568,6 +646,108 @@ impl TxGraph { } } +pub struct IndexedAdditions { + pub graph_additions: Additions, + pub index_delta: D, +} + +impl Default for IndexedAdditions { + fn default() -> Self { + Self { + graph_additions: Default::default(), + index_delta: Default::default(), + } + } +} + +impl TxIndexAdditions for IndexedAdditions { + fn append_additions(&mut self, other: Self) { + let Self { + graph_additions, + index_delta, + } = other; + self.graph_additions.append(graph_additions); + self.index_delta.append_additions(index_delta); + } +} + +pub struct IndexedTxGraph { + graph: TxGraph, + index: I, +} + +impl Default for IndexedTxGraph { + fn default() -> Self { + Self { + graph: Default::default(), + index: Default::default(), + } + } +} + +impl IndexedTxGraph { + pub fn insert_txout( + &mut self, + outpoint: OutPoint, + txout: &TxOut, + observation: Observation, + ) -> IndexedAdditions { + IndexedAdditions { + graph_additions: { + let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); + graph_additions.append(match observation { + Observation::InBlock(anchor) => self.graph.insert_anchor(outpoint.txid, anchor), + Observation::SeenAt(seen_at) => { + self.graph.insert_seen_at(outpoint.txid, seen_at) + } + }); + graph_additions + }, + index_delta: ::index_txout(&mut self.index, outpoint, txout), + } + } + + pub fn insert_tx( + &mut self, + tx: &Transaction, + observation: Observation, + ) -> IndexedAdditions { + let txid = tx.txid(); + IndexedAdditions { + graph_additions: { + let mut graph_additions = self.graph.insert_tx(tx.clone()); + graph_additions.append(match observation { + Observation::InBlock(anchor) => self.graph.insert_anchor(txid, anchor), + Observation::SeenAt(seen_at) => self.graph.insert_seen_at(txid, seen_at), + }); + graph_additions + }, + index_delta: ::index_tx(&mut self.index, tx), + } + } + + pub fn filter_and_insert_txs<'t, T>( + &mut self, + txs: T, + observation: Observation, + ) -> IndexedAdditions + where + T: Iterator, + { + txs.filter_map(|tx| { + if self.index.is_tx_relevant(tx) { + Some(self.insert_tx(tx, observation.clone())) + } else { + None + } + }) + .fold(IndexedAdditions::default(), |mut acc, other| { + acc.append_additions(other); + acc + }) + } +} + /// A structure that represents changes to a [`TxGraph`]. /// /// It is named "additions" because [`TxGraph`] is monotone, so transactions can only be added and From 43b648fee02291858dfcab9b639c55a0bc3fad81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sun, 26 Mar 2023 11:24:30 +0800 Subject: [PATCH 03/48] [bdk_chain_redesign] Add `..in_chain` methods Add methods to `TxGraph` and `IndexedTxGraph` that gets in-best-chain data (such as transactions, txouts, unspent txouts). --- crates/bdk/src/wallet/mod.rs | 4 +- crates/chain/src/chain_data.rs | 41 +++- crates/chain/src/chain_graph.rs | 6 +- crates/chain/src/keychain/txout_index.rs | 8 +- crates/chain/src/spk_txout_index.rs | 8 +- crates/chain/src/tx_data_traits.rs | 7 +- crates/chain/src/tx_graph.rs | 208 ++++++++++++++++---- crates/chain/tests/test_chain_graph.rs | 10 +- crates/chain/tests/test_keychain_tracker.rs | 4 +- crates/chain/tests/test_tx_graph.rs | 4 +- 10 files changed, 236 insertions(+), 64 deletions(-) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 65d3008b..194c6c90 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -24,7 +24,7 @@ use bdk_chain::{ chain_graph, keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker}, sparse_chain, - tx_graph::GraphedTx, + tx_graph::TxInGraph, BlockId, ConfirmationTime, }; use bitcoin::consensus::encode::serialize; @@ -524,7 +524,7 @@ impl Wallet { /// unconfirmed transactions last. pub fn transactions( &self, - ) -> impl DoubleEndedIterator)> + '_ + ) -> impl DoubleEndedIterator)> + '_ { self.keychain_tracker .chain_graph() diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 147ce240..43eb64f6 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -6,12 +6,41 @@ use crate::{ }; /// Represents an observation of some chain data. -#[derive(Debug, Clone, Copy)] -pub enum Observation { +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] +pub enum ObservedIn { /// The chain data is seen in a block identified by `A`. - InBlock(A), - /// The chain data is seen at this given unix timestamp. - SeenAt(u64), + Block(A), + /// The chain data is seen in mempool at this given timestamp. + Mempool(u64), +} + +impl ChainPosition for ObservedIn { + fn height(&self) -> TxHeight { + match self { + ObservedIn::Block(block_id) => TxHeight::Confirmed(block_id.height), + ObservedIn::Mempool(_) => TxHeight::Unconfirmed, + } + } + + fn max_ord_of_height(height: TxHeight) -> Self { + match height { + TxHeight::Confirmed(height) => ObservedIn::Block(BlockId { + height, + hash: Hash::from_inner([u8::MAX; 32]), + }), + TxHeight::Unconfirmed => Self::Mempool(u64::MAX), + } + } + + fn min_ord_of_height(height: TxHeight) -> Self { + match height { + TxHeight::Confirmed(height) => ObservedIn::Block(BlockId { + height, + hash: Hash::from_inner([u8::MIN; 32]), + }), + TxHeight::Unconfirmed => Self::Mempool(u64::MIN), + } + } } /// Represents the height at which a transaction is confirmed. @@ -177,7 +206,7 @@ impl From<(&u32, &BlockHash)> for BlockId { } /// A `TxOut` with as much data as we can retrieve about it -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct FullTxOut { /// The location of the `TxOut`. pub outpoint: OutPoint, diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index 1a6ccb1e..fcb98043 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -2,7 +2,7 @@ use crate::{ collections::HashSet, sparse_chain::{self, ChainPosition, SparseChain}, - tx_graph::{self, GraphedTx, TxGraph}, + tx_graph::{self, TxGraph, TxInGraph}, BlockAnchor, BlockId, ForEachTxOut, FullTxOut, TxHeight, }; use alloc::{string::ToString, vec::Vec}; @@ -213,7 +213,7 @@ 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, GraphedTx<'_, Transaction, A>)> { + pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, TxInGraph<'_, Transaction, A>)> { let position = self.chain.tx_position(txid)?; let graphed_tx = self.graph.get_tx(txid).expect("must exist"); Some((position, graphed_tx)) @@ -441,7 +441,7 @@ where /// in ascending order. pub fn transactions_in_chain( &self, - ) -> impl DoubleEndedIterator)> { + ) -> impl DoubleEndedIterator)> { self.chain .txids() .map(move |(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist"))) diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index b60e0584..176254b4 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -88,9 +88,11 @@ impl Deref for KeychainTxOutIndex { } } -impl TxIndex for KeychainTxOutIndex { +impl TxIndex for KeychainTxOutIndex { type Additions = DerivationAdditions; + type SpkIndex = (K, u32); + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { self.scan_txout(outpoint, txout) } @@ -102,6 +104,10 @@ impl TxIndex for KeychainTxOutIndex { fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { self.is_relevant(tx) } + + fn relevant_txouts(&self) -> &BTreeMap { + self.inner.relevant_txouts() + } } impl KeychainTxOutIndex { diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 3ce6c06c..3d2f783e 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -52,9 +52,11 @@ impl Default for SpkTxOutIndex { } } -impl TxIndex for SpkTxOutIndex { +impl TxIndex for SpkTxOutIndex { type Additions = BTreeSet; + type SpkIndex = I; + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { self.scan_txout(outpoint, txout) .cloned() @@ -69,6 +71,10 @@ impl TxIndex for SpkTxOutIndex { fn is_tx_relevant(&self, tx: &Transaction) -> bool { self.is_relevant(tx) } + + fn relevant_txouts(&self) -> &BTreeMap { + &self.txouts + } } /// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 43ce487e..f412f452 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,4 +1,4 @@ -use alloc::collections::BTreeSet; +use alloc::collections::{BTreeMap, BTreeSet}; use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; use crate::BlockId; @@ -100,6 +100,8 @@ pub trait TxIndex { /// The resultant "additions" when new transaction data is indexed. type Additions: TxIndexAdditions; + type SpkIndex: Ord; + /// Scan and index the given `outpoint` and `txout`. fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions; @@ -120,4 +122,7 @@ pub trait TxIndex { /// A transaction is relevant if it contains a txout with a script_pubkey that we own, or if it /// spends an already-indexed outpoint that we have previously indexed. fn is_tx_relevant(&self, tx: &Transaction) -> bool; + + /// Lists all relevant txouts known by the index. + fn relevant_txouts(&self) -> &BTreeMap; } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index daa7e1ba..3181ed2a 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -56,8 +56,8 @@ //! ``` use crate::{ - collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, Observation, TxIndex, - TxIndexAdditions, + collections::*, sparse_chain::ChainPosition, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, + FullTxOut, ObservedIn, TxIndex, TxIndexAdditions, }; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; @@ -91,9 +91,12 @@ impl Default for TxGraph { } } +// pub type InChainTx<'a, T, A> = (ObservedIn<&'a A>, TxInGraph<'a, T, A>); +// pub type InChainTxOut<'a, I, A> = (&'a I, FullTxOut>); + /// An outward-facing view of a transaction that resides in a [`TxGraph`]. -#[derive(Clone, Debug, PartialEq)] -pub struct GraphedTx<'a, T, A> { +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxInGraph<'a, T, A> { /// Txid of the transaction. pub txid: Txid, /// A partial or full representation of the transaction. @@ -104,7 +107,7 @@ pub struct GraphedTx<'a, T, A> { pub last_seen: u64, } -impl<'a, T, A> Deref for GraphedTx<'a, T, A> { +impl<'a, T, A> Deref for TxInGraph<'a, T, A> { type Target = T; fn deref(&self) -> &Self::Target { @@ -112,7 +115,7 @@ impl<'a, T, A> Deref for GraphedTx<'a, T, A> { } } -impl<'a, A> GraphedTx<'a, Transaction, A> { +impl<'a, A> TxInGraph<'a, Transaction, A> { pub fn from_tx(tx: &'a Transaction, anchors: &'a BTreeSet) -> Self { Self { txid: tx.txid(), @@ -123,6 +126,18 @@ impl<'a, A> GraphedTx<'a, Transaction, A> { } } +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxInChain<'a, T, A> { + pub observed_in: ObservedIn<&'a A>, + pub tx: TxInGraph<'a, T, A>, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxOutInChain<'a, I, A> { + pub spk_index: &'a I, + pub txout: FullTxOut>, +} + /// 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 @@ -157,11 +172,11 @@ impl TxGraph { } /// Iterate over all full transactions in the graph. - pub fn full_transactions(&self) -> impl Iterator> { + pub fn full_transactions(&self) -> impl Iterator> { self.txs .iter() .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { - TxNode::Whole(tx) => Some(GraphedTx { + TxNode::Whole(tx) => Some(TxInGraph { txid, tx, anchors, @@ -176,9 +191,9 @@ impl TxGraph { /// Refer to [`get_txout`] for getting a specific [`TxOut`]. /// /// [`get_txout`]: Self::get_txout - pub fn get_tx(&self, txid: Txid) -> Option> { + pub fn get_tx(&self, txid: Txid) -> Option> { match &self.txs.get(&txid)? { - (TxNode::Whole(tx), anchors, last_seen) => Some(GraphedTx { + (TxNode::Whole(tx), anchors, last_seen) => Some(TxInGraph { txid, tx, anchors, @@ -212,12 +227,6 @@ impl TxGraph { }) } - pub fn get_anchors_and_last_seen(&self, txid: Txid) -> Option<(&BTreeSet, u64)> { - self.txs - .get(&txid) - .map(|(_, anchors, last_seen)| (anchors, *last_seen)) - } - /// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction. /// Returns `Some(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as /// the full transactions or individual txouts). If the returned value is negative, then the @@ -472,10 +481,22 @@ impl TxGraph { self.determine_additions(&update) } + /// Get all heights that are relevant to the graph. + pub fn relevant_heights(&self) -> BTreeSet { + self.anchors + .iter() + .map(|(a, _)| a.anchor_block().height) + .collect() + } + /// Determines whether a transaction of `txid` is in the best chain. /// /// TODO: Also return conflicting tx list, ordered by last_seen. - pub fn is_txid_in_best_chain(&self, chain: C, txid: Txid) -> Result + pub fn get_position_in_chain( + &self, + chain: C, + txid: Txid, + ) -> Result>, C::Error> where C: ChainOracle, { @@ -483,12 +504,12 @@ impl TxGraph { Some((tx, anchors, last_seen)) if !(anchors.is_empty() && *last_seen == 0) => { (tx, anchors, last_seen) } - _ => return Ok(false), + _ => return Ok(None), }; - for block_id in anchors.iter().map(A::anchor_block) { - if chain.is_block_in_best_chain(block_id)? { - return Ok(true); + for anchor in anchors { + if chain.is_block_in_best_chain(anchor.anchor_block())? { + return Ok(Some(ObservedIn::Block(anchor))); } } @@ -499,7 +520,7 @@ impl TxGraph { TxNode::Partial(_) => { // [TODO] Unfortunately, we can't iterate over conflicts of partial txs right now! // [TODO] So we just assume the partial tx does not exist in the best chain :/ - return Ok(false); + return Ok(None); } }; @@ -509,7 +530,7 @@ impl TxGraph { for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) { if chain.is_block_in_best_chain(block_id)? { // conflicting tx is in best chain, so the current tx cannot be in best chain! - return Ok(false); + return Ok(None); } } if conflicting_tx.last_seen > latest_last_seen { @@ -517,28 +538,47 @@ impl TxGraph { } } if last_seen >= latest_last_seen { - Ok(true) + Ok(Some(ObservedIn::Mempool(last_seen))) } else { - Ok(false) + Ok(None) } } - /// Return true if `outpoint` exists in best chain and is unspent. - pub fn is_unspent(&self, chain: C, outpoint: OutPoint) -> Result + pub fn get_spend_in_chain( + &self, + chain: C, + outpoint: OutPoint, + ) -> Result, Txid)>, C::Error> where C: ChainOracle, { - if !self.is_txid_in_best_chain(&chain, outpoint.txid)? { - return Ok(false); + if self.get_position_in_chain(&chain, outpoint.txid)?.is_none() { + return Ok(None); } if let Some(spends) = self.spends.get(&outpoint) { for &txid in spends { - if self.is_txid_in_best_chain(&chain, txid)? { - return Ok(false); + if let Some(observed_at) = self.get_position_in_chain(&chain, txid)? { + return Ok(Some((observed_at, txid))); } } } - Ok(true) + Ok(None) + } + + pub fn transactions_in_chain( + &self, + chain: C, + ) -> Result>, C::Error> + where + C: ChainOracle, + { + self.full_transactions() + .filter_map(|tx| { + self.get_position_in_chain(&chain, tx.txid) + .map(|v| v.map(|observed_in| TxInChain { observed_in, tx })) + .transpose() + }) + .collect() } } @@ -574,12 +614,12 @@ impl TxGraph { /// Iterate over all partial transactions (outputs only) in the graph. pub fn partial_transactions( &self, - ) -> impl Iterator, A>> { + ) -> impl Iterator, A>> { self.txs .iter() .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { TxNode::Whole(_) => None, - TxNode::Partial(partial) => Some(GraphedTx { + TxNode::Partial(partial) => Some(TxInGraph { txid, tx: partial, anchors, @@ -686,18 +726,29 @@ impl Default for IndexedTxGraph { } impl IndexedTxGraph { + /// Get a reference of the internal transaction graph. + pub fn graph(&self) -> &TxGraph { + &self.graph + } + + /// Get a reference of the internal transaction index. + pub fn index(&self) -> &I { + &self.index + } + + /// Insert a `txout` that exists in `outpoint` with the given `observation`. pub fn insert_txout( &mut self, outpoint: OutPoint, txout: &TxOut, - observation: Observation, + observation: ObservedIn, ) -> IndexedAdditions { IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); graph_additions.append(match observation { - Observation::InBlock(anchor) => self.graph.insert_anchor(outpoint.txid, anchor), - Observation::SeenAt(seen_at) => { + ObservedIn::Block(anchor) => self.graph.insert_anchor(outpoint.txid, anchor), + ObservedIn::Mempool(seen_at) => { self.graph.insert_seen_at(outpoint.txid, seen_at) } }); @@ -710,15 +761,15 @@ impl IndexedTxGraph { pub fn insert_tx( &mut self, tx: &Transaction, - observation: Observation, + observation: ObservedIn, ) -> IndexedAdditions { let txid = tx.txid(); IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_tx(tx.clone()); graph_additions.append(match observation { - Observation::InBlock(anchor) => self.graph.insert_anchor(txid, anchor), - Observation::SeenAt(seen_at) => self.graph.insert_seen_at(txid, seen_at), + ObservedIn::Block(anchor) => self.graph.insert_anchor(txid, anchor), + ObservedIn::Mempool(seen_at) => self.graph.insert_seen_at(txid, seen_at), }); graph_additions }, @@ -729,7 +780,7 @@ impl IndexedTxGraph { pub fn filter_and_insert_txs<'t, T>( &mut self, txs: T, - observation: Observation, + observation: ObservedIn, ) -> IndexedAdditions where T: Iterator, @@ -746,6 +797,81 @@ impl IndexedTxGraph { acc }) } + + pub fn relevant_heights(&self) -> BTreeSet { + self.graph.relevant_heights() + } + + pub fn txs_in_chain( + &self, + chain: C, + ) -> Result>, C::Error> + where + C: ChainOracle, + { + let mut tx_set = self.graph.transactions_in_chain(chain)?; + tx_set.retain(|tx| self.index.is_tx_relevant(&tx.tx)); + Ok(tx_set) + } + + pub fn txouts_in_chain( + &self, + chain: C, + ) -> Result>, C::Error> + where + C: ChainOracle, + ObservedIn: ChainPosition, + { + self.index + .relevant_txouts() + .iter() + .filter_map(|(op, (spk_i, txout))| -> Option> { + let graph_tx = self.graph.get_tx(op.txid)?; + + let is_on_coinbase = graph_tx.is_coin_base(); + + let chain_position = match self.graph.get_position_in_chain(&chain, op.txid) { + Ok(Some(observed_at)) => observed_at, + Ok(None) => return None, + Err(err) => return Some(Err(err)), + }; + + let spent_by = match self.graph.get_spend_in_chain(&chain, *op) { + Ok(spent_by) => spent_by, + Err(err) => return Some(Err(err)), + }; + + let full_txout = FullTxOut { + outpoint: *op, + txout: txout.clone(), + chain_position, + spent_by, + is_on_coinbase, + }; + + let txout_in_chain = TxOutInChain { + spk_index: spk_i, + txout: full_txout, + }; + + Some(Ok(txout_in_chain)) + }) + .collect() + } + + /// Return relevant unspents. + pub fn utxos_in_chain( + &self, + chain: C, + ) -> Result>, C::Error> + where + C: ChainOracle, + ObservedIn: ChainPosition, + { + let mut txouts = self.txouts_in_chain(chain)?; + txouts.retain(|txo| txo.txout.spent_by.is_none()); + Ok(txouts) + } } /// A structure that represents changes to a [`TxGraph`]. diff --git a/crates/chain/tests/test_chain_graph.rs b/crates/chain/tests/test_chain_graph.rs index cd2a2894..f7b39d2b 100644 --- a/crates/chain/tests/test_chain_graph.rs +++ b/crates/chain/tests/test_chain_graph.rs @@ -7,7 +7,7 @@ use bdk_chain::{ chain_graph::*, collections::HashSet, sparse_chain, - tx_graph::{self, GraphedTx, TxGraph}, + tx_graph::{self, TxGraph, TxInGraph}, BlockId, TxHeight, }; use bitcoin::{ @@ -371,7 +371,7 @@ fn test_get_tx_in_chain() { cg.get_tx_in_chain(tx.txid()), Some(( &TxHeight::Unconfirmed, - GraphedTx { + TxInGraph { txid: tx.txid(), tx: &tx, anchors: &BTreeSet::new(), @@ -411,15 +411,15 @@ fn test_iterate_transactions() { vec![ ( &TxHeight::Confirmed(0), - GraphedTx::from_tx(&txs[2], &BTreeSet::new()) + TxInGraph::from_tx(&txs[2], &BTreeSet::new()) ), ( &TxHeight::Confirmed(1), - GraphedTx::from_tx(&txs[0], &BTreeSet::new()) + TxInGraph::from_tx(&txs[0], &BTreeSet::new()) ), ( &TxHeight::Unconfirmed, - GraphedTx::from_tx(&txs[1], &BTreeSet::new()) + TxInGraph::from_tx(&txs[1], &BTreeSet::new()) ), ] ); diff --git a/crates/chain/tests/test_keychain_tracker.rs b/crates/chain/tests/test_keychain_tracker.rs index 1c5e0795..b4e51d85 100644 --- a/crates/chain/tests/test_keychain_tracker.rs +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -9,7 +9,7 @@ use bdk_chain::{ bitcoin::{secp256k1::Secp256k1, OutPoint, PackedLockTime, Transaction, TxOut}, Descriptor, }, - tx_graph::GraphedTx, + tx_graph::TxInGraph, BlockId, ConfirmationTime, TxHeight, }; use bitcoin::{BlockHash, TxIn}; @@ -45,7 +45,7 @@ fn test_insert_tx() { .collect::>(), vec![( &ConfirmationTime::Unconfirmed, - GraphedTx::from_tx(&tx, &BTreeSet::new()) + TxInGraph::from_tx(&tx, &BTreeSet::new()) )] ); diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 2550d556..107e106d 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,7 +2,7 @@ mod common; use bdk_chain::{ collections::*, - tx_graph::{Additions, GraphedTx, TxGraph}, + tx_graph::{Additions, TxGraph, TxInGraph}, BlockId, }; use bitcoin::{ @@ -157,7 +157,7 @@ fn insert_tx_can_retrieve_full_tx_from_graph() { let _ = graph.insert_tx(tx.clone()); assert_eq!( graph.get_tx(tx.txid()), - Some(GraphedTx::from_tx(&tx, &BTreeSet::new())) + Some(TxInGraph::from_tx(&tx, &BTreeSet::new())) ); } From 784cd34e3db727659dbb26c428ed9096927286c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 27 Mar 2023 13:59:51 +0800 Subject: [PATCH 04/48] [bdk_chain_redesign] List chain data methods can be try/non-try Methods that list chain data have try and non-try versions. Both of these versions now return an `Iterator`. * Try versions return `Iterator`. * Non-try versions require the `ChainOracle` implementation to be `ChainOracle`. --- crates/chain/src/tx_graph.rs | 130 +++++++++++++++++++++++------------ 1 file changed, 87 insertions(+), 43 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 3181ed2a..63b75324 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -61,7 +61,10 @@ use crate::{ }; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; -use core::ops::{Deref, RangeInclusive}; +use core::{ + convert::Infallible, + ops::{Deref, RangeInclusive}, +}; /// A graph of transactions and spends. /// @@ -492,7 +495,7 @@ impl TxGraph { /// Determines whether a transaction of `txid` is in the best chain. /// /// TODO: Also return conflicting tx list, ordered by last_seen. - pub fn get_position_in_chain( + pub fn try_get_chain_position( &self, chain: C, txid: Txid, @@ -544,7 +547,15 @@ impl TxGraph { } } - pub fn get_spend_in_chain( + pub fn get_chain_position(&self, chain: C, txid: Txid) -> Option> + where + C: ChainOracle, + { + self.try_get_chain_position(chain, txid) + .expect("error is infallible") + } + + pub fn try_get_spend_in_chain( &self, chain: C, outpoint: OutPoint, @@ -552,12 +563,15 @@ impl TxGraph { where C: ChainOracle, { - if self.get_position_in_chain(&chain, outpoint.txid)?.is_none() { + if self + .try_get_chain_position(&chain, outpoint.txid)? + .is_none() + { return Ok(None); } if let Some(spends) = self.spends.get(&outpoint) { for &txid in spends { - if let Some(observed_at) = self.get_position_in_chain(&chain, txid)? { + if let Some(observed_at) = self.try_get_chain_position(&chain, txid)? { return Ok(Some((observed_at, txid))); } } @@ -565,20 +579,12 @@ impl TxGraph { Ok(None) } - pub fn transactions_in_chain( - &self, - chain: C, - ) -> Result>, C::Error> + pub fn get_chain_spend(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedIn<&A>, Txid)> where - C: ChainOracle, + C: ChainOracle, { - self.full_transactions() - .filter_map(|tx| { - self.get_position_in_chain(&chain, tx.txid) - .map(|v| v.map(|observed_in| TxInChain { observed_in, tx })) - .transpose() - }) - .collect() + self.try_get_spend_in_chain(chain, outpoint) + .expect("error is infallible") } } @@ -802,41 +808,56 @@ impl IndexedTxGraph { self.graph.relevant_heights() } - pub fn txs_in_chain( - &self, + pub fn try_list_chain_txs<'a, C>( + &'a self, chain: C, - ) -> Result>, C::Error> + ) -> impl Iterator, C::Error>> where - C: ChainOracle, + C: ChainOracle + 'a, { - let mut tx_set = self.graph.transactions_in_chain(chain)?; - tx_set.retain(|tx| self.index.is_tx_relevant(&tx.tx)); - Ok(tx_set) + self.graph + .full_transactions() + .filter(|tx| self.index.is_tx_relevant(tx)) + .filter_map(move |tx| { + self.graph + .try_get_chain_position(&chain, tx.txid) + .map(|v| v.map(|observed_in| TxInChain { observed_in, tx })) + .transpose() + }) } - pub fn txouts_in_chain( - &self, + pub fn list_chain_txs<'a, C>( + &'a self, chain: C, - ) -> Result>, C::Error> + ) -> impl Iterator> where - C: ChainOracle, + C: ChainOracle + 'a, + { + self.try_list_chain_txs(chain) + .map(|r| r.expect("error is infallible")) + } + + pub fn try_list_chain_txouts<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator, C::Error>> + where + C: ChainOracle + 'a, ObservedIn: ChainPosition, { - self.index - .relevant_txouts() - .iter() - .filter_map(|(op, (spk_i, txout))| -> Option> { + self.index.relevant_txouts().iter().filter_map( + move |(op, (spk_i, txout))| -> Option> { let graph_tx = self.graph.get_tx(op.txid)?; let is_on_coinbase = graph_tx.is_coin_base(); - let chain_position = match self.graph.get_position_in_chain(&chain, op.txid) { + let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) { Ok(Some(observed_at)) => observed_at, Ok(None) => return None, Err(err) => return Some(Err(err)), }; - let spent_by = match self.graph.get_spend_in_chain(&chain, *op) { + let spent_by = match self.graph.try_get_spend_in_chain(&chain, *op) { Ok(spent_by) => spent_by, Err(err) => return Some(Err(err)), }; @@ -855,22 +876,45 @@ impl IndexedTxGraph { }; Some(Ok(txout_in_chain)) - }) - .collect() + }, + ) + } + + pub fn list_chain_txouts<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator> + where + C: ChainOracle + 'a, + ObservedIn: ChainPosition, + { + self.try_list_chain_txouts(chain) + .map(|r| r.expect("error in infallible")) } /// Return relevant unspents. - pub fn utxos_in_chain( - &self, + pub fn try_list_chain_utxos<'a, C>( + &'a self, chain: C, - ) -> Result>, C::Error> + ) -> impl Iterator, C::Error>> where - C: ChainOracle, + C: ChainOracle + 'a, ObservedIn: ChainPosition, { - let mut txouts = self.txouts_in_chain(chain)?; - txouts.retain(|txo| txo.txout.spent_by.is_none()); - Ok(txouts) + self.try_list_chain_txouts(chain) + .filter(|r| !matches!(r, Ok(txo) if txo.txout.spent_by.is_none())) + } + + pub fn list_chain_utxos<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator> + where + C: ChainOracle + 'a, + ObservedIn: ChainPosition, + { + self.try_list_chain_utxos(chain) + .map(|r| r.expect("error is infallible")) } } From 6cbb18d409d84ea0c399d9b3ecb0cdb49cc0b32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 27 Mar 2023 14:21:10 +0800 Subject: [PATCH 05/48] [bdk_chain_redesign] MOVE: `IndexedTxGraph` into submodule --- crates/chain/src/indexed_tx_graph.rs | 248 +++++++++++++++++++++++++++ crates/chain/src/lib.rs | 1 + crates/chain/src/tx_graph.rs | 243 +------------------------- 3 files changed, 250 insertions(+), 242 deletions(-) create mode 100644 crates/chain/src/indexed_tx_graph.rs diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs new file mode 100644 index 00000000..b0d547c7 --- /dev/null +++ b/crates/chain/src/indexed_tx_graph.rs @@ -0,0 +1,248 @@ +use core::convert::Infallible; + +use alloc::collections::BTreeSet; +use bitcoin::{OutPoint, Transaction, TxOut}; + +use crate::{ + sparse_chain::ChainPosition, + tx_graph::{Additions, TxGraph, TxInGraph}, + BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex, TxIndexAdditions, +}; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxInChain<'a, T, A> { + pub observed_in: ObservedIn<&'a A>, + pub tx: TxInGraph<'a, T, A>, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct TxOutInChain<'a, I, A> { + pub spk_index: &'a I, + pub txout: FullTxOut>, +} + +pub struct IndexedAdditions { + pub graph_additions: Additions, + pub index_delta: D, +} + +impl Default for IndexedAdditions { + fn default() -> Self { + Self { + graph_additions: Default::default(), + index_delta: Default::default(), + } + } +} + +impl TxIndexAdditions for IndexedAdditions { + fn append_additions(&mut self, other: Self) { + let Self { + graph_additions, + index_delta, + } = other; + self.graph_additions.append(graph_additions); + self.index_delta.append_additions(index_delta); + } +} + +pub struct IndexedTxGraph { + graph: TxGraph, + index: I, +} + +impl Default for IndexedTxGraph { + fn default() -> Self { + Self { + graph: Default::default(), + index: Default::default(), + } + } +} + +impl IndexedTxGraph { + /// Get a reference of the internal transaction graph. + pub fn graph(&self) -> &TxGraph { + &self.graph + } + + /// Get a reference of the internal transaction index. + pub fn index(&self) -> &I { + &self.index + } + + /// Insert a `txout` that exists in `outpoint` with the given `observation`. + pub fn insert_txout( + &mut self, + outpoint: OutPoint, + txout: &TxOut, + observation: ObservedIn, + ) -> IndexedAdditions { + IndexedAdditions { + graph_additions: { + let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); + graph_additions.append(match observation { + ObservedIn::Block(anchor) => self.graph.insert_anchor(outpoint.txid, anchor), + ObservedIn::Mempool(seen_at) => { + self.graph.insert_seen_at(outpoint.txid, seen_at) + } + }); + graph_additions + }, + index_delta: ::index_txout(&mut self.index, outpoint, txout), + } + } + + pub fn insert_tx( + &mut self, + tx: &Transaction, + observation: ObservedIn, + ) -> IndexedAdditions { + let txid = tx.txid(); + IndexedAdditions { + graph_additions: { + let mut graph_additions = self.graph.insert_tx(tx.clone()); + graph_additions.append(match observation { + ObservedIn::Block(anchor) => self.graph.insert_anchor(txid, anchor), + ObservedIn::Mempool(seen_at) => self.graph.insert_seen_at(txid, seen_at), + }); + graph_additions + }, + index_delta: ::index_tx(&mut self.index, tx), + } + } + + pub fn filter_and_insert_txs<'t, T>( + &mut self, + txs: T, + observation: ObservedIn, + ) -> IndexedAdditions + where + T: Iterator, + { + txs.filter_map(|tx| { + if self.index.is_tx_relevant(tx) { + Some(self.insert_tx(tx, observation.clone())) + } else { + None + } + }) + .fold(IndexedAdditions::default(), |mut acc, other| { + acc.append_additions(other); + acc + }) + } + + pub fn relevant_heights(&self) -> BTreeSet { + self.graph.relevant_heights() + } + + pub fn try_list_chain_txs<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator, C::Error>> + where + C: ChainOracle + 'a, + { + self.graph + .full_transactions() + .filter(|tx| self.index.is_tx_relevant(tx)) + .filter_map(move |tx| { + self.graph + .try_get_chain_position(&chain, tx.txid) + .map(|v| v.map(|observed_in| TxInChain { observed_in, tx })) + .transpose() + }) + } + + pub fn list_chain_txs<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator> + where + C: ChainOracle + 'a, + { + self.try_list_chain_txs(chain) + .map(|r| r.expect("error is infallible")) + } + + pub fn try_list_chain_txouts<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator, C::Error>> + where + C: ChainOracle + 'a, + ObservedIn: ChainPosition, + { + self.index.relevant_txouts().iter().filter_map( + move |(op, (spk_i, txout))| -> Option> { + let graph_tx = self.graph.get_tx(op.txid)?; + + let is_on_coinbase = graph_tx.is_coin_base(); + + let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) { + Ok(Some(observed_at)) => observed_at, + Ok(None) => return None, + Err(err) => return Some(Err(err)), + }; + + let spent_by = match self.graph.try_get_spend_in_chain(&chain, *op) { + Ok(spent_by) => spent_by, + Err(err) => return Some(Err(err)), + }; + + let full_txout = FullTxOut { + outpoint: *op, + txout: txout.clone(), + chain_position, + spent_by, + is_on_coinbase, + }; + + let txout_in_chain = TxOutInChain { + spk_index: spk_i, + txout: full_txout, + }; + + Some(Ok(txout_in_chain)) + }, + ) + } + + pub fn list_chain_txouts<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator> + where + C: ChainOracle + 'a, + ObservedIn: ChainPosition, + { + self.try_list_chain_txouts(chain) + .map(|r| r.expect("error in infallible")) + } + + /// Return relevant unspents. + pub fn try_list_chain_utxos<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator, C::Error>> + where + C: ChainOracle + 'a, + ObservedIn: ChainPosition, + { + self.try_list_chain_txouts(chain) + .filter(|r| !matches!(r, Ok(txo) if txo.txout.spent_by.is_none())) + } + + pub fn list_chain_utxos<'a, C>( + &'a self, + chain: C, + ) -> impl Iterator> + where + C: ChainOracle + 'a, + ObservedIn: ChainPosition, + { + self.try_list_chain_utxos(chain) + .map(|r| r.expect("error is infallible")) + } +} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 4e49e34e..52844097 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -24,6 +24,7 @@ mod spk_txout_index; pub use spk_txout_index::*; mod chain_data; pub use chain_data::*; +pub mod indexed_tx_graph; pub mod keychain; pub mod sparse_chain; mod tx_data_traits; diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 63b75324..ddeb5e13 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,10 +55,7 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{ - collections::*, sparse_chain::ChainPosition, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, - FullTxOut, ObservedIn, TxIndex, TxIndexAdditions, -}; +use crate::{collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, ObservedIn}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::{ @@ -129,18 +126,6 @@ impl<'a, A> TxInGraph<'a, Transaction, A> { } } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct TxInChain<'a, T, A> { - pub observed_in: ObservedIn<&'a A>, - pub tx: TxInGraph<'a, T, A>, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct TxOutInChain<'a, I, A> { - pub spk_index: &'a I, - pub txout: FullTxOut>, -} - /// 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 @@ -692,232 +677,6 @@ impl TxGraph { } } -pub struct IndexedAdditions { - pub graph_additions: Additions, - pub index_delta: D, -} - -impl Default for IndexedAdditions { - fn default() -> Self { - Self { - graph_additions: Default::default(), - index_delta: Default::default(), - } - } -} - -impl TxIndexAdditions for IndexedAdditions { - fn append_additions(&mut self, other: Self) { - let Self { - graph_additions, - index_delta, - } = other; - self.graph_additions.append(graph_additions); - self.index_delta.append_additions(index_delta); - } -} - -pub struct IndexedTxGraph { - graph: TxGraph, - index: I, -} - -impl Default for IndexedTxGraph { - fn default() -> Self { - Self { - graph: Default::default(), - index: Default::default(), - } - } -} - -impl IndexedTxGraph { - /// Get a reference of the internal transaction graph. - pub fn graph(&self) -> &TxGraph { - &self.graph - } - - /// Get a reference of the internal transaction index. - pub fn index(&self) -> &I { - &self.index - } - - /// Insert a `txout` that exists in `outpoint` with the given `observation`. - pub fn insert_txout( - &mut self, - outpoint: OutPoint, - txout: &TxOut, - observation: ObservedIn, - ) -> IndexedAdditions { - IndexedAdditions { - graph_additions: { - let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); - graph_additions.append(match observation { - ObservedIn::Block(anchor) => self.graph.insert_anchor(outpoint.txid, anchor), - ObservedIn::Mempool(seen_at) => { - self.graph.insert_seen_at(outpoint.txid, seen_at) - } - }); - graph_additions - }, - index_delta: ::index_txout(&mut self.index, outpoint, txout), - } - } - - pub fn insert_tx( - &mut self, - tx: &Transaction, - observation: ObservedIn, - ) -> IndexedAdditions { - let txid = tx.txid(); - IndexedAdditions { - graph_additions: { - let mut graph_additions = self.graph.insert_tx(tx.clone()); - graph_additions.append(match observation { - ObservedIn::Block(anchor) => self.graph.insert_anchor(txid, anchor), - ObservedIn::Mempool(seen_at) => self.graph.insert_seen_at(txid, seen_at), - }); - graph_additions - }, - index_delta: ::index_tx(&mut self.index, tx), - } - } - - pub fn filter_and_insert_txs<'t, T>( - &mut self, - txs: T, - observation: ObservedIn, - ) -> IndexedAdditions - where - T: Iterator, - { - txs.filter_map(|tx| { - if self.index.is_tx_relevant(tx) { - Some(self.insert_tx(tx, observation.clone())) - } else { - None - } - }) - .fold(IndexedAdditions::default(), |mut acc, other| { - acc.append_additions(other); - acc - }) - } - - pub fn relevant_heights(&self) -> BTreeSet { - self.graph.relevant_heights() - } - - pub fn try_list_chain_txs<'a, C>( - &'a self, - chain: C, - ) -> impl Iterator, C::Error>> - where - C: ChainOracle + 'a, - { - self.graph - .full_transactions() - .filter(|tx| self.index.is_tx_relevant(tx)) - .filter_map(move |tx| { - self.graph - .try_get_chain_position(&chain, tx.txid) - .map(|v| v.map(|observed_in| TxInChain { observed_in, tx })) - .transpose() - }) - } - - pub fn list_chain_txs<'a, C>( - &'a self, - chain: C, - ) -> impl Iterator> - where - C: ChainOracle + 'a, - { - self.try_list_chain_txs(chain) - .map(|r| r.expect("error is infallible")) - } - - pub fn try_list_chain_txouts<'a, C>( - &'a self, - chain: C, - ) -> impl Iterator, C::Error>> - where - C: ChainOracle + 'a, - ObservedIn: ChainPosition, - { - self.index.relevant_txouts().iter().filter_map( - move |(op, (spk_i, txout))| -> Option> { - let graph_tx = self.graph.get_tx(op.txid)?; - - let is_on_coinbase = graph_tx.is_coin_base(); - - let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) { - Ok(Some(observed_at)) => observed_at, - Ok(None) => return None, - Err(err) => return Some(Err(err)), - }; - - let spent_by = match self.graph.try_get_spend_in_chain(&chain, *op) { - Ok(spent_by) => spent_by, - Err(err) => return Some(Err(err)), - }; - - let full_txout = FullTxOut { - outpoint: *op, - txout: txout.clone(), - chain_position, - spent_by, - is_on_coinbase, - }; - - let txout_in_chain = TxOutInChain { - spk_index: spk_i, - txout: full_txout, - }; - - Some(Ok(txout_in_chain)) - }, - ) - } - - pub fn list_chain_txouts<'a, C>( - &'a self, - chain: C, - ) -> impl Iterator> - where - C: ChainOracle + 'a, - ObservedIn: ChainPosition, - { - self.try_list_chain_txouts(chain) - .map(|r| r.expect("error in infallible")) - } - - /// Return relevant unspents. - pub fn try_list_chain_utxos<'a, C>( - &'a self, - chain: C, - ) -> impl Iterator, C::Error>> - where - C: ChainOracle + 'a, - ObservedIn: ChainPosition, - { - self.try_list_chain_txouts(chain) - .filter(|r| !matches!(r, Ok(txo) if txo.txout.spent_by.is_none())) - } - - pub fn list_chain_utxos<'a, C>( - &'a self, - chain: C, - ) -> impl Iterator> - where - C: ChainOracle + 'a, - ObservedIn: ChainPosition, - { - self.try_list_chain_utxos(chain) - .map(|r| r.expect("error is infallible")) - } -} - /// A structure that represents changes to a [`TxGraph`]. /// /// It is named "additions" because [`TxGraph`] is monotone, so transactions can only be added and From d0a2aa83befce5dda26f8b3ae05449f6967df25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 27 Mar 2023 15:36:37 +0800 Subject: [PATCH 06/48] [bdk_chain_redesign] Add `apply_additions` to `IndexedTxGraph` * Get mutable index from `IndexedChainGraph`. * Also add `apply_additions` method to `TxIndex` trait. --- crates/chain/src/indexed_tx_graph.rs | 15 +++++++++++++++ crates/chain/src/keychain/txout_index.rs | 4 ++++ crates/chain/src/spk_txout_index.rs | 4 ++++ crates/chain/src/tx_data_traits.rs | 3 +++ 4 files changed, 26 insertions(+) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index b0d547c7..5071fb2c 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -71,6 +71,21 @@ impl IndexedTxGraph { &self.index } + /// Get a mutable reference to the internal transaction index. + pub fn mut_index(&mut self) -> &mut I { + &mut self.index + } + + /// Applies the [`IndexedAdditions`] to the [`IndexedTxGraph`]. + pub fn apply_additions(&mut self, additions: IndexedAdditions) { + let IndexedAdditions { + graph_additions, + index_delta, + } = additions; + self.graph.apply_additions(graph_additions); + self.index.apply_additions(index_delta); + } + /// Insert a `txout` that exists in `outpoint` with the given `observation`. pub fn insert_txout( &mut self, diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index 176254b4..d19aada7 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -101,6 +101,10 @@ impl TxIndex for KeychainTxOutIndex { self.scan(tx) } + fn apply_additions(&mut self, additions: Self::Additions) { + self.apply_additions(additions) + } + fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { self.is_relevant(tx) } diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 3d2f783e..3d1af948 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -68,6 +68,10 @@ impl TxIndex for SpkTxOutIndex { self.scan(tx) } + fn apply_additions(&mut self, _additions: Self::Additions) { + // This applies nothing. + } + fn is_tx_relevant(&self, tx: &Transaction) -> bool { self.is_relevant(tx) } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index f412f452..2ffb9a60 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -119,6 +119,9 @@ pub trait TxIndex { .unwrap_or_default() } + /// Apply additions to itself. + fn apply_additions(&mut self, additions: Self::Additions); + /// A transaction is relevant if it contains a txout with a script_pubkey that we own, or if it /// spends an already-indexed outpoint that we have previously indexed. fn is_tx_relevant(&self, tx: &Transaction) -> bool; From db7883d813e97229340c32a8fa82a9a13bac7361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 27 Mar 2023 19:55:57 +0800 Subject: [PATCH 07/48] [bdk_chain_redesign] Add balance methods to `IndexedTxGraph` --- crates/chain/src/chain_data.rs | 21 +++++++ crates/chain/src/indexed_tx_graph.rs | 83 ++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 43eb64f6..df5a5e9c 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -14,6 +14,15 @@ pub enum ObservedIn { Mempool(u64), } +impl ObservedIn<&A> { + pub fn into_owned(self) -> ObservedIn { + match self { + ObservedIn::Block(a) => ObservedIn::Block(a.clone()), + ObservedIn::Mempool(last_seen) => ObservedIn::Mempool(last_seen), + } + } +} + impl ChainPosition for ObservedIn { fn height(&self) -> TxHeight { match self { @@ -259,4 +268,16 @@ impl FullTxOut { } } +impl FullTxOut> { + pub fn into_owned(self) -> FullTxOut> { + FullTxOut { + outpoint: self.outpoint, + txout: self.txout, + chain_position: self.chain_position.into_owned(), + spent_by: self.spent_by.map(|(o, txid)| (o.into_owned(), txid)), + is_on_coinbase: self.is_on_coinbase, + } + } +} + // TODO: make test diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 5071fb2c..5361437e 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -4,6 +4,7 @@ use alloc::collections::BTreeSet; use bitcoin::{OutPoint, Transaction, TxOut}; use crate::{ + keychain::Balance, sparse_chain::ChainPosition, tx_graph::{Additions, TxGraph, TxInGraph}, BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex, TxIndexAdditions, @@ -260,4 +261,86 @@ impl IndexedTxGraph { self.try_list_chain_utxos(chain) .map(|r| r.expect("error is infallible")) } + + pub fn try_balance( + &self, + chain: C, + tip: u32, + mut should_trust: F, + ) -> Result + where + C: ChainOracle, + ObservedIn: ChainPosition + Clone, + F: FnMut(&I::SpkIndex) -> bool, + { + let mut immature = 0; + let mut trusted_pending = 0; + let mut untrusted_pending = 0; + let mut confirmed = 0; + + for res in self.try_list_chain_txouts(&chain) { + let TxOutInChain { spk_index, txout } = res?; + let txout = txout.into_owned(); + + match &txout.chain_position { + ObservedIn::Block(_) => { + if txout.is_on_coinbase { + if txout.is_mature(tip) { + confirmed += txout.txout.value; + } else { + immature += txout.txout.value; + } + } + } + ObservedIn::Mempool(_) => { + if should_trust(spk_index) { + trusted_pending += txout.txout.value; + } else { + untrusted_pending += txout.txout.value; + } + } + } + } + + Ok(Balance { + immature, + trusted_pending, + untrusted_pending, + confirmed, + }) + } + + pub fn balance(&self, chain: C, tip: u32, should_trust: F) -> Balance + where + C: ChainOracle, + ObservedIn: ChainPosition + Clone, + F: FnMut(&I::SpkIndex) -> bool, + { + self.try_balance(chain, tip, should_trust) + .expect("error is infallible") + } + + pub fn try_balance_at(&self, chain: C, height: u32) -> Result + where + C: ChainOracle, + ObservedIn: ChainPosition + Clone, + { + let mut sum = 0; + for res in self.try_list_chain_txouts(chain) { + let txo = res?.txout.into_owned(); + if txo.is_spendable_at(height) { + sum += txo.txout.value; + } + } + Ok(sum) + } + + pub fn balance_at(&self, chain: C, height: u32) -> u64 + where + C: ChainOracle, + ObservedIn: ChainPosition + Clone, + { + self.try_balance_at(chain, height) + .expect("error is infallible") + } } From 313965d8c84f5de43cafa58c7bd9250aea93b22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 27 Mar 2023 20:56:42 +0800 Subject: [PATCH 08/48] [bdk_chain_redesign] `mut_index` should be `index_mut` --- crates/chain/src/indexed_tx_graph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 5361437e..91ecd571 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -73,7 +73,7 @@ impl IndexedTxGraph { } /// Get a mutable reference to the internal transaction index. - pub fn mut_index(&mut self) -> &mut I { + pub fn index_mut(&mut self) -> &mut I { &mut self.index } From e902c10295ba430bf0522b92ffab68cd60bd1666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 27 Mar 2023 21:51:11 +0800 Subject: [PATCH 09/48] [bdk_chain_redesign] Fix `apply_additions` logic for `IndexedTxGraph`. --- crates/chain/src/indexed_tx_graph.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 91ecd571..4245e57d 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -22,6 +22,7 @@ pub struct TxOutInChain<'a, I, A> { pub txout: FullTxOut>, } +#[must_use] pub struct IndexedAdditions { pub graph_additions: Additions, pub index_delta: D, @@ -83,8 +84,17 @@ impl IndexedTxGraph { graph_additions, index_delta, } = additions; - self.graph.apply_additions(graph_additions); + self.index.apply_additions(index_delta); + + for tx in &graph_additions.tx { + self.index.index_tx(tx); + } + for (&outpoint, txout) in &graph_additions.txout { + self.index.index_txout(outpoint, txout); + } + + self.graph.apply_additions(graph_additions); } /// Insert a `txout` that exists in `outpoint` with the given `observation`. From 236c50fa7bace29a0373dd16416ecebbb6dc1ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 27 Mar 2023 22:42:39 +0800 Subject: [PATCH 10/48] [bdk_chain_redesign] `IndexedTxGraph` keeps track of the last synced height This is important as a `ChainOracle` implementation is updated separately to an `IndexedTxGraph`. --- crates/chain/src/indexed_tx_graph.rs | 59 ++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 4245e57d..08926f56 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,6 +1,5 @@ use core::convert::Infallible; -use alloc::collections::BTreeSet; use bitcoin::{OutPoint, Transaction, TxOut}; use crate::{ @@ -26,6 +25,7 @@ pub struct TxOutInChain<'a, I, A> { pub struct IndexedAdditions { pub graph_additions: Additions, pub index_delta: D, + pub last_height: Option, } impl Default for IndexedAdditions { @@ -33,6 +33,7 @@ impl Default for IndexedAdditions { Self { graph_additions: Default::default(), index_delta: Default::default(), + last_height: None, } } } @@ -42,15 +43,22 @@ impl TxIndexAdditions for IndexedAdditions< let Self { graph_additions, index_delta, + last_height, } = other; self.graph_additions.append(graph_additions); self.index_delta.append_additions(index_delta); + if self.last_height < last_height { + let last_height = + last_height.expect("must exist as it is larger than self.last_height"); + self.last_height.replace(last_height); + } } } pub struct IndexedTxGraph { graph: TxGraph, index: I, + last_height: u32, } impl Default for IndexedTxGraph { @@ -58,6 +66,7 @@ impl Default for IndexedTxGraph { Self { graph: Default::default(), index: Default::default(), + last_height: u32::MIN, } } } @@ -83,6 +92,7 @@ impl IndexedTxGraph { let IndexedAdditions { graph_additions, index_delta, + last_height, } = additions; self.index.apply_additions(index_delta); @@ -95,6 +105,23 @@ impl IndexedTxGraph { } self.graph.apply_additions(graph_additions); + + if let Some(height) = last_height { + self.last_height = height; + } + } + + /// Insert a block height that the chain source has scanned up to. + pub fn insert_height(&mut self, tip: u32) -> IndexedAdditions { + if self.last_height < tip { + self.last_height = tip; + IndexedAdditions { + last_height: Some(tip), + ..Default::default() + } + } else { + IndexedAdditions::default() + } } /// Insert a `txout` that exists in `outpoint` with the given `observation`. @@ -104,7 +131,12 @@ impl IndexedTxGraph { txout: &TxOut, observation: ObservedIn, ) -> IndexedAdditions { - IndexedAdditions { + let mut additions = match &observation { + ObservedIn::Block(anchor) => self.insert_height(anchor.anchor_block().height), + ObservedIn::Mempool(_) => IndexedAdditions::default(), + }; + + additions.append_additions(IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); graph_additions.append(match observation { @@ -116,7 +148,10 @@ impl IndexedTxGraph { graph_additions }, index_delta: ::index_txout(&mut self.index, outpoint, txout), - } + last_height: None, + }); + + additions } pub fn insert_tx( @@ -125,7 +160,13 @@ impl IndexedTxGraph { observation: ObservedIn, ) -> IndexedAdditions { let txid = tx.txid(); - IndexedAdditions { + + let mut additions = match &observation { + ObservedIn::Block(anchor) => self.insert_height(anchor.anchor_block().height), + ObservedIn::Mempool(_) => IndexedAdditions::default(), + }; + + additions.append_additions(IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_tx(tx.clone()); graph_additions.append(match observation { @@ -135,7 +176,10 @@ impl IndexedTxGraph { graph_additions }, index_delta: ::index_tx(&mut self.index, tx), - } + last_height: None, + }); + + additions } pub fn filter_and_insert_txs<'t, T>( @@ -159,8 +203,9 @@ impl IndexedTxGraph { }) } - pub fn relevant_heights(&self) -> BTreeSet { - self.graph.relevant_heights() + /// Get the last block height that we are synced up to. + pub fn last_height(&self) -> u32 { + self.last_height } pub fn try_list_chain_txs<'a, C>( From 3440a057110fbbcc653f2f8c7d58175472299bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Tue, 28 Mar 2023 10:58:23 +0800 Subject: [PATCH 11/48] [bdk_chain_redesign] Add docs --- crates/chain/src/indexed_tx_graph.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 08926f56..b2b206cf 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -9,22 +9,33 @@ use crate::{ BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex, TxIndexAdditions, }; +/// An outwards-facing view of a transaction that is part of the *best chain*'s history. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct TxInChain<'a, T, A> { + /// Where the transaction is observed (in a block or in mempool). pub observed_in: ObservedIn<&'a A>, + /// The transaction with anchors and last seen timestamp. pub tx: TxInGraph<'a, T, A>, } +/// An outwards-facing view of a relevant txout that is part of the *best chain*'s history. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct TxOutInChain<'a, I, A> { + /// The custom index of the txout's script pubkey. pub spk_index: &'a I, + /// The full txout. pub txout: FullTxOut>, } +/// A structure that represents changes to an [`IndexedTxGraph`]. +#[derive(Clone, Debug, PartialEq)] #[must_use] pub struct IndexedAdditions { + /// [`TxGraph`] additions. pub graph_additions: Additions, + /// [`TxIndex`] additions. pub index_delta: D, + /// Last block height witnessed (if any). pub last_height: Option, } From 34d0277e44fd054c8d463dfa756d8531ccda3ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Tue, 28 Mar 2023 14:58:59 +0800 Subject: [PATCH 12/48] [bdk_chain_redesign] Rm anchor type param for structs that don't use it --- crates/bdk/src/wallet/mod.rs | 28 +++--- crates/bdk/src/wallet/tx_builder.rs | 3 +- crates/chain/src/chain_graph.rs | 87 ++++++++----------- crates/chain/src/keychain.rs | 40 ++++----- crates/chain/src/keychain/persist.rs | 24 ++--- crates/chain/src/keychain/tracker.rs | 41 +++++---- crates/chain/tests/test_chain_graph.rs | 32 ++++--- crates/chain/tests/test_keychain_tracker.rs | 6 +- 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 +++++----- 16 files changed, 174 insertions(+), 201 deletions(-) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 194c6c90..6e4adf74 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -85,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`. @@ -197,7 +197,7 @@ impl Wallet { network: Network, ) -> Result> where - D: persist::PersistBackend, + D: persist::PersistBackend, { let secp = Secp256k1::new(); @@ -259,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) } @@ -273,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; @@ -620,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 @@ -1694,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); @@ -1706,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() } @@ -1724,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() } } @@ -1735,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 150d33aa..dbd4811c 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -39,7 +39,6 @@ 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; @@ -527,7 +526,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_graph.rs b/crates/chain/src/chain_graph.rs index fcb98043..3c841965 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -3,7 +3,7 @@ use crate::{ collections::HashSet, sparse_chain::{self, ChainPosition, SparseChain}, tx_graph::{self, TxGraph, TxInGraph}, - BlockAnchor, BlockId, ForEachTxOut, FullTxOut, TxHeight, + 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,39 +39,38 @@ 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`. @@ -82,7 +81,7 @@ 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(graphed_tx) = graph.get_tx(*txid) { @@ -129,7 +128,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(); @@ -188,7 +187,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() @@ -200,9 +199,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()); @@ -213,7 +212,7 @@ 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, TxInGraph<'_, Transaction, A>)> { + pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, TxInGraph<'_, Transaction, BlockId>)> { let position = self.chain.tx_position(txid)?; let graphed_tx = self.graph.get_tx(txid).expect("must exist"); Some((position, graphed_tx)) @@ -228,7 +227,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), @@ -241,18 +240,14 @@ 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), @@ -263,7 +258,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 @@ -276,7 +271,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 { @@ -292,7 +287,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) @@ -301,8 +296,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) @@ -337,10 +332,7 @@ 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 { @@ -416,17 +408,14 @@ 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) @@ -441,7 +430,7 @@ where /// in ascending order. pub fn transactions_in_chain( &self, - ) -> impl DoubleEndedIterator)> { + ) -> impl DoubleEndedIterator)> { self.chain .txids() .map(move |(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist"))) @@ -472,18 +461,18 @@ where serde( crate = "serde_crate", bound( - deserialize = "A: Ord + serde::Deserialize<'de>, P: serde::Deserialize<'de>", - serialize = "A: Ord + serde::Serialize, P: serde::Serialize" + deserialize = "P: serde::Deserialize<'de>", + 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() @@ -499,7 +488,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, { @@ -508,7 +497,7 @@ impl ChangeSet { } } -impl Default for ChangeSet { +impl

Default for ChangeSet

{ fn default() -> Self { Self { chain: Default::default(), @@ -523,7 +512,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 dd419db5..da2af6f2 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -105,14 +105,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(), @@ -121,8 +121,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(), @@ -140,20 +140,20 @@ impl From> for KeychainScan { serde( crate = "serde_crate", bound( - 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" + deserialize = "K: Ord + serde::Deserialize<'de>, P: serde::Deserialize<'de>", + serialize = "K: 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(), @@ -162,7 +162,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() @@ -173,7 +173,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, @@ -183,8 +183,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() @@ -192,7 +192,7 @@ impl From> for KeychainChangeSet } } -impl From> for KeychainChangeSet { +impl From> for KeychainChangeSet { fn from(additions: DerivationAdditions) -> Self { Self { derivation_indices: additions, @@ -201,13 +201,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) } @@ -293,12 +293,12 @@ mod test { rhs_di.insert(Keychain::Four, 4); let mut lhs = KeychainChangeSet { derivation_indices: DerivationAdditions(lhs_di), - chain_graph: chain_graph::ChangeSet::<(), TxHeight>::default(), + chain_graph: chain_graph::ChangeSet::::default(), }; let rhs = KeychainChangeSet { derivation_indices: DerivationAdditions(rhs_di), - chain_graph: chain_graph::ChangeSet::<(), TxHeight>::default(), + chain_graph: chain_graph::ChangeSet::::default(), }; lhs.append(rhs); diff --git a/crates/chain/src/keychain/persist.rs b/crates/chain/src/keychain/persist.rs index f0bc8d11..1a3ffab0 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 db4e8d89..fff5ee2b 100644 --- a/crates/chain/src/keychain/tracker.rs +++ b/crates/chain/src/keychain/tracker.rs @@ -17,16 +17,15 @@ 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. @@ -65,8 +64,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| { @@ -90,8 +89,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) @@ -101,7 +100,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, @@ -133,12 +132,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() } @@ -160,7 +159,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() @@ -177,7 +176,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) @@ -192,7 +191,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() @@ -210,7 +209,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) @@ -281,7 +280,7 @@ where } } -impl Default for KeychainTracker { +impl Default for KeychainTracker { fn default() -> Self { Self { txout_index: Default::default(), @@ -290,20 +289,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/tests/test_chain_graph.rs b/crates/chain/tests/test_chain_graph.rs index f7b39d2b..0514acc9 100644 --- a/crates/chain/tests/test_chain_graph.rs +++ b/crates/chain/tests/test_chain_graph.rs @@ -10,9 +10,7 @@ use bdk_chain::{ tx_graph::{self, TxGraph, TxInGraph}, BlockId, TxHeight, }; -use bitcoin::{ - BlockHash, OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness, -}; +use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness}; #[test] fn test_spent_by() { @@ -47,7 +45,7 @@ fn test_spent_by() { output: vec![], }; - let mut cg1 = ChainGraph::<(u32, BlockHash), _>::default(); + let mut cg1 = ChainGraph::default(); let _ = cg1 .insert_tx(tx1, TxHeight::Unconfirmed) .expect("should insert"); @@ -128,7 +126,7 @@ fn update_evicts_conflicting_tx() { cg }; - let changeset = ChangeSet::<(u32, BlockHash), TxHeight> { + let changeset = ChangeSet:: { chain: sparse_chain::ChangeSet { checkpoints: Default::default(), txids: [ @@ -137,7 +135,7 @@ fn update_evicts_conflicting_tx() { ] .into(), }, - graph: tx_graph::Additions::<(u32, BlockHash)> { + graph: tx_graph::Additions { tx: [tx_b2.clone()].into(), txout: [].into(), ..Default::default() @@ -154,7 +152,7 @@ fn update_evicts_conflicting_tx() { { let cg1 = { - let mut cg = ChainGraph::<(u32, BlockHash), _>::default(); + let mut cg = ChainGraph::default(); let _ = cg.insert_checkpoint(cp_a).expect("should insert cp"); let _ = cg.insert_checkpoint(cp_b).expect("should insert cp"); let _ = cg @@ -208,7 +206,7 @@ fn update_evicts_conflicting_tx() { cg }; - let changeset = ChangeSet::<(u32, BlockHash), TxHeight> { + let changeset = ChangeSet:: { chain: sparse_chain::ChangeSet { checkpoints: [(1, Some(h!("B'")))].into(), txids: [ @@ -217,7 +215,7 @@ fn update_evicts_conflicting_tx() { ] .into(), }, - graph: tx_graph::Additions::<(u32, BlockHash)> { + graph: tx_graph::Additions { tx: [tx_b2].into(), txout: [].into(), ..Default::default() @@ -256,7 +254,7 @@ fn chain_graph_new_missing() { (tx_b.txid(), TxHeight::Confirmed(0)) ] ); - let mut graph = TxGraph::<(u32, BlockHash)>::default(); + let mut graph = TxGraph::default(); let mut expected_missing = HashSet::new(); expected_missing.insert(tx_a.txid()); @@ -293,7 +291,7 @@ fn chain_graph_new_missing() { let new_graph = ChainGraph::new(update.clone(), graph.clone()).unwrap(); let expected_graph = { - let mut cg = ChainGraph::<(u32, BlockHash), TxHeight>::default(); + let mut cg = ChainGraph::::default(); let _ = cg .insert_checkpoint(update.latest_checkpoint().unwrap()) .unwrap(); @@ -348,7 +346,7 @@ fn chain_graph_new_conflicts() { ] ); - let graph = TxGraph::<(u32, BlockHash)>::new([tx_a, tx_b, tx_b2]); + let graph = TxGraph::new([tx_a, tx_b, tx_b2]); assert!(matches!( ChainGraph::new(chain, graph), @@ -358,7 +356,7 @@ fn chain_graph_new_conflicts() { #[test] fn test_get_tx_in_chain() { - let mut cg = ChainGraph::<(u32, BlockHash), _>::default(); + let mut cg = ChainGraph::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -383,7 +381,7 @@ fn test_get_tx_in_chain() { #[test] fn test_iterate_transactions() { - let mut cg = ChainGraph::::default(); + let mut cg = ChainGraph::default(); let txs = (0..3) .map(|i| Transaction { version: i, @@ -480,7 +478,7 @@ fn test_apply_changes_reintroduce_tx() { // block1, block2a, tx1, tx2a let mut cg = { - let mut cg = ChainGraph::<(u32, BlockHash), _>::default(); + let mut cg = ChainGraph::default(); let _ = cg.insert_checkpoint(block1).unwrap(); let _ = cg.insert_checkpoint(block2a).unwrap(); let _ = cg.insert_tx(tx1, TxHeight::Confirmed(1)).unwrap(); @@ -636,7 +634,7 @@ fn test_evict_descendants() { let txid_conflict = tx_conflict.txid(); let cg = { - let mut cg = ChainGraph::<(u32, BlockHash), TxHeight>::default(); + let mut cg = ChainGraph::::default(); let _ = cg.insert_checkpoint(block_1); let _ = cg.insert_checkpoint(block_2a); let _ = cg.insert_tx(tx_1, TxHeight::Confirmed(1)); @@ -648,7 +646,7 @@ fn test_evict_descendants() { }; let update = { - let mut cg = ChainGraph::<(u32, BlockHash), TxHeight>::default(); + let mut cg = ChainGraph::::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 b4e51d85..c3fee347 100644 --- a/crates/chain/tests/test_keychain_tracker.rs +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -12,11 +12,11 @@ use bdk_chain::{ tx_graph::TxInGraph, BlockId, ConfirmationTime, TxHeight, }; -use bitcoin::{BlockHash, TxIn}; +use bitcoin::TxIn; #[test] fn test_insert_tx() { - let mut tracker = KeychainTracker::<_, BlockId, _>::default(); + let mut tracker = KeychainTracker::default(); let secp = Secp256k1::new(); let (descriptor, _) = Descriptor::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap(); tracker.add_keychain((), descriptor.clone()); @@ -72,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/electrum/src/lib.rs b/crates/electrum/src/lib.rs index d062cfdc..bddbd8f2 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, - BlockAnchor, BlockId, ConfirmationTime, TxHeight, + BlockId, ConfirmationTime, TxHeight, }; pub use electrum_client; use electrum_client::{Client, ElectrumApi, Error}; @@ -243,14 +243,13 @@ 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>, - A: BlockAnchor, + CG: AsRef>, { Ok(KeychainScan { update: chain_graph diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 420f1197..266fd30b 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 d4a511ac..c22668a5 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 ba0dc21d..824e3ccc 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, BlockAnchor, + sparse_chain, }; use bincode::{DefaultOptions, Options}; use core::marker::PhantomData; @@ -23,21 +23,20 @@ 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, A, P)>, + changeset_type_params: core::marker::PhantomData<(K, 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`]. /// @@ -86,9 +85,7 @@ 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 _))?; @@ -107,7 +104,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()?; @@ -127,7 +124,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?) @@ -141,7 +138,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(()); @@ -291,7 +288,7 @@ mod test { use super::*; use bdk_chain::{ keychain::{DerivationAdditions, KeychainChangeSet}, - BlockId, TxHeight, + TxHeight, }; use std::{ io::{Read, Write}, @@ -335,7 +332,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), }; @@ -349,7 +346,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()) } @@ -373,9 +370,8 @@ 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 a9673be9..e3347419 100644 --- a/crates/file_store/src/lib.rs +++ b/crates/file_store/src/lib.rs @@ -3,16 +3,14 @@ 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; @@ -20,14 +18,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 08f29ceb..c8b9e068 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 04d121d2..cae5e960 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 e118cbf4..df42df1a 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}, - BlockAnchor, DescriptorExt, FullTxOut, + DescriptorExt, FullTxOut, }; use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; use bdk_file_store::KeychainStore; @@ -179,16 +179,15 @@ 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; @@ -242,9 +241,7 @@ where } } -pub fn run_balance_cmd( - tracker: &Mutex>, -) { +pub fn run_balance_cmd(tracker: &Mutex>) { let tracker = tracker.lock().unwrap(); let (confirmed, unconfirmed) = tracker @@ -261,9 +258,9 @@ pub fn run_balance_cmd( println!("unconfirmed: {}", unconfirmed); } -pub fn run_txo_cmd( +pub fn run_txo_cmd( txout_cmd: TxOutCmd, - tracker: &Mutex>, + tracker: &Mutex>, network: Network, ) { match txout_cmd { @@ -316,11 +313,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, @@ -529,20 +526,19 @@ 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 @@ -623,18 +619,17 @@ 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(); @@ -660,7 +655,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() { @@ -674,8 +669,8 @@ where Ok((args, keymap, Mutex::new(tracker), Mutex::new(db))) } -pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, A: BlockAnchor, P: ChainPosition>( - tracker: &'a KeychainTracker, +pub fn planned_utxos<'a, AK: bdk_tmp_plan::CanDerive + Clone, P: ChainPosition>( + tracker: &'a KeychainTracker, assets: &'a bdk_tmp_plan::Assets, ) -> impl Iterator, FullTxOut

)> + 'a { tracker From 468701a1295c90761749e1bb46cc201cc7f95613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 29 Mar 2023 22:45:01 +0800 Subject: [PATCH 13/48] [bdk_chain_redesign] Initial work on `LocalChain`. --- crates/chain/src/chain_data.rs | 1 + crates/chain/src/indexed_tx_graph.rs | 3 +- crates/chain/src/lib.rs | 1 + crates/chain/src/local_chain.rs | 140 +++++++++++++++++++++++++++ crates/chain/src/tx_data_traits.rs | 6 ++ 5 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 crates/chain/src/local_chain.rs diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index df5a5e9c..6c1c2c3a 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -11,6 +11,7 @@ pub enum ObservedIn { /// The chain data is seen in a block identified by `A`. Block(A), /// The chain data is seen in mempool at this given timestamp. + /// TODO: Call this `Unconfirmed`. Mempool(u64), } diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index b2b206cf..21852150 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -68,7 +68,7 @@ impl TxIndexAdditions for IndexedAdditions< pub struct IndexedTxGraph { graph: TxGraph, - index: I, + index: I, // [TODO] Make public last_height: u32, } @@ -219,6 +219,7 @@ impl IndexedTxGraph { self.last_height } + // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`. pub fn try_list_chain_txs<'a, C>( &'a self, chain: C, diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 52844097..9319d4ac 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -26,6 +26,7 @@ mod chain_data; pub use chain_data::*; pub mod indexed_tx_graph; pub mod keychain; +pub mod local_chain; pub mod sparse_chain; mod tx_data_traits; pub mod tx_graph; diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs new file mode 100644 index 00000000..5bcb524f --- /dev/null +++ b/crates/chain/src/local_chain.rs @@ -0,0 +1,140 @@ +use core::convert::Infallible; + +use alloc::{collections::BTreeMap, vec::Vec}; +use bitcoin::BlockHash; + +use crate::{BlockId, ChainOracle}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct LocalChain { + blocks: BTreeMap, +} + +// [TODO] We need a cache/snapshot thing for chain oracle. +// * Minimize calls to remotes. +// * Can we cache it forever? Should we drop stuff? +// * Assume anything deeper than (i.e. 10) blocks won't be reorged. +// * Is this a cache on txs or block? or both? +// [TODO] Parents of children are confirmed if children are confirmed. +impl ChainOracle for LocalChain { + type Error = Infallible; + + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { + Ok(self.blocks.get(&height).cloned()) + } +} + +impl AsRef> for LocalChain { + fn as_ref(&self) -> &BTreeMap { + &self.blocks + } +} + +impl From for BTreeMap { + fn from(value: LocalChain) -> Self { + value.blocks + } +} + +impl LocalChain { + pub fn tip(&self) -> Option { + self.blocks + .iter() + .last() + .map(|(&height, &hash)| BlockId { height, hash }) + } + + /// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights + /// are to be re-filled. + pub fn determine_changeset(&self, update: &U) -> Result + where + U: AsRef>, + { + let update = update.as_ref(); + let update_tip = match update.keys().last().cloned() { + Some(tip) => tip, + None => return Ok(ChangeSet::default()), + }; + + // this is the latest height where both the update and local chain has the same block hash + let agreement_height = update + .iter() + .rev() + .find(|&(u_height, u_hash)| self.blocks.get(u_height) == Some(u_hash)) + .map(|(&height, _)| height); + + // the lower bound of the range to invalidate + let invalidate_lb = match agreement_height { + Some(height) if height == update_tip => u32::MAX, + Some(height) => height + 1, + None => 0, + }; + + // the first block's height to invalidate in the local chain + let invalidate_from = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h); + + // the first block of height to invalidate (if any) should be represented in the update + if let Some(first_invalid) = invalidate_from { + if !update.contains_key(&first_invalid) { + return Err(UpdateError::NotConnected(first_invalid)); + } + } + + let invalidated_heights = invalidate_from + .into_iter() + .flat_map(|from_height| self.blocks.range(from_height..).map(|(h, _)| h)); + + // invalidated heights must all exist in the update + let mut missing_heights = Vec::::new(); + for invalidated_height in invalidated_heights { + if !update.contains_key(invalidated_height) { + missing_heights.push(*invalidated_height); + } + } + if !missing_heights.is_empty() { + return Err(UpdateError::MissingHeightsInUpdate(missing_heights)); + } + + let mut changeset = BTreeMap::::new(); + for (height, new_hash) in update { + let original_hash = self.blocks.get(height); + if Some(new_hash) != original_hash { + changeset.insert(*height, *new_hash); + } + } + Ok(ChangeSet(changeset)) + } +} + +#[derive(Debug, Default)] +pub struct ChangeSet(pub BTreeMap); + +/// Represents an update failure of [`LocalChain`].j +#[derive(Clone, Debug, PartialEq)] +pub enum UpdateError { + /// The update cannot be applied to the chain because the chain suffix it represents did not + /// connect to the existing chain. This error case contains the checkpoint height to include so + /// that the chains can connect. + NotConnected(u32), + /// If the update results in displacements of original blocks, the update should include all new + /// block hashes that have displaced the original block hashes. This error case contains the + /// heights of all missing block hashes in the update. + MissingHeightsInUpdate(Vec), +} + +impl core::fmt::Display for UpdateError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + UpdateError::NotConnected(heights) => write!( + f, + "the update cannot connect with the chain, try include blockhash at height {}", + heights + ), + UpdateError::MissingHeightsInUpdate(missing_heights) => write!( + f, + "block hashes of these heights must be included in the update to succeed: {:?}", + missing_heights + ), + } + } +} diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 2ffb9a60..485e3f70 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -58,6 +58,10 @@ impl BlockAnchor for (u32, BlockHash) { } /// Represents a service that tracks the best chain history. +/// TODO: How do we ensure the chain oracle is consistent across a single call? +/// * We need to somehow lock the data! What if the ChainOracle is remote? +/// * Get tip method! And check the tip still exists at the end! And every internal call +/// does not go beyond the initial tip. pub trait ChainOracle { /// Error type. type Error: core::fmt::Debug; @@ -71,6 +75,8 @@ pub trait ChainOracle { } } +// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this? +// Box, Arc ????? I will figure it out impl ChainOracle for &C { type Error = C::Error; From 8c906170c96919cd8c65e306b8351fe01e139fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 30 Mar 2023 18:14:44 +0800 Subject: [PATCH 14/48] [bdk_chain_redesign] Make default anchor for `TxGraph` as `()` This allows us to use the old API with minimal changes. `TxGraph` methods have also been rearranged to allow for it. --- crates/bdk/src/wallet/mod.rs | 2 +- crates/chain/src/chain_graph.rs | 16 +- crates/chain/src/tx_graph.rs | 310 ++++++++++++++++---------------- 3 files changed, 164 insertions(+), 164 deletions(-) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 6e4adf74..84040d5f 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -524,7 +524,7 @@ impl Wallet { /// unconfirmed transactions last. pub fn transactions( &self, - ) -> impl DoubleEndedIterator)> + '_ + ) -> impl DoubleEndedIterator)> + '_ { self.keychain_tracker .chain_graph() diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index 3c841965..85a4c956 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -27,7 +27,7 @@ use core::fmt::Debug; #[derive(Clone, Debug, PartialEq)] pub struct ChainGraph

{ chain: SparseChain

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

Default for ChainGraph

{ @@ -45,8 +45,8 @@ impl

AsRef> for ChainGraph

{ } } -impl

AsRef> for ChainGraph

{ - fn as_ref(&self) -> &TxGraph { +impl

AsRef for ChainGraph

{ + fn as_ref(&self) -> &TxGraph { &self.graph } } @@ -64,7 +64,7 @@ impl

ChainGraph

{ } /// Returns a reference to the internal [`TxGraph`]. - pub fn graph(&self) -> &TxGraph { + pub fn graph(&self) -> &TxGraph { &self.graph } } @@ -81,7 +81,7 @@ 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(graphed_tx) = graph.get_tx(*txid) { @@ -212,7 +212,7 @@ 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, TxInGraph<'_, Transaction, BlockId>)> { + pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, TxInGraph<'_, Transaction, ()>)> { let position = self.chain.tx_position(txid)?; let graphed_tx = self.graph.get_tx(txid).expect("must exist"); Some((position, graphed_tx)) @@ -430,7 +430,7 @@ where /// in ascending order. pub fn transactions_in_chain( &self, - ) -> impl DoubleEndedIterator)> { + ) -> impl DoubleEndedIterator)> { self.chain .txids() .map(move |(pos, txid)| (pos, self.graph.get_tx(*txid).expect("must exist"))) @@ -469,7 +469,7 @@ where #[must_use] pub struct ChangeSet

{ pub chain: sparse_chain::ChangeSet

, - pub graph: tx_graph::Additions, + pub graph: tx_graph::Additions<()>, } impl

ChangeSet

{ diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index ddeb5e13..2236822b 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,7 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, ObservedIn}; +use crate::{collections::*, BlockAnchor, ChainOracle, ForEachTxOut, ObservedIn}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::{ @@ -69,7 +69,7 @@ use core::{ /// /// [module-level documentation]: crate::tx_graph #[derive(Clone, Debug, PartialEq)] -pub struct TxGraph { +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>, @@ -244,9 +244,111 @@ impl TxGraph { Some(inputs_sum - outputs_sum) } + + /// The transactions spending from this output. + /// + /// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in + /// the returned set will never be in the same active-chain. + pub fn outspends(&self, outpoint: OutPoint) -> &HashSet { + self.spends.get(&outpoint).unwrap_or(&self.empty_outspends) + } + + /// Iterates over the transactions spending from `txid`. + /// + /// The iterator item is a union of `(vout, txid-set)` where: + /// + /// - `vout` is the provided `txid`'s outpoint that is being spent + /// - `txid-set` is the set of txids spending the `vout`. + pub fn tx_outspends( + &self, + txid: Txid, + ) -> impl DoubleEndedIterator)> + '_ { + let start = OutPoint { txid, vout: 0 }; + let end = OutPoint { + txid, + vout: u32::MAX, + }; + self.spends + .range(start..=end) + .map(|(outpoint, spends)| (outpoint.vout, spends)) + } + + /// Iterate over all partial transactions (outputs only) in the graph. + 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(TxInGraph { + txid, + tx: partial, + anchors, + last_seen: *last_seen, + }), + }) + } + + /// Creates an iterator that filters and maps descendants from the starting `txid`. + /// + /// The supplied closure takes in two inputs `(depth, descendant_txid)`: + /// + /// * `depth` is the distance between the starting `txid` and the `descendant_txid`. I.e., if the + /// descendant is spending an output of the starting `txid`; the `depth` will be 1. + /// * `descendant_txid` is the descendant's txid which we are considering to walk. + /// + /// 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 + where + F: FnMut(usize, Txid) -> Option + 'g, + { + TxDescendants::new_exclude_root(self, txid, walk_map) + } + + /// Creates an iterator that both filters and maps conflicting transactions (this includes + /// 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 + where + F: FnMut(usize, Txid) -> Option + 'g, + { + let txids = self.direct_conflicts_of_tx(tx).map(|(_, txid)| txid); + TxDescendants::from_multiple_include_root(self, txids, walk_map) + } + + /// Given a transaction, return an iterator of txids that directly conflict with the given + /// transaction's inputs (spends). The conflicting txids are returned with the given + /// transaction's vin (in which it conflicts). + /// + /// Note that this only returns directly conflicting txids and does not include descendants of + /// those txids (which are technically also conflicting). + pub fn direct_conflicts_of_tx<'g>( + &'g self, + tx: &'g Transaction, + ) -> impl Iterator + '_ { + let txid = tx.txid(); + tx.input + .iter() + .enumerate() + .filter_map(move |(vin, txin)| self.spends.get(&txin.previous_output).zip(Some(vin))) + .flat_map(|(spends, vin)| core::iter::repeat(vin).zip(spends.iter().cloned())) + .filter(move |(_, conflicting_txid)| *conflicting_txid != txid) + } + + /// Whether the graph has any transactions or outputs in it. + pub fn is_empty(&self) -> bool { + self.txs.is_empty() + } } -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(); @@ -256,6 +358,24 @@ impl TxGraph { new } + /// Returns the resultant [`Additions`] if the given `txout` is inserted at `outpoint`. Does not + /// mutate `self`. + /// + /// 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 { + let mut update = Self::default(); + update.txs.insert( + outpoint.txid, + ( + TxNode::Partial([(outpoint.vout, txout)].into()), + BTreeSet::new(), + 0, + ), + ); + self.determine_additions(&update) + } + /// Inserts the given [`TxOut`] at [`OutPoint`]. /// /// Note this will ignore the action if we already have the full transaction that the txout is @@ -266,6 +386,18 @@ impl TxGraph { additions } + /// Returns the resultant [`Additions`] if the given transaction is inserted. Does not actually + /// mutate [`Self`]. + /// + /// The [`Additions`] result will be empty if `tx` already exists in `self`. + pub fn insert_tx_preview(&self, tx: Transaction) -> Additions { + let mut update = Self::default(); + update + .txs + .insert(tx.txid(), (TxNode::Whole(tx), BTreeSet::new(), 0)); + self.determine_additions(&update) + } + /// Inserts the given transaction into [`TxGraph`]. /// /// The [`Additions`] returned will be empty if `tx` already exists. @@ -275,6 +407,13 @@ impl TxGraph { additions } + /// 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) + } + /// Inserts the given `anchor` into [`TxGraph`]. /// /// This is equivalent to calling [`insert_anchor_preview`] and [`apply_additions`] in sequence. @@ -289,6 +428,16 @@ impl TxGraph { additions } + /// 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) + } + /// Inserts the given `seen_at` into [`TxGraph`]. /// /// This is equivalent to calling [`insert_seen_at_preview`] and [`apply_additions`] in @@ -421,54 +570,9 @@ impl TxGraph { additions } +} - /// Returns the resultant [`Additions`] if the given transaction is inserted. Does not actually - /// mutate [`Self`]. - /// - /// The [`Additions`] result will be empty if `tx` already exists in `self`. - pub fn insert_tx_preview(&self, tx: Transaction) -> Additions { - let mut update = Self::default(); - update - .txs - .insert(tx.txid(), (TxNode::Whole(tx), BTreeSet::new(), 0)); - self.determine_additions(&update) - } - - /// Returns the resultant [`Additions`] if the given `txout` is inserted at `outpoint`. Does not - /// mutate `self`. - /// - /// 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 { - let mut update = Self::default(); - update.txs.insert( - outpoint.txid, - ( - 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 { /// Get all heights that are relevant to the graph. pub fn relevant_heights(&self) -> BTreeSet { self.anchors @@ -573,110 +677,6 @@ impl TxGraph { } } -impl TxGraph { - /// The transactions spending from this output. - /// - /// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in - /// the returned set will never be in the same active-chain. - pub fn outspends(&self, outpoint: OutPoint) -> &HashSet { - self.spends.get(&outpoint).unwrap_or(&self.empty_outspends) - } - - /// Iterates over the transactions spending from `txid`. - /// - /// The iterator item is a union of `(vout, txid-set)` where: - /// - /// - `vout` is the provided `txid`'s outpoint that is being spent - /// - `txid-set` is the set of txids spending the `vout`. - pub fn tx_outspends( - &self, - txid: Txid, - ) -> impl DoubleEndedIterator)> + '_ { - let start = OutPoint { txid, vout: 0 }; - let end = OutPoint { - txid, - vout: u32::MAX, - }; - self.spends - .range(start..=end) - .map(|(outpoint, spends)| (outpoint.vout, spends)) - } - - /// Iterate over all partial transactions (outputs only) in the graph. - 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(TxInGraph { - txid, - tx: partial, - anchors, - last_seen: *last_seen, - }), - }) - } - - /// Creates an iterator that filters and maps descendants from the starting `txid`. - /// - /// The supplied closure takes in two inputs `(depth, descendant_txid)`: - /// - /// * `depth` is the distance between the starting `txid` and the `descendant_txid`. I.e., if the - /// descendant is spending an output of the starting `txid`; the `depth` will be 1. - /// * `descendant_txid` is the descendant's txid which we are considering to walk. - /// - /// 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 - where - F: FnMut(usize, Txid) -> Option + 'g, - { - TxDescendants::new_exclude_root(self, txid, walk_map) - } - - /// Creates an iterator that both filters and maps conflicting transactions (this includes - /// 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 - where - F: FnMut(usize, Txid) -> Option + 'g, - { - let txids = self.direct_conflicts_of_tx(tx).map(|(_, txid)| txid); - TxDescendants::from_multiple_include_root(self, txids, walk_map) - } - - /// Given a transaction, return an iterator of txids that directly conflict with the given - /// transaction's inputs (spends). The conflicting txids are returned with the given - /// transaction's vin (in which it conflicts). - /// - /// Note that this only returns directly conflicting txids and does not include descendants of - /// those txids (which are technically also conflicting). - pub fn direct_conflicts_of_tx<'g>( - &'g self, - tx: &'g Transaction, - ) -> impl Iterator + '_ { - let txid = tx.txid(); - tx.input - .iter() - .enumerate() - .filter_map(move |(vin, txin)| self.spends.get(&txin.previous_output).zip(Some(vin))) - .flat_map(|(spends, vin)| core::iter::repeat(vin).zip(spends.iter().cloned())) - .filter(move |(_, conflicting_txid)| *conflicting_txid != txid) - } - - /// Whether the graph has any transactions or outputs in it. - pub fn is_empty(&self) -> bool { - self.txs.is_empty() - } -} - /// A structure that represents changes to a [`TxGraph`]. /// /// It is named "additions" because [`TxGraph`] is monotone, so transactions can only be added and @@ -698,7 +698,7 @@ impl TxGraph { ) )] #[must_use] -pub struct Additions { +pub struct Additions { pub tx: BTreeSet, pub txout: BTreeMap, pub anchors: BTreeSet<(A, Txid)>, From a1172def7df478c61076b0d99d5d0f5f9cd99da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 30 Mar 2023 18:33:53 +0800 Subject: [PATCH 15/48] [bdk_chain_redesign] Revert some API changes Methods of old structures that return transaction(s) no longer return `TxNode`, but `Transaction` as done previously. `TxInGraph` is renamed to `TxNode`, while the internal `TxNode` is renamed to `TxNodeInternal`. --- crates/bdk/src/wallet/mod.rs | 19 ++--- crates/chain/src/chain_graph.rs | 24 +++--- crates/chain/src/indexed_tx_graph.rs | 4 +- crates/chain/src/tx_graph.rs | 86 ++++++++++++--------- crates/chain/tests/test_chain_graph.rs | 29 ++----- crates/chain/tests/test_keychain_tracker.rs | 7 +- crates/chain/tests/test_tx_graph.rs | 34 ++++---- 7 files changed, 87 insertions(+), 116 deletions(-) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 84040d5f..67032cd3 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -23,9 +23,7 @@ pub use bdk_chain::keychain::Balance; use bdk_chain::{ chain_graph, keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker}, - sparse_chain, - tx_graph::TxInGraph, - BlockId, ConfirmationTime, + sparse_chain, BlockId, ConfirmationTime, }; use bitcoin::consensus::encode::serialize; use bitcoin::secp256k1::Secp256k1; @@ -455,11 +453,7 @@ impl Wallet { let fee = inputs.map(|inputs| inputs.saturating_sub(outputs)); Some(TransactionDetails { - transaction: if include_raw { - Some(tx.tx.clone()) - } else { - None - }, + transaction: if include_raw { Some(tx.clone()) } else { None }, txid, received, sent, @@ -524,8 +518,7 @@ impl Wallet { /// unconfirmed transactions last. pub fn transactions( &self, - ) -> impl DoubleEndedIterator)> + '_ - { + ) -> impl DoubleEndedIterator + '_ { self.keychain_tracker .chain_graph() .transactions_in_chain() @@ -1034,7 +1027,7 @@ impl Wallet { Some((ConfirmationTime::Confirmed { .. }, _tx)) => { return Err(Error::TransactionConfirmed) } - Some((_, tx)) => tx.tx.clone(), + Some((_, tx)) => tx.clone(), }; if !tx @@ -1092,7 +1085,7 @@ impl Wallet { outpoint: txin.previous_output, psbt_input: Box::new(psbt::Input { witness_utxo: Some(txout.clone()), - non_witness_utxo: Some(prev_tx.tx.clone()), + non_witness_utxo: Some(prev_tx.clone()), ..Default::default() }), }, @@ -1620,7 +1613,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.tx.clone()); + psbt_input.non_witness_utxo = Some(prev_tx.clone()); } } Ok(psbt_input) diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index 85a4c956..8c954f8d 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -2,7 +2,7 @@ use crate::{ collections::HashSet, sparse_chain::{self, ChainPosition, SparseChain}, - tx_graph::{self, TxGraph, TxInGraph}, + tx_graph::{self, TxGraph}, BlockId, ForEachTxOut, FullTxOut, TxHeight, }; use alloc::{string::ToString, vec::Vec}; @@ -84,11 +84,9 @@ where pub fn new(chain: SparseChain

, graph: TxGraph) -> Result> { let mut missing = HashSet::default(); for (pos, txid) in chain.txids() { - if let Some(graphed_tx) = graph.get_tx(*txid) { + if let Some(tx) = graph.get_tx(*txid) { let conflict = graph - .walk_conflicts(graphed_tx.tx, |_, txid| { - Some((chain.tx_position(txid)?.clone(), txid)) - }) + .walk_conflicts(tx, |_, txid| Some((chain.tx_position(txid)?.clone(), txid))) .next(); if let Some((conflict_pos, conflict)) = conflict { return Err(NewError::Conflict { @@ -145,7 +143,7 @@ where match self.chain.tx_position(*txid) { Some(original_pos) => { if original_pos != pos { - let graphed_tx = self + let tx = self .graph .get_tx(*txid) .expect("tx must exist as it is referenced in sparsechain") @@ -153,7 +151,7 @@ where let _ = inflated_chain .insert_tx(*txid, pos.clone()) .expect("must insert since this was already in update"); - let _ = inflated_graph.insert_tx(graphed_tx.tx.clone()); + let _ = inflated_graph.insert_tx(tx.clone()); } } None => { @@ -212,10 +210,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, TxInGraph<'_, Transaction, ()>)> { + pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, &Transaction)> { let position = self.chain.tx_position(txid)?; - let graphed_tx = self.graph.get_tx(txid).expect("must exist"); - Some((position, graphed_tx)) + let tx = self.graph.get_tx(txid).expect("must exist"); + Some((position, tx)) } /// Determines the changes required to insert a transaction into the inner [`ChainGraph`] and @@ -348,7 +346,7 @@ where None => continue, }; - let mut full_tx = self.graph.get_tx(txid).map(|tx| tx.tx); + let mut full_tx = self.graph.get_tx(txid); if full_tx.is_none() { full_tx = changeset.graph.tx.iter().find(|tx| tx.txid() == txid) @@ -428,9 +426,7 @@ 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"))) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 21852150..2e0315d8 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -5,7 +5,7 @@ use bitcoin::{OutPoint, Transaction, TxOut}; use crate::{ keychain::Balance, sparse_chain::ChainPosition, - tx_graph::{Additions, TxGraph, TxInGraph}, + tx_graph::{Additions, TxGraph, TxNode}, BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex, TxIndexAdditions, }; @@ -15,7 +15,7 @@ pub struct TxInChain<'a, T, A> { /// Where the transaction is observed (in a block or in mempool). pub observed_in: ObservedIn<&'a A>, /// The transaction with anchors and last seen timestamp. - pub tx: TxInGraph<'a, T, A>, + pub tx: TxNode<'a, T, A>, } /// An outwards-facing view of a relevant txout that is part of the *best chain*'s history. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 2236822b..3aca6a6f 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -71,7 +71,7 @@ use core::{ #[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)>, + txs: HashMap, u64)>, spends: BTreeMap>, anchors: BTreeSet<(A, Txid)>, @@ -94,9 +94,9 @@ impl Default for TxGraph { // pub type InChainTx<'a, T, A> = (ObservedIn<&'a A>, TxInGraph<'a, T, A>); // pub type InChainTxOut<'a, I, A> = (&'a I, FullTxOut>); -/// An outward-facing view of a transaction that resides in a [`TxGraph`]. +/// An outward-facing view of a transaction node that resides in a [`TxGraph`]. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct TxInGraph<'a, T, A> { +pub struct TxNode<'a, T, A> { /// Txid of the transaction. pub txid: Txid, /// A partial or full representation of the transaction. @@ -107,7 +107,7 @@ pub struct TxInGraph<'a, T, A> { pub last_seen: u64, } -impl<'a, T, A> Deref for TxInGraph<'a, T, A> { +impl<'a, T, A> Deref for TxNode<'a, T, A> { type Target = T; fn deref(&self) -> &Self::Target { @@ -115,7 +115,7 @@ impl<'a, T, A> Deref for TxInGraph<'a, T, A> { } } -impl<'a, A> TxInGraph<'a, Transaction, A> { +impl<'a, A> TxNode<'a, Transaction, A> { pub fn from_tx(tx: &'a Transaction, anchors: &'a BTreeSet) -> Self { Self { txid: tx.txid(), @@ -131,12 +131,12 @@ impl<'a, A> TxInGraph<'a, Transaction, A> { /// This can either be a whole transaction, or a partial transaction (where we only have select /// outputs). #[derive(Clone, Debug, PartialEq)] -enum TxNode { +enum TxNodeInternal { Whole(Transaction), Partial(BTreeMap), } -impl Default for TxNode { +impl Default for TxNodeInternal { fn default() -> Self { Self::Partial(BTreeMap::new()) } @@ -146,13 +146,13 @@ 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 { - TxNode::Whole(tx) => tx + TxNodeInternal::Whole(tx) => tx .output .iter() .enumerate() .map(|(vout, txout)| (OutPoint::new(*txid, vout as _), txout)) .collect::>(), - TxNode::Partial(txouts) => txouts + TxNodeInternal::Partial(txouts) => txouts .iter() .map(|(vout, txout)| (OutPoint::new(*txid, *vout as _), txout)) .collect::>(), @@ -160,17 +160,17 @@ impl TxGraph { } /// Iterate over all full transactions in the graph. - pub fn full_transactions(&self) -> impl Iterator> { + pub fn full_transactions(&self) -> impl Iterator> { self.txs .iter() .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { - TxNode::Whole(tx) => Some(TxInGraph { + TxNodeInternal::Whole(tx) => Some(TxNode { txid, tx, anchors, last_seen: *last_seen, }), - TxNode::Partial(_) => None, + TxNodeInternal::Partial(_) => None, }) } @@ -179,9 +179,14 @@ impl TxGraph { /// Refer to [`get_txout`] for getting a specific [`TxOut`]. /// /// [`get_txout`]: Self::get_txout - pub fn get_tx(&self, txid: Txid) -> Option> { + pub fn get_tx(&self, txid: Txid) -> Option<&Transaction> { + self.get_tx_node(txid).map(|n| n.tx) + } + + /// Get a transaction node by txid. This only returns `Some` for full transactions. + pub fn get_tx_node(&self, txid: Txid) -> Option> { match &self.txs.get(&txid)? { - (TxNode::Whole(tx), anchors, last_seen) => Some(TxInGraph { + (TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode { txid, tx, anchors, @@ -194,21 +199,21 @@ impl TxGraph { /// 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)?.0 { - TxNode::Whole(tx) => tx.output.get(outpoint.vout as usize), - TxNode::Partial(txouts) => txouts.get(&outpoint.vout), + TxNodeInternal::Whole(tx) => tx.output.get(outpoint.vout as usize), + TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout), } } /// Returns a [`BTreeMap`] of vout to output of the provided `txid`. pub fn txouts(&self, txid: Txid) -> Option> { Some(match &self.txs.get(&txid)?.0 { - TxNode::Whole(tx) => tx + TxNodeInternal::Whole(tx) => tx .output .iter() .enumerate() .map(|(vout, txout)| (vout as u32, txout)) .collect::>(), - TxNode::Partial(txouts) => txouts + TxNodeInternal::Partial(txouts) => txouts .iter() .map(|(vout, txout)| (*vout, txout)) .collect::>(), @@ -276,12 +281,12 @@ impl TxGraph { /// Iterate over all partial transactions (outputs only) in the graph. pub fn partial_transactions( &self, - ) -> impl Iterator, A>> { + ) -> impl Iterator, A>> { self.txs .iter() .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { - TxNode::Whole(_) => None, - TxNode::Partial(partial) => Some(TxInGraph { + TxNodeInternal::Whole(_) => None, + TxNodeInternal::Partial(partial) => Some(TxNode { txid, tx: partial, anchors, @@ -368,7 +373,7 @@ impl TxGraph { update.txs.insert( outpoint.txid, ( - TxNode::Partial([(outpoint.vout, txout)].into()), + TxNodeInternal::Partial([(outpoint.vout, txout)].into()), BTreeSet::new(), 0, ), @@ -394,7 +399,7 @@ impl TxGraph { let mut update = Self::default(); update .txs - .insert(tx.txid(), (TxNode::Whole(tx), BTreeSet::new(), 0)); + .insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0)); self.determine_additions(&update) } @@ -478,10 +483,10 @@ impl TxGraph { }); match self.txs.get_mut(&txid) { - Some((tx_node @ TxNode::Partial(_), _, _)) => { - *tx_node = TxNode::Whole(tx); + Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => { + *tx_node = TxNodeInternal::Whole(tx); } - Some((TxNode::Whole(tx), _, _)) => { + Some((TxNodeInternal::Whole(tx), _, _)) => { debug_assert_eq!( tx.txid(), txid, @@ -490,7 +495,7 @@ impl TxGraph { } None => { self.txs - .insert(txid, (TxNode::Whole(tx), BTreeSet::new(), 0)); + .insert(txid, (TxNodeInternal::Whole(tx), BTreeSet::new(), 0)); } } } @@ -502,8 +507,9 @@ impl TxGraph { .or_insert_with(Default::default); match tx_entry { - (TxNode::Whole(_), _, _) => { /* do nothing since we already have full tx */ } - (TxNode::Partial(txouts), _, _) => { + (TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */ + } + (TxNodeInternal::Partial(txouts), _, _) => { txouts.insert(outpoint.vout, txout); } } @@ -533,11 +539,11 @@ impl TxGraph { 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)) => { + (None, TxNodeInternal::Whole(update_tx)) => { additions.tx.insert(update_tx.clone()); 0 } - (None, TxNode::Partial(update_txos)) => { + (None, TxNodeInternal::Partial(update_txos)) => { additions.txout.extend( update_txos .iter() @@ -545,12 +551,18 @@ impl TxGraph { ); 0 } - (Some((TxNode::Whole(_), _, last_seen)), _) => *last_seen, - (Some((TxNode::Partial(_), _, last_seen)), TxNode::Whole(update_tx)) => { + (Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen, + ( + Some((TxNodeInternal::Partial(_), _, last_seen)), + TxNodeInternal::Whole(update_tx), + ) => { additions.tx.insert(update_tx.clone()); *last_seen } - (Some((TxNode::Partial(txos), _, last_seen)), TxNode::Partial(update_txos)) => { + ( + Some((TxNodeInternal::Partial(txos), _, last_seen)), + TxNodeInternal::Partial(update_txos), + ) => { additions.txout.extend( update_txos .iter() @@ -608,8 +620,8 @@ impl TxGraph { // The tx is not anchored to a block which is in the best chain, let's check whether we can // ignore it by checking conflicts! let tx = match tx_node { - TxNode::Whole(tx) => tx, - TxNode::Partial(_) => { + TxNodeInternal::Whole(tx) => tx, + TxNodeInternal::Partial(_) => { // [TODO] Unfortunately, we can't iterate over conflicts of partial txs right now! // [TODO] So we just assume the partial tx does not exist in the best chain :/ return Ok(None); @@ -618,7 +630,7 @@ impl TxGraph { // [TODO] Is this logic correct? I do not think so, but it should be good enough for now! let mut latest_last_seen = 0_u64; - for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx(txid)) { + for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) { for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) { if chain.is_block_in_best_chain(block_id)? { // conflicting tx is in best chain, so the current tx cannot be in best chain! diff --git a/crates/chain/tests/test_chain_graph.rs b/crates/chain/tests/test_chain_graph.rs index 0514acc9..b5cbf5b9 100644 --- a/crates/chain/tests/test_chain_graph.rs +++ b/crates/chain/tests/test_chain_graph.rs @@ -1,13 +1,11 @@ #[macro_use] mod common; -use std::collections::BTreeSet; - use bdk_chain::{ chain_graph::*, collections::HashSet, sparse_chain, - tx_graph::{self, TxGraph, TxInGraph}, + tx_graph::{self, TxGraph}, BlockId, TxHeight, }; use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness}; @@ -367,15 +365,7 @@ 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, - TxInGraph { - txid: tx.txid(), - tx: &tx, - anchors: &BTreeSet::new(), - last_seen: 0 - } - )) + Some((&TxHeight::Unconfirmed, &tx,)) ); } @@ -407,18 +397,9 @@ fn test_iterate_transactions() { assert_eq!( cg.transactions_in_chain().collect::>(), vec![ - ( - &TxHeight::Confirmed(0), - TxInGraph::from_tx(&txs[2], &BTreeSet::new()) - ), - ( - &TxHeight::Confirmed(1), - TxInGraph::from_tx(&txs[0], &BTreeSet::new()) - ), - ( - &TxHeight::Unconfirmed, - TxInGraph::from_tx(&txs[1], &BTreeSet::new()) - ), + (&TxHeight::Confirmed(0), &txs[2],), + (&TxHeight::Confirmed(1), &txs[0],), + (&TxHeight::Unconfirmed, &txs[1],), ] ); } diff --git a/crates/chain/tests/test_keychain_tracker.rs b/crates/chain/tests/test_keychain_tracker.rs index c3fee347..bd8c6e03 100644 --- a/crates/chain/tests/test_keychain_tracker.rs +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -1,7 +1,6 @@ #![cfg(feature = "miniscript")] #[macro_use] mod common; -use std::collections::BTreeSet; use bdk_chain::{ keychain::{Balance, KeychainTracker}, @@ -9,7 +8,6 @@ use bdk_chain::{ bitcoin::{secp256k1::Secp256k1, OutPoint, PackedLockTime, Transaction, TxOut}, Descriptor, }, - tx_graph::TxInGraph, BlockId, ConfirmationTime, TxHeight, }; use bitcoin::TxIn; @@ -43,10 +41,7 @@ fn test_insert_tx() { .chain_graph() .transactions_in_chain() .collect::>(), - vec![( - &ConfirmationTime::Unconfirmed, - TxInGraph::from_tx(&tx, &BTreeSet::new()) - )] + vec![(&ConfirmationTime::Unconfirmed, &tx,)] ); assert_eq!( diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 107e106d..279ddb74 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,12 +2,9 @@ mod common; use bdk_chain::{ collections::*, - tx_graph::{Additions, TxGraph, TxInGraph}, - BlockId, -}; -use bitcoin::{ - hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid, + tx_graph::{Additions, TxGraph}, }; +use bitcoin::{hashes::Hash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid}; use core::iter; #[test] @@ -38,7 +35,7 @@ fn insert_txouts() { )]; let mut graph = { - let mut graph = TxGraph::<(u32, BlockHash)>::default(); + let mut graph = TxGraph::<()>::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -94,7 +91,7 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() { output: vec![], }; - let mut graph = TxGraph::<(u32, BlockHash)>::default(); + let mut graph = TxGraph::<()>::default(); let _ = graph.insert_tx(tx); assert!(graph.outspends(OutPoint::null()).is_empty()); assert!(graph.tx_outspends(Txid::all_zeros()).next().is_none()); @@ -124,8 +121,8 @@ fn insert_tx_graph_keeps_track_of_spend() { output: vec![], }; - let mut graph1 = TxGraph::<(u32, BlockHash)>::default(); - let mut graph2 = TxGraph::<(u32, BlockHash)>::default(); + let mut graph1 = TxGraph::<()>::default(); + let mut graph2 = TxGraph::<()>::default(); // insert in different order let _ = graph1.insert_tx(tx1.clone()); @@ -153,17 +150,14 @@ 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(TxInGraph::from_tx(&tx, &BTreeSet::new())) - ); + assert_eq!(graph.get_tx(tx.txid()), Some(&tx)); } #[test] fn insert_tx_displaces_txouts() { - let mut tx_graph = TxGraph::<(u32, BlockHash)>::default(); + let mut tx_graph = TxGraph::<()>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -219,7 +213,7 @@ fn insert_tx_displaces_txouts() { #[test] fn insert_txout_does_not_displace_tx() { - let mut tx_graph = TxGraph::<(u32, BlockHash)>::default(); + let mut tx_graph = TxGraph::<()>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -275,7 +269,7 @@ fn insert_txout_does_not_displace_tx() { #[test] fn test_calculate_fee() { - let mut graph = TxGraph::<(u32, BlockHash)>::default(); + let mut graph = TxGraph::<()>::default(); let intx1 = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -369,7 +363,7 @@ fn test_calculate_fee_on_coinbase() { output: vec![TxOut::default()], }; - let graph = TxGraph::<(u32, BlockHash)>::default(); + let graph = TxGraph::<()>::default(); assert_eq!(graph.calculate_fee(&tx), Some(0)); } @@ -411,7 +405,7 @@ fn test_conflicting_descendants() { let txid_a = tx_a.txid(); let txid_b = tx_b.txid(); - let mut graph = TxGraph::<(u32, BlockHash)>::default(); + let mut graph = TxGraph::<()>::default(); let _ = graph.insert_tx(tx_a); let _ = graph.insert_tx(tx_b); @@ -487,7 +481,7 @@ fn test_descendants_no_repeat() { }) .collect::>(); - let mut graph = TxGraph::<(u32, BlockHash)>::default(); + let mut graph = TxGraph::<()>::default(); let mut expected_txids = BTreeSet::new(); // these are NOT descendants of `tx_a` From a63ffe97397cd14bc0a13ea5e96ceddf8b63a4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 31 Mar 2023 12:39:00 +0800 Subject: [PATCH 16/48] [bdk_chain_redesign] Simplify `TxIndex` --- crates/chain/src/indexed_tx_graph.rs | 141 +++++++++++------------ crates/chain/src/keychain.rs | 10 +- crates/chain/src/keychain/txout_index.rs | 6 +- crates/chain/src/spk_txout_index.rs | 17 ++- crates/chain/src/tx_data_traits.rs | 36 +----- 5 files changed, 86 insertions(+), 124 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 2e0315d8..0b27150c 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,12 +1,12 @@ -use core::convert::Infallible; +use core::{convert::Infallible, ops::AddAssign}; -use bitcoin::{OutPoint, Transaction, TxOut}; +use bitcoin::{OutPoint, Script, Transaction, TxOut}; use crate::{ keychain::Balance, sparse_chain::ChainPosition, tx_graph::{Additions, TxGraph, TxNode}, - BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex, TxIndexAdditions, + BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex, }; /// An outwards-facing view of a transaction that is part of the *best chain*'s history. @@ -18,46 +18,37 @@ pub struct TxInChain<'a, T, A> { pub tx: TxNode<'a, T, A>, } -/// An outwards-facing view of a relevant txout that is part of the *best chain*'s history. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct TxOutInChain<'a, I, A> { - /// The custom index of the txout's script pubkey. - pub spk_index: &'a I, - /// The full txout. - pub txout: FullTxOut>, -} - /// A structure that represents changes to an [`IndexedTxGraph`]. #[derive(Clone, Debug, PartialEq)] #[must_use] -pub struct IndexedAdditions { +pub struct IndexedAdditions { /// [`TxGraph`] additions. pub graph_additions: Additions, /// [`TxIndex`] additions. - pub index_delta: D, + pub index_additions: IA, /// Last block height witnessed (if any). pub last_height: Option, } -impl Default for IndexedAdditions { +impl Default for IndexedAdditions { fn default() -> Self { Self { graph_additions: Default::default(), - index_delta: Default::default(), + index_additions: Default::default(), last_height: None, } } } -impl TxIndexAdditions for IndexedAdditions { - fn append_additions(&mut self, other: Self) { +impl AddAssign for IndexedAdditions { + fn add_assign(&mut self, rhs: Self) { let Self { graph_additions, - index_delta, + index_additions: index_delta, last_height, - } = other; + } = rhs; self.graph_additions.append(graph_additions); - self.index_delta.append_additions(index_delta); + self.index_additions += index_delta; if self.last_height < last_height { let last_height = last_height.expect("must exist as it is larger than self.last_height"); @@ -102,11 +93,11 @@ impl IndexedTxGraph { pub fn apply_additions(&mut self, additions: IndexedAdditions) { let IndexedAdditions { graph_additions, - index_delta, + index_additions, last_height, } = additions; - self.index.apply_additions(index_delta); + self.index.apply_additions(index_additions); for tx in &graph_additions.tx { self.index.index_tx(tx); @@ -122,16 +113,23 @@ impl IndexedTxGraph { } } - /// Insert a block height that the chain source has scanned up to. - pub fn insert_height(&mut self, tip: u32) -> IndexedAdditions { + fn insert_height_internal(&mut self, tip: u32) -> Option { if self.last_height < tip { self.last_height = tip; - IndexedAdditions { - last_height: Some(tip), - ..Default::default() - } + Some(tip) } else { - IndexedAdditions::default() + None + } + } + + /// Insert a block height that the chain source has scanned up to. + pub fn insert_height(&mut self, tip: u32) -> IndexedAdditions + where + I::Additions: Default, + { + IndexedAdditions { + last_height: self.insert_height_internal(tip), + ..Default::default() } } @@ -142,12 +140,12 @@ impl IndexedTxGraph { txout: &TxOut, observation: ObservedIn, ) -> IndexedAdditions { - let mut additions = match &observation { - ObservedIn::Block(anchor) => self.insert_height(anchor.anchor_block().height), - ObservedIn::Mempool(_) => IndexedAdditions::default(), + let last_height = match &observation { + ObservedIn::Block(anchor) => self.insert_height_internal(anchor.anchor_block().height), + ObservedIn::Mempool(_) => None, }; - additions.append_additions(IndexedAdditions { + IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); graph_additions.append(match observation { @@ -158,11 +156,9 @@ impl IndexedTxGraph { }); graph_additions }, - index_delta: ::index_txout(&mut self.index, outpoint, txout), - last_height: None, - }); - - additions + index_additions: ::index_txout(&mut self.index, outpoint, txout), + last_height, + } } pub fn insert_tx( @@ -172,12 +168,12 @@ impl IndexedTxGraph { ) -> IndexedAdditions { let txid = tx.txid(); - let mut additions = match &observation { - ObservedIn::Block(anchor) => self.insert_height(anchor.anchor_block().height), - ObservedIn::Mempool(_) => IndexedAdditions::default(), + let last_height = match &observation { + ObservedIn::Block(anchor) => self.insert_height_internal(anchor.anchor_block().height), + ObservedIn::Mempool(_) => None, }; - additions.append_additions(IndexedAdditions { + IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_tx(tx.clone()); graph_additions.append(match observation { @@ -186,11 +182,9 @@ impl IndexedTxGraph { }); graph_additions }, - index_delta: ::index_tx(&mut self.index, tx), - last_height: None, - }); - - additions + index_additions: ::index_tx(&mut self.index, tx), + last_height, + } } pub fn filter_and_insert_txs<'t, T>( @@ -200,6 +194,7 @@ impl IndexedTxGraph { ) -> IndexedAdditions where T: Iterator, + I::Additions: Default + AddAssign, { txs.filter_map(|tx| { if self.index.is_tx_relevant(tx) { @@ -209,7 +204,7 @@ impl IndexedTxGraph { } }) .fold(IndexedAdditions::default(), |mut acc, other| { - acc.append_additions(other); + acc += other; acc }) } @@ -252,50 +247,47 @@ impl IndexedTxGraph { pub fn try_list_chain_txouts<'a, C>( &'a self, chain: C, - ) -> impl Iterator, C::Error>> + ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, ObservedIn: ChainPosition, { - self.index.relevant_txouts().iter().filter_map( - move |(op, (spk_i, txout))| -> Option> { + self.graph + .all_txouts() + .filter(|(_, txo)| self.index.is_spk_owned(&txo.script_pubkey)) + .filter_map(move |(op, txout)| -> Option> { let graph_tx = self.graph.get_tx(op.txid)?; let is_on_coinbase = graph_tx.is_coin_base(); let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) { - Ok(Some(observed_at)) => observed_at, + Ok(Some(observed_at)) => observed_at.into_owned(), Ok(None) => return None, Err(err) => return Some(Err(err)), }; - let spent_by = match self.graph.try_get_spend_in_chain(&chain, *op) { - Ok(spent_by) => spent_by, + let spent_by = match self.graph.try_get_spend_in_chain(&chain, op) { + Ok(Some((obs, txid))) => Some((obs.into_owned(), txid)), + Ok(None) => None, Err(err) => return Some(Err(err)), }; let full_txout = FullTxOut { - outpoint: *op, + outpoint: op, txout: txout.clone(), chain_position, spent_by, is_on_coinbase, }; - let txout_in_chain = TxOutInChain { - spk_index: spk_i, - txout: full_txout, - }; - - Some(Ok(txout_in_chain)) - }, - ) + Some(Ok(full_txout)) + }) } pub fn list_chain_txouts<'a, C>( &'a self, chain: C, - ) -> impl Iterator> + ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, ObservedIn: ChainPosition, @@ -308,19 +300,19 @@ impl IndexedTxGraph { pub fn try_list_chain_utxos<'a, C>( &'a self, chain: C, - ) -> impl Iterator, C::Error>> + ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, ObservedIn: ChainPosition, { self.try_list_chain_txouts(chain) - .filter(|r| !matches!(r, Ok(txo) if txo.txout.spent_by.is_none())) + .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) } pub fn list_chain_utxos<'a, C>( &'a self, chain: C, - ) -> impl Iterator> + ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, ObservedIn: ChainPosition, @@ -338,7 +330,7 @@ impl IndexedTxGraph { where C: ChainOracle, ObservedIn: ChainPosition + Clone, - F: FnMut(&I::SpkIndex) -> bool, + F: FnMut(&Script) -> bool, { let mut immature = 0; let mut trusted_pending = 0; @@ -346,8 +338,7 @@ impl IndexedTxGraph { let mut confirmed = 0; for res in self.try_list_chain_txouts(&chain) { - let TxOutInChain { spk_index, txout } = res?; - let txout = txout.into_owned(); + let txout = res?; match &txout.chain_position { ObservedIn::Block(_) => { @@ -360,7 +351,7 @@ impl IndexedTxGraph { } } ObservedIn::Mempool(_) => { - if should_trust(spk_index) { + if should_trust(&txout.txout.script_pubkey) { trusted_pending += txout.txout.value; } else { untrusted_pending += txout.txout.value; @@ -381,7 +372,7 @@ impl IndexedTxGraph { where C: ChainOracle, ObservedIn: ChainPosition + Clone, - F: FnMut(&I::SpkIndex) -> bool, + F: FnMut(&Script) -> bool, { self.try_balance(chain, tip, should_trust) .expect("error is infallible") @@ -393,8 +384,8 @@ impl IndexedTxGraph { ObservedIn: ChainPosition + Clone, { let mut sum = 0; - for res in self.try_list_chain_txouts(chain) { - let txo = res?.txout.into_owned(); + for txo_res in self.try_list_chain_txouts(chain) { + let txo = txo_res?; if txo.is_spendable_at(height) { sum += txo.txout.value; } diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index da2af6f2..53da284f 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -14,12 +14,14 @@ //! [`KeychainChangeSet`]s. //! //! [`SpkTxOutIndex`]: crate::SpkTxOutIndex +use core::ops::AddAssign; + use crate::{ chain_graph::{self, ChainGraph}, collections::BTreeMap, sparse_chain::ChainPosition, tx_graph::TxGraph, - ForEachTxOut, TxIndexAdditions, + ForEachTxOut, }; #[cfg(feature = "miniscript")] @@ -85,9 +87,9 @@ impl DerivationAdditions { } } -impl TxIndexAdditions for DerivationAdditions { - fn append_additions(&mut self, other: Self) { - self.append(other) +impl AddAssign for DerivationAdditions { + fn add_assign(&mut self, rhs: Self) { + self.append(rhs) } } diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index d19aada7..101278b7 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -91,8 +91,6 @@ impl Deref for KeychainTxOutIndex { impl TxIndex for KeychainTxOutIndex { type Additions = DerivationAdditions; - type SpkIndex = (K, u32); - fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { self.scan_txout(outpoint, txout) } @@ -109,8 +107,8 @@ impl TxIndex for KeychainTxOutIndex { self.is_relevant(tx) } - fn relevant_txouts(&self) -> &BTreeMap { - self.inner.relevant_txouts() + fn is_spk_owned(&self, spk: &Script) -> bool { + self.index_of_spk(spk).is_some() } } diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 3d1af948..6c9739be 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -53,19 +53,16 @@ impl Default for SpkTxOutIndex { } impl TxIndex for SpkTxOutIndex { - type Additions = BTreeSet; - - type SpkIndex = I; + type Additions = (); fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { - self.scan_txout(outpoint, txout) - .cloned() - .into_iter() - .collect() + self.scan_txout(outpoint, txout); + Default::default() } fn index_tx(&mut self, tx: &Transaction) -> Self::Additions { - self.scan(tx) + self.scan(tx); + Default::default() } fn apply_additions(&mut self, _additions: Self::Additions) { @@ -76,8 +73,8 @@ impl TxIndex for SpkTxOutIndex { self.is_relevant(tx) } - fn relevant_txouts(&self) -> &BTreeMap { - &self.txouts + fn is_spk_owned(&self, spk: &Script) -> bool { + self.index_of_spk(spk).is_some() } } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 485e3f70..0e2474c4 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,5 +1,4 @@ -use alloc::collections::{BTreeMap, BTreeSet}; -use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; +use bitcoin::{Block, BlockHash, OutPoint, Script, Transaction, TxOut}; use crate::BlockId; @@ -89,41 +88,16 @@ impl ChainOracle for &C { } } -/// Represents changes to a [`TxIndex`] implementation. -pub trait TxIndexAdditions: Default { - /// Append `other` on top of `self`. - fn append_additions(&mut self, other: Self); -} - -impl TxIndexAdditions for BTreeSet { - fn append_additions(&mut self, mut other: Self) { - self.append(&mut other); - } -} - /// Represents an index of transaction data. pub trait TxIndex { /// The resultant "additions" when new transaction data is indexed. - type Additions: TxIndexAdditions; - - type SpkIndex: Ord; + type Additions; /// Scan and index the given `outpoint` and `txout`. fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions; /// Scan and index the given transaction. - fn index_tx(&mut self, tx: &Transaction) -> Self::Additions { - let txid = tx.txid(); - tx.output - .iter() - .enumerate() - .map(|(vout, txout)| self.index_txout(OutPoint::new(txid, vout as _), txout)) - .reduce(|mut acc, other| { - acc.append_additions(other); - acc - }) - .unwrap_or_default() - } + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions; /// Apply additions to itself. fn apply_additions(&mut self, additions: Self::Additions); @@ -132,6 +106,6 @@ pub trait TxIndex { /// spends an already-indexed outpoint that we have previously indexed. fn is_tx_relevant(&self, tx: &Transaction) -> bool; - /// Lists all relevant txouts known by the index. - fn relevant_txouts(&self) -> &BTreeMap; + /// Returns whether the script pubkey is owned by us. + fn is_spk_owned(&self, spk: &Script) -> bool; } From 7810059ed0f23cae7dee61fe587a1c8f3f49480a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 31 Mar 2023 14:15:34 +0800 Subject: [PATCH 17/48] [bdk_chain_redesign] `TxGraph` tweaks * Rename `TxNode::last_seen` to `last_seen_unconfirmed` and improve docs * Improve `try_get_chain_position` logic and tweak comments --- crates/chain/src/tx_graph.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 3aca6a6f..bbdd7bdb 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -103,8 +103,8 @@ pub struct TxNode<'a, T, A> { 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, + /// The last-seen unix timestamp of the transaction as unconfirmed. + pub last_seen_unconfirmed: u64, } impl<'a, T, A> Deref for TxNode<'a, T, A> { @@ -121,7 +121,7 @@ impl<'a, A> TxNode<'a, Transaction, A> { txid: tx.txid(), tx, anchors, - last_seen: 0, + last_seen_unconfirmed: 0, } } } @@ -168,7 +168,7 @@ impl TxGraph { txid, tx, anchors, - last_seen: *last_seen, + last_seen_unconfirmed: *last_seen, }), TxNodeInternal::Partial(_) => None, }) @@ -190,7 +190,7 @@ impl TxGraph { txid, tx, anchors, - last_seen: *last_seen, + last_seen_unconfirmed: *last_seen, }), _ => None, } @@ -290,7 +290,7 @@ impl TxGraph { txid, tx: partial, anchors, - last_seen: *last_seen, + last_seen_unconfirmed: *last_seen, }), }) } @@ -628,8 +628,8 @@ impl TxGraph { } }; - // [TODO] Is this logic correct? I do not think so, but it should be good enough for now! - let mut latest_last_seen = 0_u64; + // If a conflicting tx is in the best chain, or has `last_seen` higher than this tx, then + // this tx cannot exist in the best chain for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) { for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) { if chain.is_block_in_best_chain(block_id)? { @@ -637,15 +637,12 @@ impl TxGraph { return Ok(None); } } - if conflicting_tx.last_seen > latest_last_seen { - latest_last_seen = conflicting_tx.last_seen; + if conflicting_tx.last_seen_unconfirmed > last_seen { + return Ok(None); } } - if last_seen >= latest_last_seen { - Ok(Some(ObservedIn::Mempool(last_seen))) - } else { - Ok(None) - } + + Ok(Some(ObservedIn::Mempool(last_seen))) } pub fn get_chain_position(&self, chain: C, txid: Txid) -> Option> From c09cd2afce4e649caa2797628edaffae08a60628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 31 Mar 2023 22:42:47 +0800 Subject: [PATCH 18/48] [bdk_chain_redesign] Added methods to `LocalChain` Also made the `IndexedTxGraph::index` field public (`index()` and `index_mut()` methods are no longer needed). --- crates/chain/src/indexed_tx_graph.rs | 13 ++------ crates/chain/src/local_chain.rs | 50 ++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 0b27150c..79e5105c 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -58,8 +58,9 @@ impl AddAssign for IndexedAdditions { } pub struct IndexedTxGraph { + /// Transaction index. + pub index: I, graph: TxGraph, - index: I, // [TODO] Make public last_height: u32, } @@ -79,16 +80,6 @@ impl IndexedTxGraph { &self.graph } - /// Get a reference of the internal transaction index. - pub fn index(&self) -> &I { - &self.index - } - - /// Get a mutable reference to the internal transaction index. - pub fn index_mut(&mut self) -> &mut I { - &mut self.index - } - /// Applies the [`IndexedAdditions`] to the [`IndexedTxGraph`]. pub fn apply_additions(&mut self, additions: IndexedAdditions) { let IndexedAdditions { diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 5bcb524f..5d459a15 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -1,6 +1,9 @@ -use core::convert::Infallible; +use core::{convert::Infallible, ops::Deref}; -use alloc::{collections::BTreeMap, vec::Vec}; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec::Vec, +}; use bitcoin::BlockHash; use crate::{BlockId, ChainOracle}; @@ -104,11 +107,52 @@ impl LocalChain { } Ok(ChangeSet(changeset)) } + + /// Applies the given `changeset`. + pub fn apply_changeset(&mut self, mut changeset: ChangeSet) { + self.blocks.append(&mut changeset.0) + } + + /// Updates [`LocalChain`] with an update [`LocalChain`]. + /// + /// This is equivilant to calling [`determine_changeset`] and [`apply_changeset`] in sequence. + /// + /// [`determine_changeset`]: Self::determine_changeset + /// [`apply_changeset`]: Self::apply_changeset + pub fn apply_update(&mut self, update: Self) -> Result { + let changeset = self.determine_changeset(&update)?; + self.apply_changeset(changeset.clone()); + Ok(changeset) + } + + pub fn initial_changeset(&self) -> ChangeSet { + ChangeSet(self.blocks.clone()) + } + + pub fn heights(&self) -> BTreeSet { + self.blocks.keys().cloned().collect() + } } -#[derive(Debug, Default)] +/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`]. +/// +/// [`determine_changeset`]: LocalChain::determine_changeset +#[derive(Debug, Default, Clone, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(crate = "serde_crate") +)] pub struct ChangeSet(pub BTreeMap); +impl Deref for ChangeSet { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// Represents an update failure of [`LocalChain`].j #[derive(Clone, Debug, PartialEq)] pub enum UpdateError { From a7eaebbb77f8794c5ff3717aaf0cf73dd5a77480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 31 Mar 2023 22:55:57 +0800 Subject: [PATCH 19/48] [bdk_chain_redesign] Add serde support for `IndexedAdditions` --- crates/chain/src/indexed_tx_graph.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 79e5105c..a3996c13 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -20,6 +20,17 @@ pub struct TxInChain<'a, T, A> { /// A structure that represents changes to an [`IndexedTxGraph`]. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + crate = "serde_crate", + bound( + deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize, IA: serde::Serialize" + ) + ) +)] #[must_use] pub struct IndexedAdditions { /// [`TxGraph`] additions. From 6e59dce10b66212d7180cadabba887cc4d20fc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 5 Apr 2023 10:57:26 +0800 Subject: [PATCH 20/48] [bdk_chain_redesign] `chain_oracle::Cache` Introduce `chain_oracle::Cache` which is a cache for requests to the chain oracle. `ChainOracle` has also been moved to the `chain_oracle` module. Introduce `get_tip_in_best_chain` method to the `ChainOracle` trait. This allows for guaranteeing that chain state can be consistent across operations with `IndexedTxGraph`. --- crates/chain/src/chain_oracle.rs | 162 +++++++++++++++++++++++++++++ crates/chain/src/lib.rs | 2 + crates/chain/src/local_chain.rs | 10 +- crates/chain/src/sparse_chain.rs | 11 +- crates/chain/src/tx_data_traits.rs | 32 ------ crates/chain/src/tx_graph.rs | 5 +- 6 files changed, 186 insertions(+), 36 deletions(-) create mode 100644 crates/chain/src/chain_oracle.rs diff --git a/crates/chain/src/chain_oracle.rs b/crates/chain/src/chain_oracle.rs new file mode 100644 index 00000000..ccf3bc09 --- /dev/null +++ b/crates/chain/src/chain_oracle.rs @@ -0,0 +1,162 @@ +use core::{convert::Infallible, marker::PhantomData}; + +use alloc::collections::BTreeMap; +use bitcoin::BlockHash; + +use crate::BlockId; + +/// Represents a service that tracks the best chain history. +/// TODO: How do we ensure the chain oracle is consistent across a single call? +/// * We need to somehow lock the data! What if the ChainOracle is remote? +/// * Get tip method! And check the tip still exists at the end! And every internal call +/// does not go beyond the initial tip. +pub trait ChainOracle { + /// Error type. + type Error: core::fmt::Debug; + + /// Get the height and hash of the tip in the best chain. + fn get_tip_in_best_chain(&self) -> Result, Self::Error>; + + /// Returns the block hash (if any) of the given `height`. + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error>; + + /// Determines whether the block of [`BlockId`] exists in the best chain. + fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { + Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash)) + } +} + +// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this? +// Box, Arc ????? I will figure it out +impl ChainOracle for &C { + type Error = C::Error; + + fn get_tip_in_best_chain(&self) -> Result, Self::Error> { + ::get_tip_in_best_chain(self) + } + + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { + ::get_block_in_best_chain(self, height) + } + + fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { + ::is_block_in_best_chain(self, block_id) + } +} + +/// This structure increases the performance of getting chain data. +#[derive(Debug)] +pub struct Cache { + assume_final_depth: u32, + tip_height: u32, + cache: BTreeMap, + marker: PhantomData, +} + +impl Cache { + /// Creates a new [`Cache`]. + /// + /// `assume_final_depth` represents the minimum number of blocks above the block in question + /// when we can assume the block is final (reorgs cannot happen). I.e. a value of 0 means the + /// tip is assumed to be final. The cache only caches blocks that are assumed to be final. + pub fn new(assume_final_depth: u32) -> Self { + Self { + assume_final_depth, + tip_height: 0, + cache: Default::default(), + marker: Default::default(), + } + } +} + +impl Cache { + /// This is the topmost (highest) block height that we assume as final (no reorgs possible). + /// + /// Blocks higher than this height are not cached. + pub fn assume_final_height(&self) -> u32 { + self.tip_height.saturating_sub(self.assume_final_depth) + } + + /// Update the `tip_height` with the [`ChainOracle`]'s tip. + /// + /// `tip_height` is used with `assume_final_depth` to determine whether we should cache a + /// certain block height (`tip_height` - `assume_final_depth`). + pub fn try_update_tip_height(&mut self, chain: C) -> Result<(), C::Error> { + let tip = chain.get_tip_in_best_chain()?; + if let Some(BlockId { height, .. }) = tip { + self.tip_height = height; + } + Ok(()) + } + + /// Get a block from the cache with the [`ChainOracle`] as fallback. + /// + /// If the block does not exist in cache, the logic fallbacks to fetching from the internal + /// [`ChainOracle`]. If the block is at or below the "assume final height", we will also store + /// the missing block in the cache. + pub fn try_get_block(&mut self, chain: C, height: u32) -> Result, C::Error> { + if let Some(&hash) = self.cache.get(&height) { + return Ok(Some(hash)); + } + + let hash = chain.get_block_in_best_chain(height)?; + + if hash.is_some() && height > self.tip_height { + self.tip_height = height; + } + + // only cache block if at least as deep as `assume_final_depth` + let assume_final_height = self.tip_height.saturating_sub(self.assume_final_depth); + if height <= assume_final_height { + if let Some(hash) = hash { + self.cache.insert(height, hash); + } + } + + Ok(hash) + } + + /// Determines whether the block of `block_id` is in the chain using the cache. + /// + /// This uses [`try_get_block`] internally. + /// + /// [`try_get_block`]: Self::try_get_block + pub fn try_is_block_in_chain(&mut self, chain: C, block_id: BlockId) -> Result { + match self.try_get_block(chain, block_id.height)? { + Some(hash) if hash == block_id.hash => Ok(true), + _ => Ok(false), + } + } +} + +impl> Cache { + /// Updates the `tip_height` with the [`ChainOracle`]'s tip. + /// + /// This is the no-error version of [`try_update_tip_height`]. + /// + /// [`try_update_tip_height`]: Self::try_update_tip_height + pub fn update_tip_height(&mut self, chain: C) { + self.try_update_tip_height(chain) + .expect("chain oracle error is infallible") + } + + /// Get a block from the cache with the [`ChainOracle`] as fallback. + /// + /// This is the no-error version of [`try_get_block`]. + /// + /// [`try_get_block`]: Self::try_get_block + pub fn get_block(&mut self, chain: C, height: u32) -> Option { + self.try_get_block(chain, height) + .expect("chain oracle error is infallible") + } + + /// Determines whether the block at `block_id` is in the chain using the cache. + /// + /// This is the no-error version of [`try_is_block_in_chain`]. + /// + /// [`try_is_block_in_chain`]: Self::try_is_block_in_chain + pub fn is_block_in_best_chain(&mut self, chain: C, block_id: BlockId) -> bool { + self.try_is_block_in_chain(chain, block_id) + .expect("chain oracle error is infallible") + } +} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 9319d4ac..26527623 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -31,6 +31,8 @@ pub mod sparse_chain; mod tx_data_traits; pub mod tx_graph; pub use tx_data_traits::*; +mod chain_oracle; +pub use chain_oracle::*; #[doc(hidden)] pub mod example_utils; diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 5d459a15..a1ca921b 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -22,6 +22,14 @@ pub struct LocalChain { impl ChainOracle for LocalChain { type Error = Infallible; + fn get_tip_in_best_chain(&self) -> Result, Self::Error> { + Ok(self + .blocks + .iter() + .last() + .map(|(&height, &hash)| BlockId { height, hash })) + } + fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { Ok(self.blocks.get(&height).cloned()) } @@ -153,7 +161,7 @@ impl Deref for ChangeSet { } } -/// Represents an update failure of [`LocalChain`].j +/// Represents an update failure of [`LocalChain`]. #[derive(Clone, Debug, PartialEq)] pub enum UpdateError { /// The update cannot be applied to the chain because the chain suffix it represents did not diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index eb6e3e2a..7f0b67e5 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -307,6 +307,7 @@ //! ); //! ``` use core::{ + convert::Infallible, fmt::Debug, ops::{Bound, RangeBounds}, }; @@ -457,7 +458,15 @@ impl core::fmt::Display for UpdateError

{ impl std::error::Error for UpdateError

{} impl ChainOracle for SparseChain

{ - type Error = (); + type Error = Infallible; + + fn get_tip_in_best_chain(&self) -> Result, Self::Error> { + Ok(self + .checkpoints + .iter() + .last() + .map(|(&height, &hash)| BlockId { height, hash })) + } fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { Ok(self.checkpoint_at(height).map(|b| b.hash)) diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 0e2474c4..366fc34b 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -56,38 +56,6 @@ impl BlockAnchor for (u32, BlockHash) { } } -/// Represents a service that tracks the best chain history. -/// TODO: How do we ensure the chain oracle is consistent across a single call? -/// * We need to somehow lock the data! What if the ChainOracle is remote? -/// * Get tip method! And check the tip still exists at the end! And every internal call -/// does not go beyond the initial tip. -pub trait ChainOracle { - /// Error type. - type Error: core::fmt::Debug; - - /// Returns the block hash (if any) of the given `height`. - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error>; - - /// Determines whether the block of [`BlockId`] exists in the best chain. - fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { - Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash)) - } -} - -// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this? -// Box, Arc ????? I will figure it out -impl ChainOracle for &C { - type Error = C::Error; - - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { - ::get_block_in_best_chain(self, height) - } - - fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { - ::is_block_in_best_chain(self, block_id) - } -} - /// Represents an index of transaction data. pub trait TxIndex { /// The resultant "additions" when new transaction data is indexed. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index bbdd7bdb..893060ae 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -586,11 +586,12 @@ impl TxGraph { impl TxGraph { /// Get all heights that are relevant to the graph. - pub fn relevant_heights(&self) -> BTreeSet { + pub fn relevant_heights(&self) -> impl Iterator + '_ { + let mut visited = HashSet::new(); self.anchors .iter() .map(|(a, _)| a.anchor_block().height) - .collect() + .filter(move |&h| visited.insert(h)) } /// Determines whether a transaction of `txid` is in the best chain. From 89cfa4d78e059f9fe2544b690bbbf90e92b3efee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 5 Apr 2023 16:39:54 +0800 Subject: [PATCH 21/48] [bdk_chain_redesign] Better names, comments and generic bounds * Instead of implementing `ChainPosition` for `ObservedIn` to use `FullTxOut` methods (`is_spendable_at` and `is_mature`), we create alternative versions of those methods that require bounds with `Anchor`. This removes all `ObservedIn: ChainPosition` bounds for methods of `IndexedTxGraph`. * Various improvements to comments and names. --- crates/chain/src/chain_data.rs | 120 ++++++++++++++++----------- crates/chain/src/chain_graph.rs | 6 +- crates/chain/src/indexed_tx_graph.rs | 74 +++++++++-------- crates/chain/src/local_chain.rs | 2 +- crates/chain/src/sparse_chain.rs | 84 ++++++++++--------- crates/chain/src/tx_graph.rs | 19 ++--- 6 files changed, 163 insertions(+), 142 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 6c1c2c3a..85f9107c 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -6,49 +6,21 @@ use crate::{ }; /// Represents an observation of some chain data. +/// +/// The generic `A` should be a [`BlockAnchor`] implementation. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] -pub enum ObservedIn { - /// The chain data is seen in a block identified by `A`. - Block(A), +pub enum ObservedAs { + /// The chain data is seen as confirmed, and in anchored by `A`. + Confirmed(A), /// The chain data is seen in mempool at this given timestamp. - /// TODO: Call this `Unconfirmed`. - Mempool(u64), + Unconfirmed(u64), } -impl ObservedIn<&A> { - pub fn into_owned(self) -> ObservedIn { +impl ObservedAs<&A> { + pub fn cloned(self) -> ObservedAs { match self { - ObservedIn::Block(a) => ObservedIn::Block(a.clone()), - ObservedIn::Mempool(last_seen) => ObservedIn::Mempool(last_seen), - } - } -} - -impl ChainPosition for ObservedIn { - fn height(&self) -> TxHeight { - match self { - ObservedIn::Block(block_id) => TxHeight::Confirmed(block_id.height), - ObservedIn::Mempool(_) => TxHeight::Unconfirmed, - } - } - - fn max_ord_of_height(height: TxHeight) -> Self { - match height { - TxHeight::Confirmed(height) => ObservedIn::Block(BlockId { - height, - hash: Hash::from_inner([u8::MAX; 32]), - }), - TxHeight::Unconfirmed => Self::Mempool(u64::MAX), - } - } - - fn min_ord_of_height(height: TxHeight) -> Self { - match height { - TxHeight::Confirmed(height) => ObservedIn::Block(BlockId { - height, - hash: Hash::from_inner([u8::MIN; 32]), - }), - TxHeight::Unconfirmed => Self::Mempool(u64::MIN), + ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()), + ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen), } } } @@ -217,20 +189,20 @@ impl From<(&u32, &BlockHash)> for BlockId { /// A `TxOut` with as much data as we can retrieve about it #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct FullTxOut { +pub struct FullTxOut

{ /// The location of the `TxOut`. pub outpoint: OutPoint, /// The `TxOut`. pub txout: TxOut, /// The position of the transaction in `outpoint` in the overall chain. - pub chain_position: I, + pub chain_position: P, /// The txid and chain position of the transaction (if any) that has spent this output. - pub spent_by: Option<(I, Txid)>, + pub spent_by: Option<(P, Txid)>, /// Whether this output is on a coinbase transaction. pub is_on_coinbase: bool, } -impl FullTxOut { +impl FullTxOut

{ /// Whether the utxo is/was/will be spendable at `height`. /// /// It is spendable if it is not an immature coinbase output and no spending tx has been @@ -269,15 +241,63 @@ impl FullTxOut { } } -impl FullTxOut> { - pub fn into_owned(self) -> FullTxOut> { - FullTxOut { - outpoint: self.outpoint, - txout: self.txout, - chain_position: self.chain_position.into_owned(), - spent_by: self.spent_by.map(|(o, txid)| (o.into_owned(), txid)), - is_on_coinbase: self.is_on_coinbase, +impl FullTxOut> { + /// Whether the `txout` is considered mature. + /// + /// This is the alternative version of [`is_mature`] which depends on `chain_position` being a + /// [`ObservedAs`] where `A` implements [`BlockAnchor`]. + /// + /// [`is_mature`]: Self::is_mature + pub fn is_observed_as_mature(&self, tip: u32) -> bool { + if !self.is_on_coinbase { + return false; } + + let tx_height = match &self.chain_position { + ObservedAs::Confirmed(anchor) => anchor.anchor_block().height, + ObservedAs::Unconfirmed(_) => { + debug_assert!(false, "coinbase tx can never be unconfirmed"); + return false; + } + }; + + let age = tip.saturating_sub(tx_height); + if age + 1 < COINBASE_MATURITY { + return false; + } + + true + } + + /// Whether the utxo is/was/will be spendable with chain `tip`. + /// + /// This is the alternative version of [`is_spendable_at`] which depends on `chain_position` + /// being a [`ObservedAs`] where `A` implements [`BlockAnchor`]. + /// + /// [`is_spendable_at`]: Self::is_spendable_at + pub fn is_observed_as_spendable(&self, tip: u32) -> bool { + if !self.is_observed_as_mature(tip) { + return false; + } + + match &self.chain_position { + ObservedAs::Confirmed(anchor) => { + if anchor.anchor_block().height > tip { + return false; + } + } + // [TODO] Why are unconfirmed txs always considered unspendable here? + ObservedAs::Unconfirmed(_) => return false, + }; + + // if the spending tx is confirmed within tip height, the txout is no longer spendable + if let Some((ObservedAs::Confirmed(spending_anchor), _)) = &self.spent_by { + if spending_anchor.anchor_block().height <= tip { + return false; + } + } + + true } } diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index 8c954f8d..0e3e3439 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -151,7 +151,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.clone()); + let _ = inflated_graph.insert_tx(tx); } } None => { @@ -212,8 +212,8 @@ where /// the unconfirmed transaction list within the [`SparseChain`]. pub fn get_tx_in_chain(&self, txid: Txid) -> Option<(&P, &Transaction)> { let position = self.chain.tx_position(txid)?; - let tx = self.graph.get_tx(txid).expect("must exist"); - Some((position, tx)) + let full_tx = self.graph.get_tx(txid).expect("must exist"); + Some((position, full_tx)) } /// Determines the changes required to insert a transaction into the inner [`ChainGraph`] and diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index a3996c13..e2d71af1 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -4,16 +4,15 @@ use bitcoin::{OutPoint, Script, Transaction, TxOut}; use crate::{ keychain::Balance, - sparse_chain::ChainPosition, tx_graph::{Additions, TxGraph, TxNode}, - BlockAnchor, ChainOracle, FullTxOut, ObservedIn, TxIndex, + BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex, }; /// An outwards-facing view of a transaction that is part of the *best chain*'s history. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct TxInChain<'a, T, A> { +pub struct CanonicalTx<'a, T, A> { /// Where the transaction is observed (in a block or in mempool). - pub observed_in: ObservedIn<&'a A>, + pub observed_as: ObservedAs<&'a A>, /// The transaction with anchors and last seen timestamp. pub tx: TxNode<'a, T, A>, } @@ -140,19 +139,23 @@ impl IndexedTxGraph { &mut self, outpoint: OutPoint, txout: &TxOut, - observation: ObservedIn, + observation: ObservedAs, ) -> IndexedAdditions { let last_height = match &observation { - ObservedIn::Block(anchor) => self.insert_height_internal(anchor.anchor_block().height), - ObservedIn::Mempool(_) => None, + ObservedAs::Confirmed(anchor) => { + self.insert_height_internal(anchor.anchor_block().height) + } + ObservedAs::Unconfirmed(_) => None, }; IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); graph_additions.append(match observation { - ObservedIn::Block(anchor) => self.graph.insert_anchor(outpoint.txid, anchor), - ObservedIn::Mempool(seen_at) => { + ObservedAs::Confirmed(anchor) => { + self.graph.insert_anchor(outpoint.txid, anchor) + } + ObservedAs::Unconfirmed(seen_at) => { self.graph.insert_seen_at(outpoint.txid, seen_at) } }); @@ -166,21 +169,23 @@ impl IndexedTxGraph { pub fn insert_tx( &mut self, tx: &Transaction, - observation: ObservedIn, + observation: ObservedAs, ) -> IndexedAdditions { let txid = tx.txid(); let last_height = match &observation { - ObservedIn::Block(anchor) => self.insert_height_internal(anchor.anchor_block().height), - ObservedIn::Mempool(_) => None, + ObservedAs::Confirmed(anchor) => { + self.insert_height_internal(anchor.anchor_block().height) + } + ObservedAs::Unconfirmed(_) => None, }; IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_tx(tx.clone()); graph_additions.append(match observation { - ObservedIn::Block(anchor) => self.graph.insert_anchor(txid, anchor), - ObservedIn::Mempool(seen_at) => self.graph.insert_seen_at(txid, seen_at), + ObservedAs::Confirmed(anchor) => self.graph.insert_anchor(txid, anchor), + ObservedAs::Unconfirmed(seen_at) => self.graph.insert_seen_at(txid, seen_at), }); graph_additions }, @@ -192,7 +197,7 @@ impl IndexedTxGraph { pub fn filter_and_insert_txs<'t, T>( &mut self, txs: T, - observation: ObservedIn, + observation: ObservedAs, ) -> IndexedAdditions where T: Iterator, @@ -220,7 +225,7 @@ impl IndexedTxGraph { pub fn try_list_chain_txs<'a, C>( &'a self, chain: C, - ) -> impl Iterator, C::Error>> + ) -> impl Iterator, C::Error>> where C: ChainOracle + 'a, { @@ -230,7 +235,12 @@ impl IndexedTxGraph { .filter_map(move |tx| { self.graph .try_get_chain_position(&chain, tx.txid) - .map(|v| v.map(|observed_in| TxInChain { observed_in, tx })) + .map(|v| { + v.map(|observed_in| CanonicalTx { + observed_as: observed_in, + tx, + }) + }) .transpose() }) } @@ -238,7 +248,7 @@ impl IndexedTxGraph { pub fn list_chain_txs<'a, C>( &'a self, chain: C, - ) -> impl Iterator> + ) -> impl Iterator> where C: ChainOracle + 'a, { @@ -249,10 +259,9 @@ impl IndexedTxGraph { pub fn try_list_chain_txouts<'a, C>( &'a self, chain: C, - ) -> impl Iterator>, C::Error>> + 'a + ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, - ObservedIn: ChainPosition, { self.graph .all_txouts() @@ -263,13 +272,13 @@ impl IndexedTxGraph { let is_on_coinbase = graph_tx.is_coin_base(); let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) { - Ok(Some(observed_at)) => observed_at.into_owned(), + Ok(Some(observed_at)) => observed_at.cloned(), Ok(None) => return None, Err(err) => return Some(Err(err)), }; let spent_by = match self.graph.try_get_spend_in_chain(&chain, op) { - Ok(Some((obs, txid))) => Some((obs.into_owned(), txid)), + Ok(Some((obs, txid))) => Some((obs.cloned(), txid)), Ok(None) => None, Err(err) => return Some(Err(err)), }; @@ -289,10 +298,9 @@ impl IndexedTxGraph { pub fn list_chain_txouts<'a, C>( &'a self, chain: C, - ) -> impl Iterator>> + 'a + ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, - ObservedIn: ChainPosition, { self.try_list_chain_txouts(chain) .map(|r| r.expect("error in infallible")) @@ -302,10 +310,9 @@ impl IndexedTxGraph { pub fn try_list_chain_utxos<'a, C>( &'a self, chain: C, - ) -> impl Iterator>, C::Error>> + 'a + ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, - ObservedIn: ChainPosition, { self.try_list_chain_txouts(chain) .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) @@ -314,10 +321,9 @@ impl IndexedTxGraph { pub fn list_chain_utxos<'a, C>( &'a self, chain: C, - ) -> impl Iterator>> + 'a + ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, - ObservedIn: ChainPosition, { self.try_list_chain_utxos(chain) .map(|r| r.expect("error is infallible")) @@ -331,7 +337,6 @@ impl IndexedTxGraph { ) -> Result where C: ChainOracle, - ObservedIn: ChainPosition + Clone, F: FnMut(&Script) -> bool, { let mut immature = 0; @@ -343,16 +348,16 @@ impl IndexedTxGraph { let txout = res?; match &txout.chain_position { - ObservedIn::Block(_) => { + ObservedAs::Confirmed(_) => { if txout.is_on_coinbase { - if txout.is_mature(tip) { + if txout.is_observed_as_mature(tip) { confirmed += txout.txout.value; } else { immature += txout.txout.value; } } } - ObservedIn::Mempool(_) => { + ObservedAs::Unconfirmed(_) => { if should_trust(&txout.txout.script_pubkey) { trusted_pending += txout.txout.value; } else { @@ -373,7 +378,6 @@ impl IndexedTxGraph { pub fn balance(&self, chain: C, tip: u32, should_trust: F) -> Balance where C: ChainOracle, - ObservedIn: ChainPosition + Clone, F: FnMut(&Script) -> bool, { self.try_balance(chain, tip, should_trust) @@ -383,12 +387,11 @@ impl IndexedTxGraph { pub fn try_balance_at(&self, chain: C, height: u32) -> Result where C: ChainOracle, - ObservedIn: ChainPosition + Clone, { let mut sum = 0; for txo_res in self.try_list_chain_txouts(chain) { let txo = txo_res?; - if txo.is_spendable_at(height) { + if txo.is_observed_as_spendable(height) { sum += txo.txout.value; } } @@ -398,7 +401,6 @@ impl IndexedTxGraph { pub fn balance_at(&self, chain: C, height: u32) -> u64 where C: ChainOracle, - ObservedIn: ChainPosition + Clone, { self.try_balance_at(chain, height) .expect("error is infallible") diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index a1ca921b..fb7d008d 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -123,7 +123,7 @@ impl LocalChain { /// Updates [`LocalChain`] with an update [`LocalChain`]. /// - /// This is equivilant to calling [`determine_changeset`] and [`apply_changeset`] in sequence. + /// This is equivalent to calling [`determine_changeset`] and [`apply_changeset`] in sequence. /// /// [`determine_changeset`]: Self::determine_changeset /// [`apply_changeset`]: Self::apply_changeset diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index 7f0b67e5..b615f4aa 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -457,7 +457,7 @@ impl core::fmt::Display for UpdateError

{ #[cfg(feature = "std")] impl std::error::Error for UpdateError

{} -impl ChainOracle for SparseChain

{ +impl

ChainOracle for SparseChain

{ type Error = Infallible; fn get_tip_in_best_chain(&self) -> Result, Self::Error> { @@ -473,7 +473,7 @@ impl ChainOracle for SparseChain

{ } } -impl SparseChain

{ +impl

SparseChain

{ /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they /// are in the same chain. pub fn from_checkpoints(checkpoints: C) -> Self @@ -504,13 +504,6 @@ impl SparseChain

{ .map(|&hash| BlockId { height, hash }) } - /// Return the [`ChainPosition`] of a `txid`. - /// - /// This returns [`None`] if the transaction does not exist. - pub fn tx_position(&self, txid: Txid) -> Option<&P> { - self.txid_to_pos.get(&txid) - } - /// Return a [`BTreeMap`] of all checkpoints (block hashes by height). pub fn checkpoints(&self) -> &BTreeMap { &self.checkpoints @@ -526,6 +519,47 @@ impl SparseChain

{ .map(|(&height, &hash)| BlockId { height, hash }) } + /// Returns the value set as the checkpoint limit. + /// + /// Refer to [`set_checkpoint_limit`]. + /// + /// [`set_checkpoint_limit`]: Self::set_checkpoint_limit + pub fn checkpoint_limit(&self) -> Option { + self.checkpoint_limit + } + + /// Set the checkpoint limit. + /// + /// The checkpoint limit restricts the number of checkpoints that can be stored in [`Self`]. + /// Oldest checkpoints are pruned first. + pub fn set_checkpoint_limit(&mut self, limit: Option) { + self.checkpoint_limit = limit; + self.prune_checkpoints(); + } + + fn prune_checkpoints(&mut self) -> Option> { + let limit = self.checkpoint_limit?; + + // find the last height to be pruned + let last_height = *self.checkpoints.keys().rev().nth(limit)?; + // first height to be kept + let keep_height = last_height + 1; + + let mut split = self.checkpoints.split_off(&keep_height); + core::mem::swap(&mut self.checkpoints, &mut split); + + Some(split) + } +} + +impl SparseChain

{ + /// Return the [`ChainPosition`] of a `txid`. + /// + /// This returns [`None`] if the transaction does not exist. + pub fn tx_position(&self, txid: Txid) -> Option<&P> { + self.txid_to_pos.get(&txid) + } + /// Preview changes of updating [`Self`] with another chain that connects to it. /// /// If the `update` wishes to introduce confirmed transactions, it must contain a checkpoint @@ -936,24 +970,6 @@ impl SparseChain

{ }) } - /// Returns the value set as the checkpoint limit. - /// - /// Refer to [`set_checkpoint_limit`]. - /// - /// [`set_checkpoint_limit`]: Self::set_checkpoint_limit - pub fn checkpoint_limit(&self) -> Option { - self.checkpoint_limit - } - - /// Set the checkpoint limit. - /// - /// The checkpoint limit restricts the number of checkpoints that can be stored in [`Self`]. - /// Oldest checkpoints are pruned first. - pub fn set_checkpoint_limit(&mut self, limit: Option) { - self.checkpoint_limit = limit; - self.prune_checkpoints(); - } - /// Return [`Txid`]s that would be added to the sparse chain if this `changeset` was applied. pub fn changeset_additions<'a>( &'a self, @@ -969,20 +985,6 @@ impl SparseChain

{ .map(|(&txid, _)| txid) } - fn prune_checkpoints(&mut self) -> Option> { - let limit = self.checkpoint_limit?; - - // find the last height to be pruned - let last_height = *self.checkpoints.keys().rev().nth(limit)?; - // first height to be kept - let keep_height = last_height + 1; - - let mut split = self.checkpoints.split_off(&keep_height); - core::mem::swap(&mut self.checkpoints, &mut split); - - Some(split) - } - /// Finds the transaction in the chain that spends `outpoint`. /// /// [`TxGraph`] is used to provide the spend relationships. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 893060ae..620a2dc3 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,7 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, BlockAnchor, ChainOracle, ForEachTxOut, ObservedIn}; +use crate::{collections::*, BlockAnchor, ChainOracle, ForEachTxOut, ObservedAs}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::{ @@ -91,10 +91,7 @@ impl Default for TxGraph { } } -// pub type InChainTx<'a, T, A> = (ObservedIn<&'a A>, TxInGraph<'a, T, A>); -// pub type InChainTxOut<'a, I, A> = (&'a I, FullTxOut>); - -/// An outward-facing view of a transaction node that resides in a [`TxGraph`]. +/// An outward-facing representation of a (transaction) node in the [`TxGraph`]. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct TxNode<'a, T, A> { /// Txid of the transaction. @@ -601,7 +598,7 @@ impl TxGraph { &self, chain: C, txid: Txid, - ) -> Result>, C::Error> + ) -> Result>, C::Error> where C: ChainOracle, { @@ -614,7 +611,7 @@ impl TxGraph { for anchor in anchors { if chain.is_block_in_best_chain(anchor.anchor_block())? { - return Ok(Some(ObservedIn::Block(anchor))); + return Ok(Some(ObservedAs::Confirmed(anchor))); } } @@ -643,10 +640,10 @@ impl TxGraph { } } - Ok(Some(ObservedIn::Mempool(last_seen))) + Ok(Some(ObservedAs::Unconfirmed(last_seen))) } - pub fn get_chain_position(&self, chain: C, txid: Txid) -> Option> + pub fn get_chain_position(&self, chain: C, txid: Txid) -> Option> where C: ChainOracle, { @@ -658,7 +655,7 @@ impl TxGraph { &self, chain: C, outpoint: OutPoint, - ) -> Result, Txid)>, C::Error> + ) -> Result, Txid)>, C::Error> where C: ChainOracle, { @@ -678,7 +675,7 @@ impl TxGraph { Ok(None) } - pub fn get_chain_spend(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedIn<&A>, Txid)> + pub fn get_chain_spend(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedAs<&A>, Txid)> where C: ChainOracle, { From da4cef044d4a3ad0f44ff1e33936c93c38c2f774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 5 Apr 2023 17:29:20 +0800 Subject: [PATCH 22/48] [bdk_chain_redesign] Introduce `Append` trait for additions Before, we were using `core::ops::AddAsign` but it was not the most appropriate. --- crates/chain/src/indexed_tx_graph.rs | 28 ++++++++----------- crates/chain/src/keychain.rs | 13 ++------- crates/chain/src/keychain/txout_index.rs | 2 ++ crates/chain/src/tx_data_traits.rs | 10 +++++++ .../keychain_tracker_example_cli/src/lib.rs | 2 +- 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index e2d71af1..450d02b8 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,11 +1,11 @@ -use core::{convert::Infallible, ops::AddAssign}; +use core::convert::Infallible; use bitcoin::{OutPoint, Script, Transaction, TxOut}; use crate::{ keychain::Balance, tx_graph::{Additions, TxGraph, TxNode}, - BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex, + Append, BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex, }; /// An outwards-facing view of a transaction that is part of the *best chain*'s history. @@ -50,18 +50,14 @@ impl Default for IndexedAdditions { } } -impl AddAssign for IndexedAdditions { - fn add_assign(&mut self, rhs: Self) { - let Self { - graph_additions, - index_additions: index_delta, - last_height, - } = rhs; - self.graph_additions.append(graph_additions); - self.index_additions += index_delta; - if self.last_height < last_height { - let last_height = - last_height.expect("must exist as it is larger than self.last_height"); +impl Append for IndexedAdditions { + fn append(&mut self, other: Self) { + self.graph_additions.append(other.graph_additions); + self.index_additions.append(other.index_additions); + if self.last_height < other.last_height { + let last_height = other + .last_height + .expect("must exist as it is larger than self.last_height"); self.last_height.replace(last_height); } } @@ -201,7 +197,7 @@ impl IndexedTxGraph { ) -> IndexedAdditions where T: Iterator, - I::Additions: Default + AddAssign, + I::Additions: Default + Append, { txs.filter_map(|tx| { if self.index.is_tx_relevant(tx) { @@ -211,7 +207,7 @@ impl IndexedTxGraph { } }) .fold(IndexedAdditions::default(), |mut acc, other| { - acc += other; + acc.append(other); acc }) } diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 53da284f..81503049 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -14,14 +14,13 @@ //! [`KeychainChangeSet`]s. //! //! [`SpkTxOutIndex`]: crate::SpkTxOutIndex -use core::ops::AddAssign; use crate::{ chain_graph::{self, ChainGraph}, collections::BTreeMap, sparse_chain::ChainPosition, tx_graph::TxGraph, - ForEachTxOut, + Append, ForEachTxOut, }; #[cfg(feature = "miniscript")] @@ -71,12 +70,12 @@ impl DerivationAdditions { } } -impl DerivationAdditions { +impl Append for DerivationAdditions { /// Append another [`DerivationAdditions`] into self. /// /// If the keychain already exists, increase the index when the other's index > self's index. /// If the keychain did not exist, append the new keychain. - pub fn append(&mut self, mut other: Self) { + fn append(&mut self, mut other: Self) { self.0.iter_mut().for_each(|(key, index)| { if let Some(other_index) = other.0.remove(key) { *index = other_index.max(*index); @@ -87,12 +86,6 @@ impl DerivationAdditions { } } -impl AddAssign for DerivationAdditions { - fn add_assign(&mut self, rhs: Self) { - self.append(rhs) - } -} - impl Default for DerivationAdditions { fn default() -> Self { Self(Default::default()) diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index 101278b7..fc4c4e62 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -7,6 +7,8 @@ use alloc::{borrow::Cow, vec::Vec}; use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut}; use core::{fmt::Debug, ops::Deref}; +use crate::Append; + use super::DerivationAdditions; /// Maximum [BIP32](https://bips.xyz/32) derivation index. diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 366fc34b..716b45f1 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -56,6 +56,16 @@ impl BlockAnchor for (u32, BlockHash) { } } +/// Trait that makes an object appendable. +pub trait Append { + /// Append another object of the same type onto `self`. + fn append(&mut self, other: Self); +} + +impl Append for () { + fn append(&mut self, _other: Self) {} +} + /// Represents an index of transaction data. pub trait TxIndex { /// The resultant "additions" when new transaction data is indexed. diff --git a/example-crates/keychain_tracker_example_cli/src/lib.rs b/example-crates/keychain_tracker_example_cli/src/lib.rs index df42df1a..702cc2a2 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, + Append, DescriptorExt, FullTxOut, }; use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue}; use bdk_file_store::KeychainStore; From ddd5e951f5ec77070034c7390a635d8d5bd7cb85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 5 Apr 2023 18:17:08 +0800 Subject: [PATCH 23/48] [bdk_chain_redesign] Modify signature of `TxIndex` This makes the API of `TxIndex` more consistent between scanning in data and checking whether certain data is relevant. --- crates/chain/src/indexed_tx_graph.rs | 2 +- crates/chain/src/keychain/txout_index.rs | 8 ++++---- crates/chain/src/spk_txout_index.rs | 8 ++++---- crates/chain/src/tx_data_traits.rs | 11 +++++------ crates/chain/src/tx_graph.rs | 2 +- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 450d02b8..28b95adf 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -261,7 +261,7 @@ impl IndexedTxGraph { { self.graph .all_txouts() - .filter(|(_, txo)| self.index.is_spk_owned(&txo.script_pubkey)) + .filter(|&(op, txo)| self.index.is_txout_relevant(op, txo)) .filter_map(move |(op, txout)| -> Option> { let graph_tx = self.graph.get_tx(op.txid)?; diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index fc4c4e62..7dd570a6 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -105,12 +105,12 @@ impl TxIndex for KeychainTxOutIndex { self.apply_additions(additions) } - fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { - self.is_relevant(tx) + fn is_txout_relevant(&self, _outpoint: OutPoint, txout: &TxOut) -> bool { + self.index_of_spk(&txout.script_pubkey).is_some() } - fn is_spk_owned(&self, spk: &Script) -> bool { - self.index_of_spk(spk).is_some() + fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { + self.is_relevant(tx) } } diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 6c9739be..20be073a 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -69,12 +69,12 @@ impl TxIndex for SpkTxOutIndex { // This applies nothing. } - fn is_tx_relevant(&self, tx: &Transaction) -> bool { - self.is_relevant(tx) + fn is_txout_relevant(&self, _outpoint: OutPoint, txout: &TxOut) -> bool { + self.index_of_spk(&txout.script_pubkey).is_some() } - fn is_spk_owned(&self, spk: &Script) -> bool { - self.index_of_spk(spk).is_some() + fn is_tx_relevant(&self, tx: &Transaction) -> bool { + self.is_relevant(tx) } } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 716b45f1..d8cadd13 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,4 +1,4 @@ -use bitcoin::{Block, BlockHash, OutPoint, Script, Transaction, TxOut}; +use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; use crate::BlockId; @@ -80,10 +80,9 @@ pub trait TxIndex { /// Apply additions to itself. fn apply_additions(&mut self, additions: Self::Additions); - /// A transaction is relevant if it contains a txout with a script_pubkey that we own, or if it - /// spends an already-indexed outpoint that we have previously indexed. - fn is_tx_relevant(&self, tx: &Transaction) -> bool; + /// Returns whether the txout is marked as relevant in the index. + fn is_txout_relevant(&self, outpoint: OutPoint, txout: &TxOut) -> bool; - /// Returns whether the script pubkey is owned by us. - fn is_spk_owned(&self, spk: &Script) -> bool; + /// Returns whether the transaction is marked as relevant in the index. + fn is_tx_relevant(&self, tx: &Transaction) -> bool; } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 620a2dc3..c502d038 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -41,7 +41,7 @@ //! # 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 = TxGraph::default(); //! let update = TxGraph::new(vec![tx_a, tx_b]); //! //! // preview additions as the result of the update From 24cd8c5cc7f3a6bd0db2bd45642f08a28ea5337a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 5 Apr 2023 19:13:42 +0800 Subject: [PATCH 24/48] [bdk_chain_redesign] More tweaks and renamings --- crates/chain/src/indexed_tx_graph.rs | 2 +- crates/chain/src/local_chain.rs | 10 +++++----- crates/chain/src/tx_graph.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 28b95adf..f50f454b 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -190,7 +190,7 @@ impl IndexedTxGraph { } } - pub fn filter_and_insert_txs<'t, T>( + pub fn insert_relevant_txs<'t, T>( &mut self, txs: T, observation: ObservedAs, diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index fb7d008d..e1b24ad0 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -82,16 +82,16 @@ impl LocalChain { }; // the first block's height to invalidate in the local chain - let invalidate_from = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h); + let invalidate_from_height = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h); // the first block of height to invalidate (if any) should be represented in the update - if let Some(first_invalid) = invalidate_from { - if !update.contains_key(&first_invalid) { - return Err(UpdateError::NotConnected(first_invalid)); + if let Some(first_invalid_height) = invalidate_from_height { + if !update.contains_key(&first_invalid_height) { + return Err(UpdateError::NotConnected(first_invalid_height)); } } - let invalidated_heights = invalidate_from + let invalidated_heights = invalidate_from_height .into_iter() .flat_map(|from_height| self.blocks.range(from_height..).map(|(h, _)| h)); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index c502d038..0959456d 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -21,7 +21,7 @@ //! # 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 = TxGraph::default(); //! //! // preview a transaction insertion (not actually inserted) //! let additions = graph.insert_tx_preview(tx_a); From bff80ec378fab29556099f9830bcb42911658710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 7 Apr 2023 09:23:00 +0800 Subject: [PATCH 25/48] [bdk_chain_redesign] Improve `BlockAnchor` docs --- crates/chain/src/tx_data_traits.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index d8cadd13..1399ebeb 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -34,9 +34,12 @@ impl ForEachTxOut for Transaction { } } -/// Trait that "anchors" blockchain data in a specific block of height and hash. +/// Trait that "anchors" blockchain data to a specific block of height and hash. /// -/// This trait is typically associated with blockchain data such as transactions. +/// I.e. If transaction A is anchored in block B, then if block B is in the best chain, we can +/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean +/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a +/// parent block of B. pub trait BlockAnchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash + Send + Sync + 'static { From 611d2e3ea2ed9249ddf04e0f9089642160e5c901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 10 Apr 2023 13:03:51 +0800 Subject: [PATCH 26/48] [bdk_chain_redesign] Consistent `ChainOracle` The problem with the previous `ChainOracle` interface is that it had no guarantee for consistency. For example, a block deemed to be part of the "best chain" can be reorged out. So when `ChainOracle` is called multiple times for an operation (such as getting the UTXO set), the returned result may be inconsistent. This PR changes `ChainOracle::is_block_in_chain` to take in another input `static_block`, ensuring `block` is an ancestor of `static_block`. Thus, if `static_block` is consistent across the operation, the result will be consistent also. `is_block_in_chain` now returns `Option`. The `None` case means that the oracle implementation cannot determine whether block is an ancestor of static block. `IndexedTxGraph::list_chain_txouts` handles this case by checking child spends that are in chain, and if so, the parent tx must be in chain too. --- crates/chain/src/chain_data.rs | 6 +- crates/chain/src/chain_oracle.rs | 203 ++++++++------------------- crates/chain/src/indexed_tx_graph.rs | 78 ++++++---- crates/chain/src/local_chain.rs | 29 ++-- crates/chain/src/sparse_chain.rs | 24 ++-- crates/chain/src/tx_graph.rs | 61 ++++++-- 6 files changed, 191 insertions(+), 210 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 85f9107c..5615b094 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -248,7 +248,7 @@ impl FullTxOut> { /// [`ObservedAs`] where `A` implements [`BlockAnchor`]. /// /// [`is_mature`]: Self::is_mature - pub fn is_observed_as_mature(&self, tip: u32) -> bool { + pub fn is_observed_as_confirmed_and_mature(&self, tip: u32) -> bool { if !self.is_on_coinbase { return false; } @@ -275,8 +275,8 @@ impl FullTxOut> { /// being a [`ObservedAs`] where `A` implements [`BlockAnchor`]. /// /// [`is_spendable_at`]: Self::is_spendable_at - pub fn is_observed_as_spendable(&self, tip: u32) -> bool { - if !self.is_observed_as_mature(tip) { + pub fn is_observed_as_confirmed_and_spendable(&self, tip: u32) -> bool { + if !self.is_observed_as_confirmed_and_mature(tip) { return false; } diff --git a/crates/chain/src/chain_oracle.rs b/crates/chain/src/chain_oracle.rs index ccf3bc09..7e975ad2 100644 --- a/crates/chain/src/chain_oracle.rs +++ b/crates/chain/src/chain_oracle.rs @@ -1,162 +1,77 @@ -use core::{convert::Infallible, marker::PhantomData}; +use crate::collections::HashSet; +use core::marker::PhantomData; -use alloc::collections::BTreeMap; +use alloc::{collections::VecDeque, vec::Vec}; use bitcoin::BlockHash; use crate::BlockId; -/// Represents a service that tracks the best chain history. -/// TODO: How do we ensure the chain oracle is consistent across a single call? -/// * We need to somehow lock the data! What if the ChainOracle is remote? -/// * Get tip method! And check the tip still exists at the end! And every internal call -/// does not go beyond the initial tip. +/// Represents a service that tracks the blockchain. +/// +/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`] +/// is an ancestor of another "static block". +/// +/// [`is_block_in_chain`]: Self::is_block_in_chain pub trait ChainOracle { /// Error type. type Error: core::fmt::Debug; - /// Get the height and hash of the tip in the best chain. - fn get_tip_in_best_chain(&self) -> Result, Self::Error>; - - /// Returns the block hash (if any) of the given `height`. - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error>; - - /// Determines whether the block of [`BlockId`] exists in the best chain. - fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { - Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash)) - } + /// Determines whether `block` of [`BlockId`] exists as an ancestor of `static_block`. + /// + /// If `None` is returned, it means the implementation cannot determine whether `block` exists. + fn is_block_in_chain( + &self, + block: BlockId, + static_block: BlockId, + ) -> Result, Self::Error>; } -// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this? -// Box, Arc ????? I will figure it out -impl ChainOracle for &C { - type Error = C::Error; - - fn get_tip_in_best_chain(&self) -> Result, Self::Error> { - ::get_tip_in_best_chain(self) - } - - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { - ::get_block_in_best_chain(self, height) - } - - fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { - ::is_block_in_best_chain(self, block_id) - } -} - -/// This structure increases the performance of getting chain data. -#[derive(Debug)] -pub struct Cache { - assume_final_depth: u32, - tip_height: u32, - cache: BTreeMap, +/// A cache structure increases the performance of getting chain data. +/// +/// A simple FIFO cache replacement policy is used. Something more efficient and advanced can be +/// implemented later. +#[derive(Debug, Default)] +pub struct CacheBackend { + cache: HashSet<(BlockHash, BlockHash)>, + fifo: VecDeque<(BlockHash, BlockHash)>, marker: PhantomData, } -impl Cache { - /// Creates a new [`Cache`]. +impl CacheBackend { + /// Get the number of elements in the cache. + pub fn cache_size(&self) -> usize { + self.cache.len() + } + + /// Prunes the cache to reach the `max_size` target. /// - /// `assume_final_depth` represents the minimum number of blocks above the block in question - /// when we can assume the block is final (reorgs cannot happen). I.e. a value of 0 means the - /// tip is assumed to be final. The cache only caches blocks that are assumed to be final. - pub fn new(assume_final_depth: u32) -> Self { - Self { - assume_final_depth, - tip_height: 0, - cache: Default::default(), - marker: Default::default(), + /// Returns pruned elements. + pub fn prune(&mut self, max_size: usize) -> Vec<(BlockHash, BlockHash)> { + let prune_count = self.cache.len().saturating_sub(max_size); + (0..prune_count) + .filter_map(|_| self.fifo.pop_front()) + .filter(|k| self.cache.remove(k)) + .collect() + } + + pub fn contains(&self, static_block: BlockId, block: BlockId) -> bool { + if static_block.height < block.height + || static_block.height == block.height && static_block.hash != block.hash + { + return false; + } + + self.cache.contains(&(static_block.hash, block.hash)) + } + + pub fn insert(&mut self, static_block: BlockId, block: BlockId) -> bool { + let cache_key = (static_block.hash, block.hash); + + if self.cache.insert(cache_key) { + self.fifo.push_back(cache_key); + true + } else { + false } } } - -impl Cache { - /// This is the topmost (highest) block height that we assume as final (no reorgs possible). - /// - /// Blocks higher than this height are not cached. - pub fn assume_final_height(&self) -> u32 { - self.tip_height.saturating_sub(self.assume_final_depth) - } - - /// Update the `tip_height` with the [`ChainOracle`]'s tip. - /// - /// `tip_height` is used with `assume_final_depth` to determine whether we should cache a - /// certain block height (`tip_height` - `assume_final_depth`). - pub fn try_update_tip_height(&mut self, chain: C) -> Result<(), C::Error> { - let tip = chain.get_tip_in_best_chain()?; - if let Some(BlockId { height, .. }) = tip { - self.tip_height = height; - } - Ok(()) - } - - /// Get a block from the cache with the [`ChainOracle`] as fallback. - /// - /// If the block does not exist in cache, the logic fallbacks to fetching from the internal - /// [`ChainOracle`]. If the block is at or below the "assume final height", we will also store - /// the missing block in the cache. - pub fn try_get_block(&mut self, chain: C, height: u32) -> Result, C::Error> { - if let Some(&hash) = self.cache.get(&height) { - return Ok(Some(hash)); - } - - let hash = chain.get_block_in_best_chain(height)?; - - if hash.is_some() && height > self.tip_height { - self.tip_height = height; - } - - // only cache block if at least as deep as `assume_final_depth` - let assume_final_height = self.tip_height.saturating_sub(self.assume_final_depth); - if height <= assume_final_height { - if let Some(hash) = hash { - self.cache.insert(height, hash); - } - } - - Ok(hash) - } - - /// Determines whether the block of `block_id` is in the chain using the cache. - /// - /// This uses [`try_get_block`] internally. - /// - /// [`try_get_block`]: Self::try_get_block - pub fn try_is_block_in_chain(&mut self, chain: C, block_id: BlockId) -> Result { - match self.try_get_block(chain, block_id.height)? { - Some(hash) if hash == block_id.hash => Ok(true), - _ => Ok(false), - } - } -} - -impl> Cache { - /// Updates the `tip_height` with the [`ChainOracle`]'s tip. - /// - /// This is the no-error version of [`try_update_tip_height`]. - /// - /// [`try_update_tip_height`]: Self::try_update_tip_height - pub fn update_tip_height(&mut self, chain: C) { - self.try_update_tip_height(chain) - .expect("chain oracle error is infallible") - } - - /// Get a block from the cache with the [`ChainOracle`] as fallback. - /// - /// This is the no-error version of [`try_get_block`]. - /// - /// [`try_get_block`]: Self::try_get_block - pub fn get_block(&mut self, chain: C, height: u32) -> Option { - self.try_get_block(chain, height) - .expect("chain oracle error is infallible") - } - - /// Determines whether the block at `block_id` is in the chain using the cache. - /// - /// This is the no-error version of [`try_is_block_in_chain`]. - /// - /// [`try_is_block_in_chain`]: Self::try_is_block_in_chain - pub fn is_block_in_best_chain(&mut self, chain: C, block_id: BlockId) -> bool { - self.try_is_block_in_chain(chain, block_id) - .expect("chain oracle error is infallible") - } -} diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index f50f454b..dac05e72 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -5,7 +5,7 @@ use bitcoin::{OutPoint, Script, Transaction, TxOut}; use crate::{ keychain::Balance, tx_graph::{Additions, TxGraph, TxNode}, - Append, BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex, + Append, BlockAnchor, BlockId, ChainOracle, FullTxOut, ObservedAs, TxIndex, }; /// An outwards-facing view of a transaction that is part of the *best chain*'s history. @@ -220,7 +220,8 @@ impl IndexedTxGraph { // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`. pub fn try_list_chain_txs<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator, C::Error>> where C: ChainOracle + 'a, @@ -230,7 +231,7 @@ impl IndexedTxGraph { .filter(|tx| self.index.is_tx_relevant(tx)) .filter_map(move |tx| { self.graph - .try_get_chain_position(&chain, tx.txid) + .try_get_chain_position(chain, static_block, tx.txid) .map(|v| { v.map(|observed_in| CanonicalTx { observed_as: observed_in, @@ -243,18 +244,20 @@ impl IndexedTxGraph { pub fn list_chain_txs<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator> where C: ChainOracle + 'a, { - self.try_list_chain_txs(chain) + self.try_list_chain_txs(chain, static_block) .map(|r| r.expect("error is infallible")) } pub fn try_list_chain_txouts<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, @@ -267,13 +270,17 @@ impl IndexedTxGraph { let is_on_coinbase = graph_tx.is_coin_base(); - let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) { - Ok(Some(observed_at)) => observed_at.cloned(), - Ok(None) => return None, - Err(err) => return Some(Err(err)), - }; + let chain_position = + match self + .graph + .try_get_chain_position(chain, static_block, op.txid) + { + Ok(Some(observed_at)) => observed_at.cloned(), + Ok(None) => return None, + Err(err) => return Some(Err(err)), + }; - let spent_by = match self.graph.try_get_spend_in_chain(&chain, op) { + let spent_by = match self.graph.try_get_spend_in_chain(chain, static_block, op) { Ok(Some((obs, txid))) => Some((obs.cloned(), txid)), Ok(None) => None, Err(err) => return Some(Err(err)), @@ -293,41 +300,45 @@ impl IndexedTxGraph { pub fn list_chain_txouts<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, { - self.try_list_chain_txouts(chain) + self.try_list_chain_txouts(chain, static_block) .map(|r| r.expect("error in infallible")) } /// Return relevant unspents. pub fn try_list_chain_utxos<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, { - self.try_list_chain_txouts(chain) + self.try_list_chain_txouts(chain, static_block) .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) } pub fn list_chain_utxos<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, { - self.try_list_chain_utxos(chain) + self.try_list_chain_utxos(chain, static_block) .map(|r| r.expect("error is infallible")) } pub fn try_balance( &self, - chain: C, + chain: &C, + static_block: BlockId, tip: u32, mut should_trust: F, ) -> Result @@ -340,13 +351,13 @@ impl IndexedTxGraph { let mut untrusted_pending = 0; let mut confirmed = 0; - for res in self.try_list_chain_txouts(&chain) { + for res in self.try_list_chain_txouts(chain, static_block) { let txout = res?; match &txout.chain_position { ObservedAs::Confirmed(_) => { if txout.is_on_coinbase { - if txout.is_observed_as_mature(tip) { + if txout.is_observed_as_confirmed_and_mature(tip) { confirmed += txout.txout.value; } else { immature += txout.txout.value; @@ -371,34 +382,45 @@ impl IndexedTxGraph { }) } - pub fn balance(&self, chain: C, tip: u32, should_trust: F) -> Balance + pub fn balance( + &self, + chain: &C, + static_block: BlockId, + tip: u32, + should_trust: F, + ) -> Balance where C: ChainOracle, F: FnMut(&Script) -> bool, { - self.try_balance(chain, tip, should_trust) + self.try_balance(chain, static_block, tip, should_trust) .expect("error is infallible") } - pub fn try_balance_at(&self, chain: C, height: u32) -> Result + pub fn try_balance_at( + &self, + chain: &C, + static_block: BlockId, + height: u32, + ) -> Result where C: ChainOracle, { let mut sum = 0; - for txo_res in self.try_list_chain_txouts(chain) { + for txo_res in self.try_list_chain_txouts(chain, static_block) { let txo = txo_res?; - if txo.is_observed_as_spendable(height) { + if txo.is_observed_as_confirmed_and_spendable(height) { sum += txo.txout.value; } } Ok(sum) } - pub fn balance_at(&self, chain: C, height: u32) -> u64 + pub fn balance_at(&self, chain: &C, static_block: BlockId, height: u32) -> u64 where C: ChainOracle, { - self.try_balance_at(chain, height) + self.try_balance_at(chain, static_block, height) .expect("error is infallible") } } diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index e1b24ad0..20b54a2f 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -22,16 +22,25 @@ pub struct LocalChain { impl ChainOracle for LocalChain { type Error = Infallible; - fn get_tip_in_best_chain(&self) -> Result, Self::Error> { - Ok(self - .blocks - .iter() - .last() - .map(|(&height, &hash)| BlockId { height, hash })) - } - - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { - Ok(self.blocks.get(&height).cloned()) + fn is_block_in_chain( + &self, + block: BlockId, + static_block: BlockId, + ) -> Result, Self::Error> { + if block.height > static_block.height { + return Ok(None); + } + Ok( + match ( + self.blocks.get(&block.height), + self.blocks.get(&static_block.height), + ) { + (Some(&hash), Some(&static_hash)) => { + Some(hash == block.hash && static_hash == static_block.hash) + } + _ => None, + }, + ) } } diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index b615f4aa..acc61601 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -460,16 +460,20 @@ impl std::error::Error for UpdateError

{} impl

ChainOracle for SparseChain

{ type Error = Infallible; - fn get_tip_in_best_chain(&self) -> Result, Self::Error> { - Ok(self - .checkpoints - .iter() - .last() - .map(|(&height, &hash)| BlockId { height, hash })) - } - - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { - Ok(self.checkpoint_at(height).map(|b| b.hash)) + fn is_block_in_chain( + &self, + block: BlockId, + static_block: BlockId, + ) -> Result, Self::Error> { + Ok( + match ( + self.checkpoint_at(block.height), + self.checkpoint_at(static_block.height), + ) { + (Some(b), Some(static_b)) => Some(b == block && static_b == static_block), + _ => None, + }, + ) } } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 0959456d..e3afce0e 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,7 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, BlockAnchor, ChainOracle, ForEachTxOut, ObservedAs}; +use crate::{collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, ObservedAs}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::{ @@ -596,7 +596,8 @@ impl TxGraph { /// TODO: Also return conflicting tx list, ordered by last_seen. pub fn try_get_chain_position( &self, - chain: C, + chain: &C, + static_block: BlockId, txid: Txid, ) -> Result>, C::Error> where @@ -610,8 +611,28 @@ impl TxGraph { }; for anchor in anchors { - if chain.is_block_in_best_chain(anchor.anchor_block())? { - return Ok(Some(ObservedAs::Confirmed(anchor))); + match chain.is_block_in_chain(anchor.anchor_block(), static_block)? { + Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), + Some(false) => continue, + // if we cannot determine whether block is in the best chain, we can check whether + // a spending transaction is confirmed in best chain, and if so, it is guaranteed + // that the tx being spent (this tx) is in the best chain + None => { + let spending_anchors = self + .spends + .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) + .flat_map(|(_, spending_txids)| spending_txids) + .filter_map(|spending_txid| self.txs.get(spending_txid)) + .flat_map(|(_, spending_anchors, _)| spending_anchors); + for spending_anchor in spending_anchors { + match chain + .is_block_in_chain(spending_anchor.anchor_block(), static_block)? + { + Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), + _ => continue, + } + } + } } } @@ -620,8 +641,7 @@ impl TxGraph { let tx = match tx_node { TxNodeInternal::Whole(tx) => tx, TxNodeInternal::Partial(_) => { - // [TODO] Unfortunately, we can't iterate over conflicts of partial txs right now! - // [TODO] So we just assume the partial tx does not exist in the best chain :/ + // Partial transactions (outputs only) cannot have conflicts. return Ok(None); } }; @@ -629,8 +649,8 @@ impl TxGraph { // If a conflicting tx is in the best chain, or has `last_seen` higher than this tx, then // this tx cannot exist in the best chain for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) { - for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) { - if chain.is_block_in_best_chain(block_id)? { + for block in conflicting_tx.anchors.iter().map(A::anchor_block) { + if chain.is_block_in_chain(block, static_block)? == Some(true) { // conflicting tx is in best chain, so the current tx cannot be in best chain! return Ok(None); } @@ -643,31 +663,37 @@ impl TxGraph { Ok(Some(ObservedAs::Unconfirmed(last_seen))) } - pub fn get_chain_position(&self, chain: C, txid: Txid) -> Option> + pub fn get_chain_position( + &self, + chain: &C, + static_block: BlockId, + txid: Txid, + ) -> Option> where C: ChainOracle, { - self.try_get_chain_position(chain, txid) + self.try_get_chain_position(chain, static_block, txid) .expect("error is infallible") } pub fn try_get_spend_in_chain( &self, - chain: C, + chain: &C, + static_block: BlockId, outpoint: OutPoint, ) -> Result, Txid)>, C::Error> where C: ChainOracle, { if self - .try_get_chain_position(&chain, outpoint.txid)? + .try_get_chain_position(chain, static_block, outpoint.txid)? .is_none() { return Ok(None); } if let Some(spends) = self.spends.get(&outpoint) { for &txid in spends { - if let Some(observed_at) = self.try_get_chain_position(&chain, txid)? { + if let Some(observed_at) = self.try_get_chain_position(chain, static_block, txid)? { return Ok(Some((observed_at, txid))); } } @@ -675,11 +701,16 @@ impl TxGraph { Ok(None) } - pub fn get_chain_spend(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedAs<&A>, Txid)> + pub fn get_chain_spend( + &self, + chain: &C, + static_block: BlockId, + outpoint: OutPoint, + ) -> Option<(ObservedAs<&A>, Txid)> where C: ChainOracle, { - self.try_get_spend_in_chain(chain, outpoint) + self.try_get_spend_in_chain(chain, static_block, outpoint) .expect("error is infallible") } } From ee1060f2ff168e6aaffa41882be2b319729f7de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 10 Apr 2023 15:04:20 +0800 Subject: [PATCH 27/48] [bdk_chain_redesign] Simplify `LocalChain` Remove the requirement that evicted blocks should have in-best-chain counterparts in the update. --- crates/chain/src/local_chain.rs | 91 ++++++++++++++------------------- 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 20b54a2f..58cf923b 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -1,12 +1,9 @@ use core::{convert::Infallible, ops::Deref}; -use alloc::{ - collections::{BTreeMap, BTreeSet}, - vec::Vec, -}; +use alloc::collections::{BTreeMap, BTreeSet}; use bitcoin::BlockHash; -use crate::{BlockId, ChainOracle}; +use crate::{Append, BlockId, ChainOracle}; #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct LocalChain { @@ -56,6 +53,12 @@ impl From for BTreeMap { } } +impl From> for LocalChain { + fn from(value: BTreeMap) -> Self { + Self { blocks: value } + } +} + impl LocalChain { pub fn tip(&self) -> Option { self.blocks @@ -66,10 +69,7 @@ impl LocalChain { /// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights /// are to be re-filled. - pub fn determine_changeset(&self, update: &U) -> Result - where - U: AsRef>, - { + pub fn determine_changeset(&self, update: &Self) -> Result { let update = update.as_ref(); let update_tip = match update.keys().last().cloned() { Some(tip) => tip, @@ -96,25 +96,10 @@ impl LocalChain { // the first block of height to invalidate (if any) should be represented in the update if let Some(first_invalid_height) = invalidate_from_height { if !update.contains_key(&first_invalid_height) { - return Err(UpdateError::NotConnected(first_invalid_height)); + return Err(UpdateNotConnectedError(first_invalid_height)); } } - let invalidated_heights = invalidate_from_height - .into_iter() - .flat_map(|from_height| self.blocks.range(from_height..).map(|(h, _)| h)); - - // invalidated heights must all exist in the update - let mut missing_heights = Vec::::new(); - for invalidated_height in invalidated_heights { - if !update.contains_key(invalidated_height) { - missing_heights.push(*invalidated_height); - } - } - if !missing_heights.is_empty() { - return Err(UpdateError::MissingHeightsInUpdate(missing_heights)); - } - let mut changeset = BTreeMap::::new(); for (height, new_hash) in update { let original_hash = self.blocks.get(height); @@ -136,7 +121,7 @@ impl LocalChain { /// /// [`determine_changeset`]: Self::determine_changeset /// [`apply_changeset`]: Self::apply_changeset - pub fn apply_update(&mut self, update: Self) -> Result { + pub fn apply_update(&mut self, update: Self) -> Result { let changeset = self.determine_changeset(&update)?; self.apply_changeset(changeset.clone()); Ok(changeset) @@ -160,7 +145,7 @@ impl LocalChain { derive(serde::Deserialize, serde::Serialize), serde(crate = "serde_crate") )] -pub struct ChangeSet(pub BTreeMap); +pub struct ChangeSet(pub(crate) BTreeMap); impl Deref for ChangeSet { type Target = BTreeMap; @@ -170,32 +155,30 @@ impl Deref for ChangeSet { } } -/// Represents an update failure of [`LocalChain`]. -#[derive(Clone, Debug, PartialEq)] -pub enum UpdateError { - /// The update cannot be applied to the chain because the chain suffix it represents did not - /// connect to the existing chain. This error case contains the checkpoint height to include so - /// that the chains can connect. - NotConnected(u32), - /// If the update results in displacements of original blocks, the update should include all new - /// block hashes that have displaced the original block hashes. This error case contains the - /// heights of all missing block hashes in the update. - MissingHeightsInUpdate(Vec), -} - -impl core::fmt::Display for UpdateError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - UpdateError::NotConnected(heights) => write!( - f, - "the update cannot connect with the chain, try include blockhash at height {}", - heights - ), - UpdateError::MissingHeightsInUpdate(missing_heights) => write!( - f, - "block hashes of these heights must be included in the update to succeed: {:?}", - missing_heights - ), - } +impl Append for ChangeSet { + fn append(&mut self, mut other: Self) { + BTreeMap::append(&mut self.0, &mut other.0) } } + +/// Represents an update failure of [`LocalChain`] due to the update not connecting to the original +/// chain. +/// +/// The update cannot be applied to the chain because the chain suffix it represents did not +/// connect to the existing chain. This error case contains the checkpoint height to include so +/// that the chains can connect. +#[derive(Clone, Debug, PartialEq)] +pub struct UpdateNotConnectedError(u32); + +impl core::fmt::Display for UpdateNotConnectedError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "the update cannot connect with the chain, try include block at height {}", + self.0 + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for UpdateNotConnectedError {} From a7fbe0ac672cde6c308737fc98020f6693071a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 10 Apr 2023 16:23:10 +0800 Subject: [PATCH 28/48] [bdk_chain_redesign] Documentation improvements --- crates/chain/src/chain_data.rs | 3 ++- crates/chain/src/indexed_tx_graph.rs | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 5615b094..aa1e74d4 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -271,6 +271,8 @@ impl FullTxOut> { /// Whether the utxo is/was/will be spendable with chain `tip`. /// + /// Currently this method does not take into account the locktime. + /// /// This is the alternative version of [`is_spendable_at`] which depends on `chain_position` /// being a [`ObservedAs`] where `A` implements [`BlockAnchor`]. /// @@ -286,7 +288,6 @@ impl FullTxOut> { return false; } } - // [TODO] Why are unconfirmed txs always considered unspendable here? ObservedAs::Unconfirmed(_) => return false, }; diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index dac05e72..2de8114a 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -55,10 +55,11 @@ impl Append for IndexedAdditions { self.graph_additions.append(other.graph_additions); self.index_additions.append(other.index_additions); if self.last_height < other.last_height { - let last_height = other - .last_height - .expect("must exist as it is larger than self.last_height"); - self.last_height.replace(last_height); + self.last_height = Some( + other + .last_height + .expect("must exist as it is larger than self.last_height"), + ); } } } From 7d92337b932fdcfec7008da8ed81f2b4b6e7a069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 10 Apr 2023 16:51:16 +0800 Subject: [PATCH 29/48] [bdk_chain_redesign] Remove `IndexedTxGraph::last_height` It is better to have this external to this structure. --- crates/chain/src/indexed_tx_graph.rs | 58 ---------------------------- 1 file changed, 58 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 2de8114a..574afc1b 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -36,8 +36,6 @@ pub struct IndexedAdditions { pub graph_additions: Additions, /// [`TxIndex`] additions. pub index_additions: IA, - /// Last block height witnessed (if any). - pub last_height: Option, } impl Default for IndexedAdditions { @@ -45,7 +43,6 @@ impl Default for IndexedAdditions { Self { graph_additions: Default::default(), index_additions: Default::default(), - last_height: None, } } } @@ -54,13 +51,6 @@ impl Append for IndexedAdditions { fn append(&mut self, other: Self) { self.graph_additions.append(other.graph_additions); self.index_additions.append(other.index_additions); - if self.last_height < other.last_height { - self.last_height = Some( - other - .last_height - .expect("must exist as it is larger than self.last_height"), - ); - } } } @@ -68,7 +58,6 @@ pub struct IndexedTxGraph { /// Transaction index. pub index: I, graph: TxGraph, - last_height: u32, } impl Default for IndexedTxGraph { @@ -76,7 +65,6 @@ impl Default for IndexedTxGraph { Self { graph: Default::default(), index: Default::default(), - last_height: u32::MIN, } } } @@ -92,7 +80,6 @@ impl IndexedTxGraph { let IndexedAdditions { graph_additions, index_additions, - last_height, } = additions; self.index.apply_additions(index_additions); @@ -105,30 +92,6 @@ impl IndexedTxGraph { } self.graph.apply_additions(graph_additions); - - if let Some(height) = last_height { - self.last_height = height; - } - } - - fn insert_height_internal(&mut self, tip: u32) -> Option { - if self.last_height < tip { - self.last_height = tip; - Some(tip) - } else { - None - } - } - - /// Insert a block height that the chain source has scanned up to. - pub fn insert_height(&mut self, tip: u32) -> IndexedAdditions - where - I::Additions: Default, - { - IndexedAdditions { - last_height: self.insert_height_internal(tip), - ..Default::default() - } } /// Insert a `txout` that exists in `outpoint` with the given `observation`. @@ -138,13 +101,6 @@ impl IndexedTxGraph { txout: &TxOut, observation: ObservedAs, ) -> IndexedAdditions { - let last_height = match &observation { - ObservedAs::Confirmed(anchor) => { - self.insert_height_internal(anchor.anchor_block().height) - } - ObservedAs::Unconfirmed(_) => None, - }; - IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); @@ -159,7 +115,6 @@ impl IndexedTxGraph { graph_additions }, index_additions: ::index_txout(&mut self.index, outpoint, txout), - last_height, } } @@ -170,13 +125,6 @@ impl IndexedTxGraph { ) -> IndexedAdditions { let txid = tx.txid(); - let last_height = match &observation { - ObservedAs::Confirmed(anchor) => { - self.insert_height_internal(anchor.anchor_block().height) - } - ObservedAs::Unconfirmed(_) => None, - }; - IndexedAdditions { graph_additions: { let mut graph_additions = self.graph.insert_tx(tx.clone()); @@ -187,7 +135,6 @@ impl IndexedTxGraph { graph_additions }, index_additions: ::index_tx(&mut self.index, tx), - last_height, } } @@ -213,11 +160,6 @@ impl IndexedTxGraph { }) } - /// Get the last block height that we are synced up to. - pub fn last_height(&self) -> u32 { - self.last_height - } - // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`. pub fn try_list_chain_txs<'a, C>( &'a self, From 10ab77c549e597d0d8157d94ae6fa3b4d39fd5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 12 Apr 2023 11:24:05 +0800 Subject: [PATCH 30/48] [bdk_chain_redesign] MOVE `TxIndex` into `indexed_chain_graph.rs` `tx_graph.rs` is rearranged as well. --- crates/chain/src/indexed_tx_graph.rs | 117 ++++++++++++++--------- crates/chain/src/keychain/txout_index.rs | 3 +- crates/chain/src/spk_txout_index.rs | 3 +- crates/chain/src/tx_data_traits.rs | 21 ---- 4 files changed, 74 insertions(+), 70 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 574afc1b..0cee62ed 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -5,55 +5,9 @@ use bitcoin::{OutPoint, Script, Transaction, TxOut}; use crate::{ keychain::Balance, tx_graph::{Additions, TxGraph, TxNode}, - Append, BlockAnchor, BlockId, ChainOracle, FullTxOut, ObservedAs, TxIndex, + Append, BlockAnchor, BlockId, ChainOracle, FullTxOut, ObservedAs, }; -/// An outwards-facing view of a transaction that is part of the *best chain*'s history. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct CanonicalTx<'a, T, A> { - /// Where the transaction is observed (in a block or in mempool). - pub observed_as: ObservedAs<&'a A>, - /// The transaction with anchors and last seen timestamp. - pub tx: TxNode<'a, T, A>, -} - -/// A structure that represents changes to an [`IndexedTxGraph`]. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde( - crate = "serde_crate", - bound( - deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>", - serialize = "A: Ord + serde::Serialize, IA: serde::Serialize" - ) - ) -)] -#[must_use] -pub struct IndexedAdditions { - /// [`TxGraph`] additions. - pub graph_additions: Additions, - /// [`TxIndex`] additions. - pub index_additions: IA, -} - -impl Default for IndexedAdditions { - fn default() -> Self { - Self { - graph_additions: Default::default(), - index_additions: Default::default(), - } - } -} - -impl Append for IndexedAdditions { - fn append(&mut self, other: Self) { - self.graph_additions.append(other.graph_additions); - self.index_additions.append(other.index_additions); - } -} - pub struct IndexedTxGraph { /// Transaction index. pub index: I, @@ -367,3 +321,72 @@ impl IndexedTxGraph { .expect("error is infallible") } } + +/// A structure that represents changes to an [`IndexedTxGraph`]. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + crate = "serde_crate", + bound( + deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize, IA: serde::Serialize" + ) + ) +)] +#[must_use] +pub struct IndexedAdditions { + /// [`TxGraph`] additions. + pub graph_additions: Additions, + /// [`TxIndex`] additions. + pub index_additions: IA, +} + +impl Default for IndexedAdditions { + fn default() -> Self { + Self { + graph_additions: Default::default(), + index_additions: Default::default(), + } + } +} + +impl Append for IndexedAdditions { + fn append(&mut self, other: Self) { + self.graph_additions.append(other.graph_additions); + self.index_additions.append(other.index_additions); + } +} + +/// An outwards-facing view of a transaction that is part of the *best chain*'s history. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CanonicalTx<'a, T, A> { + /// Where the transaction is observed (in a block or in mempool). + pub observed_as: ObservedAs<&'a A>, + /// The transaction with anchors and last seen timestamp. + pub tx: TxNode<'a, T, A>, +} + +/// Represents an index of transaction data. +pub trait TxIndex { + /// The resultant "additions" when new transaction data is indexed. + type Additions; + + /// Scan and index the given `outpoint` and `txout`. + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions; + + /// Scan and index the given transaction. + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions; + + /// Apply additions to itself. + fn apply_additions(&mut self, additions: Self::Additions); + + /// Returns whether the txout is marked as relevant in the index. + fn is_txout_relevant(&self, outpoint: OutPoint, txout: &TxOut) -> bool; + + /// Returns whether the transaction is marked as relevant in the index. + fn is_tx_relevant(&self, tx: &Transaction) -> bool; +} + +pub trait SpkIndex: TxIndex {} diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index 7dd570a6..43623a3e 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,7 +1,8 @@ use crate::{ collections::*, + indexed_tx_graph::TxIndex, miniscript::{Descriptor, DescriptorPublicKey}, - ForEachTxOut, SpkTxOutIndex, TxIndex, + ForEachTxOut, SpkTxOutIndex, }; use alloc::{borrow::Cow, vec::Vec}; use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut}; diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 20be073a..03b7b6a7 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -2,7 +2,8 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - ForEachTxOut, TxIndex, + indexed_tx_graph::TxIndex, + ForEachTxOut, }; use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid}; diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 1399ebeb..401b00f9 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -68,24 +68,3 @@ pub trait Append { impl Append for () { fn append(&mut self, _other: Self) {} } - -/// Represents an index of transaction data. -pub trait TxIndex { - /// The resultant "additions" when new transaction data is indexed. - type Additions; - - /// Scan and index the given `outpoint` and `txout`. - fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions; - - /// Scan and index the given transaction. - fn index_tx(&mut self, tx: &Transaction) -> Self::Additions; - - /// Apply additions to itself. - fn apply_additions(&mut self, additions: Self::Additions); - - /// Returns whether the txout is marked as relevant in the index. - fn is_txout_relevant(&self, outpoint: OutPoint, txout: &TxOut) -> bool; - - /// Returns whether the transaction is marked as relevant in the index. - fn is_tx_relevant(&self, tx: &Transaction) -> bool; -} From 001efdd1cb658f3a2929ffcf9047e6b07e9bd15c Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Sat, 8 Apr 2023 21:02:33 +0530 Subject: [PATCH 31/48] Include tests for new updates of TxGraph --- crates/chain/src/local_chain.rs | 7 + crates/chain/tests/test_tx_graph.rs | 282 +++++++++++++++++++++++++++- 2 files changed, 283 insertions(+), 6 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 58cf923b..e3385e1b 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -67,6 +67,13 @@ impl LocalChain { .map(|(&height, &hash)| BlockId { height, hash }) } + /// Get a block at the given height. + pub fn get_block(&self, height: u32) -> Option { + self.blocks + .get(&height) + .map(|&hash| BlockId { height, hash }) + } + /// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights /// are to be re-filled. pub fn determine_changeset(&self, update: &Self) -> Result { diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 279ddb74..e3bc0ac8 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,13 +2,18 @@ mod common; use bdk_chain::{ collections::*, + local_chain::LocalChain, tx_graph::{Additions, TxGraph}, + BlockId, ObservedAs, +}; +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] fn insert_txouts() { + // 2 (Outpoint, TxOut) tupples that denotes original data in the graph, as partial transactions. let original_ops = [ ( OutPoint::new(h!("tx1"), 1), @@ -26,6 +31,7 @@ fn insert_txouts() { ), ]; + // Another (OutPoint, TxOut) tupple to be used as update as partial transaction. let update_ops = [( OutPoint::new(h!("tx2"), 0), TxOut { @@ -34,8 +40,32 @@ fn insert_txouts() { }, )]; + // One full transaction to be included in the update + let update_txs = Transaction { + version: 0x01, + lock_time: PackedLockTime(0), + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut { + value: 30_000, + script_pubkey: Script::new(), + }], + }; + + // Conf anchor used to mark the full transaction as confirmed. + let conf_anchor = ObservedAs::Confirmed(BlockId { + height: 100, + hash: h!("random blockhash"), + }); + + // Unconfirmed anchor to mark the partial transactions as unconfirmed + let unconf_anchor = ObservedAs::::Unconfirmed(1000000); + + // Make the original graph let mut graph = { - let mut graph = TxGraph::<()>::default(); + let mut graph = TxGraph::>::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -48,9 +78,11 @@ fn insert_txouts() { graph }; + // Make the update graph let update = { let mut graph = TxGraph::default(); for (outpoint, txout) in &update_ops { + // Insert partials transactions assert_eq!( graph.insert_txout(*outpoint, txout.clone()), Additions { @@ -58,25 +90,101 @@ fn insert_txouts() { ..Default::default() } ); + // Mark them unconfirmed. + assert_eq!( + graph.insert_anchor(outpoint.txid, unconf_anchor), + Additions { + tx: [].into(), + txout: [].into(), + anchors: [(unconf_anchor, outpoint.txid)].into(), + last_seen: [].into() + } + ); + // Mark them last seen at. + assert_eq!( + graph.insert_seen_at(outpoint.txid, 1000000), + Additions { + tx: [].into(), + txout: [].into(), + anchors: [].into(), + last_seen: [(outpoint.txid, 1000000)].into() + } + ); } + // Insert the full transaction + assert_eq!( + graph.insert_tx(update_txs.clone()), + Additions { + tx: [update_txs.clone()].into(), + ..Default::default() + } + ); + + // Mark it as confirmed. + assert_eq!( + graph.insert_anchor(update_txs.txid(), conf_anchor), + Additions { + tx: [].into(), + txout: [].into(), + anchors: [(conf_anchor, update_txs.txid())].into(), + last_seen: [].into() + } + ); graph }; + // Check the resulting addition. let additions = graph.determine_additions(&update); assert_eq!( additions, Additions { - tx: [].into(), + tx: [update_txs.clone()].into(), txout: update_ops.into(), - ..Default::default() + anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(), + last_seen: [(h!("tx2"), 1000000)].into() } ); + // Apply addition and check the new graph counts. graph.apply_additions(additions); - assert_eq!(graph.all_txouts().count(), 3); - assert_eq!(graph.full_transactions().count(), 0); + assert_eq!(graph.all_txouts().count(), 4); + assert_eq!(graph.full_transactions().count(), 1); assert_eq!(graph.partial_transactions().count(), 2); + + // Check TxOuts are fetched correctly from the graph. + assert_eq!( + graph.txouts(h!("tx1")).expect("should exists"), + [ + ( + 1u32, + &TxOut { + value: 10_000, + script_pubkey: Script::new(), + } + ), + ( + 2u32, + &TxOut { + value: 20_000, + script_pubkey: Script::new(), + } + ) + ] + .into() + ); + + assert_eq!( + graph.txouts(update_txs.txid()).expect("should exists"), + [( + 0u32, + &TxOut { + value: 30_000, + script_pubkey: Script::new() + } + )] + .into() + ); } #[test] @@ -511,3 +619,165 @@ fn test_descendants_no_repeat() { } assert!(expected_txids.is_empty()); } + +#[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"); + + // The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx. + // The parent tx is confirmed at block 95. + let tx_0 = Transaction { + input: vec![], + output: vec![ + TxOut { + value: 10_000, + script_pubkey: Script::new(), + }, + TxOut { + value: 20_000, + script_pubkey: Script::new(), + }, + ], + ..common::new_tx(0) + }; + + // The first confirmed transaction spends vout: 0. And is confirmed at block 98. + let tx_1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_0.txid(), 0), + ..TxIn::default() + }], + output: vec![ + TxOut { + value: 5_000, + script_pubkey: Script::new(), + }, + TxOut { + value: 5_000, + script_pubkey: Script::new(), + }, + ], + ..common::new_tx(0) + }; + + // The second transactions spends vout:1, and is unconfirmed. + let tx_2 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_0.txid(), 1), + ..TxIn::default() + }], + output: vec![ + TxOut { + value: 10_000, + script_pubkey: Script::new(), + }, + TxOut { + value: 10_000, + script_pubkey: Script::new(), + }, + ], + ..common::new_tx(0) + }; + + let mut graph = TxGraph::::default(); + + let _ = graph.insert_tx(tx_0.clone()); + let _ = graph.insert_tx(tx_1.clone()); + let _ = graph.insert_tx(tx_2.clone()); + + [95, 98] + .iter() + .zip([&tx_0, &tx_1].into_iter()) + .for_each(|(ht, tx)| { + let block_id = local_chain.get_block(*ht).expect("block expected"); + let _ = graph.insert_anchor(tx.txid(), block_id); + }); + + // Assert that confirmed spends are returned correctly. + assert_eq!( + graph + .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0)) + .unwrap(), + ( + ObservedAs::Confirmed(&local_chain.get_block(98).expect("block expected")), + tx_1.txid() + ) + ); + + // Check if chain position is returned correctly. + assert_eq!( + graph + .get_chain_position(&local_chain, tip, tx_0.txid()) + .expect("position expected"), + ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected")) + ); + + // As long the unconfirmed tx isn't marked as seen, chain_spend will return None. + assert!(graph + .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)) + .is_none()); + + // Mark the unconfirmed as seen and check correct ObservedAs status is returned. + let _ = graph.insert_seen_at(tx_2.txid(), 1234567); + + // Check chain spend returned correctly. + assert_eq!( + graph + .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)) + .unwrap(), + (ObservedAs::Unconfirmed(1234567), tx_2.txid()) + ); + + // A conflicting transaction that conflicts with tx_1. + let tx_1_conflict = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_0.txid(), 0), + ..Default::default() + }], + ..common::new_tx(0) + }; + let _ = graph.insert_tx(tx_1_conflict.clone()); + + // Because this tx conflicts with an already confirmed transaction, chain position should return none. + assert!(graph + .get_chain_position(&local_chain, tip, tx_1_conflict.txid()) + .is_none()); + + // Another conflicting tx that conflicts with tx_2. + let tx_2_conflict = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_0.txid(), 1), + ..Default::default() + }], + ..common::new_tx(0) + }; + + // Insert in graph and mark it as seen. + let _ = graph.insert_tx(tx_2_conflict.clone()); + let _ = graph.insert_seen_at(tx_2_conflict.txid(), 1234568); + + // This should return a valid observation with correct last seen. + assert_eq!( + graph + .get_chain_position(&local_chain, tip, tx_2_conflict.txid()) + .expect("position expected"), + ObservedAs::Unconfirmed(1234568) + ); + + // Chain_spend now catches the new transaction as the spend. + assert_eq!( + graph + .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)) + .expect("expect observation"), + (ObservedAs::Unconfirmed(1234568), tx_2_conflict.txid()) + ); + + // Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict` + assert!(graph + .get_chain_position(&local_chain, tip, tx_2.txid()) + .is_none()); +} From 81436fcd72a3c45bb10a098be28de0116322d22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 17 Apr 2023 23:25:57 +0800 Subject: [PATCH 32/48] [bdk_chain_redesign] Fix `Anchor` definition + docs Previously, I have misunderstood the definition of anchor. If a tx is anchored in a block, it does not necessarily mean it is confirmed in that block. The tx can be confirmed in an ancestor block of the anchor block. With this new definition, we need a new trait `ConfirmationHeight` that has one method `confirmation_height`. This trait can be used to extend `Anchor` for those implementations that can give us the exact conirmation height of a tx (which is useful in most cases). Another change is to add another variant to the `ObservedAs` enum; `ObservedAs::ConfirmedImplicit(A)`. If a tx does not have an anchor, but another tx that spends it has an anchor that in in the best chain, we can assume that tx is also in the best chain. The logic of `TxGraph::try_get_chain_position` is also changed to reflect this. Some methods from `IndexedTxGraph` have been moved to `TxGraph` as they do not require the `Indexer`. Some `TxGraph` methods have been renamed for clarity and consistency. Also more docs are added. --- crates/chain/src/chain_data.rs | 39 ++- crates/chain/src/indexed_tx_graph.rs | 268 +++++++++--------- crates/chain/src/keychain/txout_index.rs | 8 +- crates/chain/src/sparse_chain.rs | 2 +- crates/chain/src/spk_txout_index.rs | 8 +- crates/chain/src/tx_data_traits.rs | 17 +- crates/chain/src/tx_graph.rs | 342 +++++++++++++++++++---- crates/chain/tests/test_tx_graph.rs | 16 +- 8 files changed, 474 insertions(+), 226 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index aa1e74d4..d8ce8cda 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -2,16 +2,19 @@ use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; use crate::{ sparse_chain::{self, ChainPosition}, - BlockAnchor, COINBASE_MATURITY, + Anchor, ConfirmationHeight, COINBASE_MATURITY, }; /// Represents an observation of some chain data. /// -/// The generic `A` should be a [`BlockAnchor`] implementation. +/// The generic `A` should be a [`Anchor`] implementation. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] pub enum ObservedAs { /// The chain data is seen as confirmed, and in anchored by `A`. Confirmed(A), + /// The chain data is assumed to be confirmed, because a transaction that spends it is anchored + /// by `A`. + ConfirmedImplicit(A), /// The chain data is seen in mempool at this given timestamp. Unconfirmed(u64), } @@ -20,6 +23,7 @@ impl ObservedAs<&A> { pub fn cloned(self) -> ObservedAs { match self { ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()), + ObservedAs::ConfirmedImplicit(a) => ObservedAs::ConfirmedImplicit(a.clone()), ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen), } } @@ -160,7 +164,7 @@ impl Default for BlockId { } } -impl BlockAnchor for BlockId { +impl Anchor for BlockId { fn anchor_block(&self) -> BlockId { *self } @@ -241,20 +245,23 @@ impl FullTxOut

{ } } -impl FullTxOut> { +impl FullTxOut> { /// Whether the `txout` is considered mature. /// /// This is the alternative version of [`is_mature`] which depends on `chain_position` being a - /// [`ObservedAs`] where `A` implements [`BlockAnchor`]. + /// [`ObservedAs`] where `A` implements [`Anchor`]. /// /// [`is_mature`]: Self::is_mature - pub fn is_observed_as_confirmed_and_mature(&self, tip: u32) -> bool { + pub fn is_observed_as_mature(&self, tip: u32) -> bool { if !self.is_on_coinbase { return false; } let tx_height = match &self.chain_position { - ObservedAs::Confirmed(anchor) => anchor.anchor_block().height, + ObservedAs::Confirmed(anchor) => anchor.confirmation_height(), + // although we do not know the exact confirm height, the returned height here is the + // "upper bound" so only false-negatives are possible + ObservedAs::ConfirmedImplicit(anchor) => anchor.confirmation_height(), ObservedAs::Unconfirmed(_) => { debug_assert!(false, "coinbase tx can never be unconfirmed"); return false; @@ -274,22 +281,24 @@ impl FullTxOut> { /// Currently this method does not take into account the locktime. /// /// This is the alternative version of [`is_spendable_at`] which depends on `chain_position` - /// being a [`ObservedAs`] where `A` implements [`BlockAnchor`]. + /// being a [`ObservedAs`] where `A` implements [`Anchor`]. /// /// [`is_spendable_at`]: Self::is_spendable_at pub fn is_observed_as_confirmed_and_spendable(&self, tip: u32) -> bool { - if !self.is_observed_as_confirmed_and_mature(tip) { + if !self.is_observed_as_mature(tip) { return false; } - match &self.chain_position { - ObservedAs::Confirmed(anchor) => { - if anchor.anchor_block().height > tip { - return false; - } - } + let confirmation_height = match &self.chain_position { + ObservedAs::Confirmed(anchor) => anchor.confirmation_height(), + // although we do not know the exact confirm height, the returned height here is the + // "upper bound" so only false-negatives are possible + ObservedAs::ConfirmedImplicit(anchor) => anchor.confirmation_height(), ObservedAs::Unconfirmed(_) => return false, }; + if confirmation_height > tip { + return false; + } // if the spending tx is confirmed within tip height, the txout is no longer spendable if let Some((ObservedAs::Confirmed(spending_anchor), _)) = &self.spent_by { diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 0cee62ed..00137c41 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,13 +1,16 @@ use core::convert::Infallible; -use bitcoin::{OutPoint, Script, Transaction, TxOut}; +use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use crate::{ keychain::Balance, - tx_graph::{Additions, TxGraph, TxNode}, - Append, BlockAnchor, BlockId, ChainOracle, FullTxOut, ObservedAs, + tx_graph::{Additions, CanonicalTx, TxGraph}, + Anchor, Append, BlockId, ChainOracle, ConfirmationHeight, FullTxOut, ObservedAs, }; +/// A struct that combines [`TxGraph`] and an [`Indexer`] implementation. +/// +/// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically. pub struct IndexedTxGraph { /// Transaction index. pub index: I, @@ -23,7 +26,7 @@ impl Default for IndexedTxGraph { } } -impl IndexedTxGraph { +impl IndexedTxGraph { /// Get a reference of the internal transaction graph. pub fn graph(&self) -> &TxGraph { &self.graph @@ -47,195 +50,191 @@ impl IndexedTxGraph { self.graph.apply_additions(graph_additions); } +} - /// Insert a `txout` that exists in `outpoint` with the given `observation`. +impl IndexedTxGraph +where + I::Additions: Default + Append, +{ + /// Apply an `update` directly. + /// + /// `update` is a [`TxGraph`] and the resultant changes is returned as [`IndexedAdditions`]. + pub fn apply_update(&mut self, update: TxGraph) -> IndexedAdditions { + let graph_additions = self.graph.apply_update(update); + + let mut index_additions = I::Additions::default(); + for added_tx in &graph_additions.tx { + index_additions.append(self.index.index_tx(added_tx)); + } + for (&added_outpoint, added_txout) in &graph_additions.txout { + index_additions.append(self.index.index_txout(added_outpoint, added_txout)); + } + + IndexedAdditions { + graph_additions, + index_additions, + } + } + + /// Insert a floating `txout` of given `outpoint`. pub fn insert_txout( &mut self, outpoint: OutPoint, txout: &TxOut, - observation: ObservedAs, ) -> IndexedAdditions { - IndexedAdditions { - graph_additions: { - let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); - graph_additions.append(match observation { - ObservedAs::Confirmed(anchor) => { - self.graph.insert_anchor(outpoint.txid, anchor) - } - ObservedAs::Unconfirmed(seen_at) => { - self.graph.insert_seen_at(outpoint.txid, seen_at) - } - }); - graph_additions - }, - index_additions: ::index_txout(&mut self.index, outpoint, txout), - } + let mut update = TxGraph::::default(); + let _ = update.insert_txout(outpoint, txout.clone()); + self.apply_update(update) } + /// Insert and index a transaction into the graph. + /// + /// `anchors` can be provided to anchor the transaction to various blocks. `seen_at` is a + /// unix timestamp of when the transaction is last seen. pub fn insert_tx( &mut self, tx: &Transaction, - observation: ObservedAs, + anchors: impl IntoIterator, + seen_at: Option, ) -> IndexedAdditions { let txid = tx.txid(); - IndexedAdditions { - graph_additions: { - let mut graph_additions = self.graph.insert_tx(tx.clone()); - graph_additions.append(match observation { - ObservedAs::Confirmed(anchor) => self.graph.insert_anchor(txid, anchor), - ObservedAs::Unconfirmed(seen_at) => self.graph.insert_seen_at(txid, seen_at), - }); - graph_additions - }, - index_additions: ::index_tx(&mut self.index, tx), + let mut update = TxGraph::::default(); + if self.graph.get_tx(txid).is_none() { + let _ = update.insert_tx(tx.clone()); } + for anchor in anchors.into_iter() { + let _ = update.insert_anchor(txid, anchor); + } + if let Some(seen_at) = seen_at { + let _ = update.insert_seen_at(txid, seen_at); + } + + self.apply_update(update) } + /// Insert relevant transactions from the given `txs` iterator. + /// + /// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant + /// transactions in `txs` will be ignored. + /// + /// `anchors` can be provided to anchor the transactions to blocks. `seen_at` is a unix + /// timestamp of when the transactions are last seen. pub fn insert_relevant_txs<'t, T>( &mut self, txs: T, - observation: ObservedAs, + anchors: impl IntoIterator + Clone, + seen_at: Option, ) -> IndexedAdditions where T: Iterator, - I::Additions: Default + Append, { - txs.filter_map(|tx| { - if self.index.is_tx_relevant(tx) { - Some(self.insert_tx(tx, observation.clone())) - } else { - None - } + txs.filter_map(|tx| match self.index.is_tx_relevant(tx) { + true => Some(self.insert_tx(tx, anchors.clone(), seen_at)), + false => None, }) - .fold(IndexedAdditions::default(), |mut acc, other| { + .fold(Default::default(), |mut acc, other| { acc.append(other); acc }) } +} - // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`. - pub fn try_list_chain_txs<'a, C>( +impl IndexedTxGraph { + pub fn try_list_owned_txs<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator, C::Error>> where C: ChainOracle + 'a, { self.graph - .full_transactions() - .filter(|tx| self.index.is_tx_relevant(tx)) - .filter_map(move |tx| { + .full_txs() + .filter(|node| tx_alters_owned_utxo_set(&self.graph, &self.index, node.txid, node.tx)) + .filter_map(move |tx_node| { self.graph - .try_get_chain_position(chain, static_block, tx.txid) + .try_get_chain_position(chain, chain_tip, tx_node.txid) .map(|v| { - v.map(|observed_in| CanonicalTx { - observed_as: observed_in, - tx, + v.map(|observed_as| CanonicalTx { + observed_as, + node: tx_node, }) }) .transpose() }) } - pub fn list_chain_txs<'a, C>( + pub fn list_owned_txs<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator> where C: ChainOracle + 'a, { - self.try_list_chain_txs(chain, static_block) - .map(|r| r.expect("error is infallible")) + self.try_list_owned_txs(chain, chain_tip) + .map(|r| r.expect("chain oracle is infallible")) } - pub fn try_list_chain_txouts<'a, C>( + pub fn try_list_owned_txouts<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, { - self.graph - .all_txouts() - .filter(|&(op, txo)| self.index.is_txout_relevant(op, txo)) - .filter_map(move |(op, txout)| -> Option> { - let graph_tx = self.graph.get_tx(op.txid)?; - - let is_on_coinbase = graph_tx.is_coin_base(); - - let chain_position = - match self - .graph - .try_get_chain_position(chain, static_block, op.txid) - { - Ok(Some(observed_at)) => observed_at.cloned(), - Ok(None) => return None, - Err(err) => return Some(Err(err)), - }; - - let spent_by = match self.graph.try_get_spend_in_chain(chain, static_block, op) { - Ok(Some((obs, txid))) => Some((obs.cloned(), txid)), - Ok(None) => None, - Err(err) => return Some(Err(err)), - }; - - let full_txout = FullTxOut { - outpoint: op, - txout: txout.clone(), - chain_position, - spent_by, - is_on_coinbase, - }; - - Some(Ok(full_txout)) + self.graph() + .try_list_chain_txouts(chain, chain_tip, |_, txout| { + self.index.is_spk_owned(&txout.script_pubkey) }) } - pub fn list_chain_txouts<'a, C>( + pub fn list_owned_txouts<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator>> + 'a where - C: ChainOracle + 'a, + C: ChainOracle + 'a, { - self.try_list_chain_txouts(chain, static_block) - .map(|r| r.expect("error in infallible")) + self.try_list_owned_txouts(chain, chain_tip) + .map(|r| r.expect("oracle is infallible")) } - /// Return relevant unspents. - pub fn try_list_chain_utxos<'a, C>( + pub fn try_list_owned_unspents<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, { - self.try_list_chain_txouts(chain, static_block) - .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) + self.graph() + .try_list_chain_unspents(chain, chain_tip, |_, txout| { + self.index.is_spk_owned(&txout.script_pubkey) + }) } - pub fn list_chain_utxos<'a, C>( + pub fn list_owned_unspents<'a, C>( &'a self, chain: &'a C, - static_block: BlockId, + chain_tip: BlockId, ) -> impl Iterator>> + 'a where - C: ChainOracle + 'a, + C: ChainOracle + 'a, { - self.try_list_chain_utxos(chain, static_block) - .map(|r| r.expect("error is infallible")) + self.try_list_owned_unspents(chain, chain_tip) + .map(|r| r.expect("oracle is infallible")) } +} +impl IndexedTxGraph { pub fn try_balance( &self, chain: &C, - static_block: BlockId, + chain_tip: BlockId, tip: u32, mut should_trust: F, ) -> Result @@ -248,13 +247,13 @@ impl IndexedTxGraph { let mut untrusted_pending = 0; let mut confirmed = 0; - for res in self.try_list_chain_txouts(chain, static_block) { + for res in self.try_list_owned_txouts(chain, chain_tip) { let txout = res?; match &txout.chain_position { - ObservedAs::Confirmed(_) => { + ObservedAs::Confirmed(_) | ObservedAs::ConfirmedImplicit(_) => { if txout.is_on_coinbase { - if txout.is_observed_as_confirmed_and_mature(tip) { + if txout.is_observed_as_mature(tip) { confirmed += txout.txout.value; } else { immature += txout.txout.value; @@ -304,7 +303,10 @@ impl IndexedTxGraph { C: ChainOracle, { let mut sum = 0; - for txo_res in self.try_list_chain_txouts(chain, static_block) { + for txo_res in self + .graph() + .try_list_chain_txouts(chain, static_block, |_, _| true) + { let txo = txo_res?; if txo.is_observed_as_confirmed_and_spendable(height) { sum += txo.txout.value; @@ -339,7 +341,7 @@ impl IndexedTxGraph { pub struct IndexedAdditions { /// [`TxGraph`] additions. pub graph_additions: Additions, - /// [`TxIndex`] additions. + /// [`Indexer`] additions. pub index_additions: IA, } @@ -352,24 +354,15 @@ impl Default for IndexedAdditions { } } -impl Append for IndexedAdditions { +impl Append for IndexedAdditions { fn append(&mut self, other: Self) { self.graph_additions.append(other.graph_additions); self.index_additions.append(other.index_additions); } } -/// An outwards-facing view of a transaction that is part of the *best chain*'s history. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct CanonicalTx<'a, T, A> { - /// Where the transaction is observed (in a block or in mempool). - pub observed_as: ObservedAs<&'a A>, - /// The transaction with anchors and last seen timestamp. - pub tx: TxNode<'a, T, A>, -} - -/// Represents an index of transaction data. -pub trait TxIndex { +/// Represents a structure that can index transaction data. +pub trait Indexer { /// The resultant "additions" when new transaction data is indexed. type Additions; @@ -382,11 +375,30 @@ pub trait TxIndex { /// Apply additions to itself. fn apply_additions(&mut self, additions: Self::Additions); - /// Returns whether the txout is marked as relevant in the index. - fn is_txout_relevant(&self, outpoint: OutPoint, txout: &TxOut) -> bool; - - /// Returns whether the transaction is marked as relevant in the index. + /// Determines whether the transaction should be included in the index. fn is_tx_relevant(&self, tx: &Transaction) -> bool; } -pub trait SpkIndex: TxIndex {} +/// A trait that extends [`Indexer`] to also index "owned" script pubkeys. +pub trait OwnedIndexer: Indexer { + /// Determines whether a given script pubkey (`spk`) is owned. + fn is_spk_owned(&self, spk: &Script) -> bool; +} + +fn tx_alters_owned_utxo_set( + graph: &TxGraph, + index: &I, + txid: Txid, + tx: &Transaction, +) -> bool +where + A: Anchor, + I: OwnedIndexer, +{ + let prev_spends = (0..tx.input.len() as u32) + .map(|vout| OutPoint { txid, vout }) + .filter_map(|op| graph.get_txout(op)); + prev_spends + .chain(&tx.output) + .any(|txout| index.is_spk_owned(&txout.script_pubkey)) +} diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index 43623a3e..e4ac3ef4 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,6 +1,6 @@ use crate::{ collections::*, - indexed_tx_graph::TxIndex, + indexed_tx_graph::Indexer, miniscript::{Descriptor, DescriptorPublicKey}, ForEachTxOut, SpkTxOutIndex, }; @@ -91,7 +91,7 @@ impl Deref for KeychainTxOutIndex { } } -impl TxIndex for KeychainTxOutIndex { +impl Indexer for KeychainTxOutIndex { type Additions = DerivationAdditions; fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { @@ -106,10 +106,6 @@ impl TxIndex for KeychainTxOutIndex { self.apply_additions(additions) } - fn is_txout_relevant(&self, _outpoint: OutPoint, txout: &TxOut) -> bool { - self.index_of_spk(&txout.script_pubkey).is_some() - } - fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { self.is_relevant(tx) } diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index acc61601..fbcdcaa5 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -997,7 +997,7 @@ impl SparseChain

{ /// `chain` for this to return `Some`. pub fn spent_by(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<(&P, Txid)> { graph - .outspends(outpoint) + .output_spends(outpoint) .iter() .find_map(|&txid| Some((self.tx_position(txid)?, txid))) } diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 03b7b6a7..3e89ba39 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -2,7 +2,7 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - indexed_tx_graph::TxIndex, + indexed_tx_graph::Indexer, ForEachTxOut, }; use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid}; @@ -53,7 +53,7 @@ impl Default for SpkTxOutIndex { } } -impl TxIndex for SpkTxOutIndex { +impl Indexer for SpkTxOutIndex { type Additions = (); fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { @@ -70,10 +70,6 @@ impl TxIndex for SpkTxOutIndex { // This applies nothing. } - fn is_txout_relevant(&self, _outpoint: OutPoint, txout: &TxOut) -> bool { - self.index_of_spk(&txout.script_pubkey).is_some() - } - fn is_tx_relevant(&self, tx: &Transaction) -> bool { self.is_relevant(tx) } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 401b00f9..9d41c549 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -40,25 +40,34 @@ impl ForEachTxOut for Transaction { /// assume that transaction A is also confirmed in the best chain. This does not necessarily mean /// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a /// parent block of B. -pub trait BlockAnchor: +pub trait Anchor: 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 &'static A { +impl Anchor for &'static A { fn anchor_block(&self) -> BlockId { - ::anchor_block(self) + ::anchor_block(self) } } -impl BlockAnchor for (u32, BlockHash) { +impl Anchor for (u32, BlockHash) { fn anchor_block(&self) -> BlockId { (*self).into() } } +/// A trait that returns a confirmation height. +/// +/// This is typically used to provide an [`Anchor`] implementation the exact confirmation height of +/// the data being anchored. +pub trait ConfirmationHeight { + /// Returns the confirmation height. + fn confirmation_height(&self) -> u32; +} + /// Trait that makes an object appendable. pub trait Append { /// Append another object of the same type onto `self`. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index e3afce0e..7c32606a 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,7 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, ObservedAs}; +use crate::{collections::*, Anchor, BlockId, ChainOracle, ForEachTxOut, FullTxOut, ObservedAs}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::{ @@ -91,7 +91,7 @@ impl Default for TxGraph { } } -/// An outward-facing representation of a (transaction) node in the [`TxGraph`]. +/// An outward-facing view of a (transaction) node in the [`TxGraph`]. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct TxNode<'a, T, A> { /// Txid of the transaction. @@ -139,8 +139,19 @@ impl Default for TxNodeInternal { } } +/// An outwards-facing view of a transaction that is part of the *best chain*'s history. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CanonicalTx<'a, T, A> { + /// How the transaction is observed as (confirmed or unconfirmed). + pub observed_as: ObservedAs<&'a A>, + /// The transaction node (as part of the graph). + pub node: TxNode<'a, T, A>, +} + impl TxGraph { /// Iterate over all tx outputs known by [`TxGraph`]. + /// + /// This includes txouts of both full transactions as well as floating transactions. pub fn all_txouts(&self) -> impl Iterator { self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx { TxNodeInternal::Whole(tx) => tx @@ -156,8 +167,26 @@ impl TxGraph { }) } + /// Iterate over floating txouts known by [`TxGraph`]. + /// + /// Floating txouts are txouts that do not have the residing full transaction contained in the + /// graph. + pub fn floating_txouts(&self) -> impl Iterator { + self.txs + .iter() + .filter_map(|(txid, (tx_node, _, _))| match tx_node { + TxNodeInternal::Whole(_) => None, + TxNodeInternal::Partial(txouts) => Some( + txouts + .iter() + .map(|(&vout, txout)| (OutPoint::new(*txid, vout), txout)), + ), + }) + .flatten() + } + /// Iterate over all full transactions in the graph. - pub fn full_transactions(&self) -> impl Iterator> { + pub fn full_txs(&self) -> impl Iterator> { self.txs .iter() .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { @@ -201,8 +230,10 @@ impl TxGraph { } } + /// Returns known outputs of a given `txid`. + /// /// Returns a [`BTreeMap`] of vout to output of the provided `txid`. - pub fn txouts(&self, txid: Txid) -> Option> { + pub fn tx_outputs(&self, txid: Txid) -> Option> { Some(match &self.txs.get(&txid)?.0 { TxNodeInternal::Whole(tx) => tx .output @@ -251,7 +282,7 @@ impl TxGraph { /// /// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in /// the returned set will never be in the same active-chain. - pub fn outspends(&self, outpoint: OutPoint) -> &HashSet { + pub fn output_spends(&self, outpoint: OutPoint) -> &HashSet { self.spends.get(&outpoint).unwrap_or(&self.empty_outspends) } @@ -261,7 +292,7 @@ impl TxGraph { /// /// - `vout` is the provided `txid`'s outpoint that is being spent /// - `txid-set` is the set of txids spending the `vout`. - pub fn tx_outspends( + pub fn tx_spends( &self, txid: Txid, ) -> impl DoubleEndedIterator)> + '_ { @@ -275,23 +306,6 @@ impl TxGraph { .map(|(outpoint, spends)| (outpoint.vout, spends)) } - /// Iterate over all partial transactions (outputs only) in the graph. - pub fn partial_transactions( - &self, - ) -> impl Iterator, A>> { - self.txs - .iter() - .filter_map(|(&txid, (tx, anchors, last_seen))| match tx { - TxNodeInternal::Whole(_) => None, - TxNodeInternal::Partial(partial) => Some(TxNode { - txid, - tx: partial, - anchors, - last_seen_unconfirmed: *last_seen, - }), - }) - } - /// Creates an iterator that filters and maps descendants from the starting `txid`. /// /// The supplied closure takes in two inputs `(depth, descendant_txid)`: @@ -363,6 +377,9 @@ impl TxGraph { /// Returns the resultant [`Additions`] if the given `txout` is inserted at `outpoint`. Does not /// mutate `self`. /// + /// Inserting floating txouts are useful for determining fee/feerate of transactions we care + /// about. + /// /// 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 { @@ -380,8 +397,10 @@ impl TxGraph { /// 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!). + /// This is equivalent to calling [`insert_txout_preview`] and [`apply_additions`] in sequence. + /// + /// [`insert_txout_preview`]: Self::insert_txout_preview + /// [`apply_additions`]: Self::apply_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()); @@ -581,7 +600,7 @@ impl TxGraph { } } -impl TxGraph { +impl TxGraph { /// Get all heights that are relevant to the graph. pub fn relevant_heights(&self) -> impl Iterator + '_ { let mut visited = HashSet::new(); @@ -591,13 +610,21 @@ impl TxGraph { .filter(move |&h| visited.insert(h)) } - /// Determines whether a transaction of `txid` is in the best chain. + /// Get the position of the transaction in `chain` with tip `chain_tip`. /// - /// TODO: Also return conflicting tx list, ordered by last_seen. + /// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is + /// returned. + /// + /// # Error + /// + /// An error will occur if the [`ChainOracle`] implementation (`chain`) fails. If the + /// [`ChainOracle`] is infallible, [`get_chain_position`] can be used instead. + /// + /// [`get_chain_position`]: Self::get_chain_position pub fn try_get_chain_position( &self, chain: &C, - static_block: BlockId, + chain_tip: BlockId, txid: Txid, ) -> Result>, C::Error> where @@ -611,28 +638,27 @@ impl TxGraph { }; for anchor in anchors { - match chain.is_block_in_chain(anchor.anchor_block(), static_block)? { + match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? { Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), - Some(false) => continue, - // if we cannot determine whether block is in the best chain, we can check whether - // a spending transaction is confirmed in best chain, and if so, it is guaranteed - // that the tx being spent (this tx) is in the best chain - None => { - let spending_anchors = self - .spends - .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) - .flat_map(|(_, spending_txids)| spending_txids) - .filter_map(|spending_txid| self.txs.get(spending_txid)) - .flat_map(|(_, spending_anchors, _)| spending_anchors); - for spending_anchor in spending_anchors { - match chain - .is_block_in_chain(spending_anchor.anchor_block(), static_block)? - { - Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), - _ => continue, - } - } - } + _ => continue, + } + } + + // If we cannot determine whether tx is in best chain, we can check whether a spending tx is + // confirmed and in best chain, and if so, it is guaranteed that this tx is in the best + // chain. + // + // [TODO] This logic is incomplete as we do not check spends of spends. + let spending_anchors = self + .spends + .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) + .flat_map(|(_, spending_txids)| spending_txids) + .filter_map(|spending_txid| self.txs.get(spending_txid)) + .flat_map(|(_, spending_anchors, _)| spending_anchors); + for spending_anchor in spending_anchors { + match chain.is_block_in_chain(spending_anchor.anchor_block(), chain_tip)? { + Some(true) => return Ok(Some(ObservedAs::ConfirmedImplicit(spending_anchor))), + _ => continue, } } @@ -650,7 +676,7 @@ impl TxGraph { // this tx cannot exist in the best chain for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) { for block in conflicting_tx.anchors.iter().map(A::anchor_block) { - if chain.is_block_in_chain(block, static_block)? == Some(true) { + if chain.is_block_in_chain(block, chain_tip)? == Some(true) { // conflicting tx is in best chain, so the current tx cannot be in best chain! return Ok(None); } @@ -663,37 +689,54 @@ impl TxGraph { Ok(Some(ObservedAs::Unconfirmed(last_seen))) } + /// Get the position of the transaction in `chain` with tip `chain_tip`. + /// + /// This is the infallible version of [`try_get_chain_position`]. + /// + /// [`try_get_chain_position`]: Self::try_get_chain_position pub fn get_chain_position( &self, chain: &C, - static_block: BlockId, + chain_tip: BlockId, txid: Txid, ) -> Option> where C: ChainOracle, { - self.try_get_chain_position(chain, static_block, txid) + self.try_get_chain_position(chain, chain_tip, txid) .expect("error is infallible") } - pub fn try_get_spend_in_chain( + /// Get the txid of the spending transaction and where the spending transaction is observed in + /// the `chain` of `chain_tip`. + /// + /// If no in-chain transaction spends `outpoint`, `None` will be returned. + /// + /// # Error + /// + /// An error will occur only if the [`ChainOracle`] implementation (`chain`) fails. + /// + /// If the [`ChainOracle`] is infallible, [`get_chain_spend`] can be used instead. + /// + /// [`get_chain_spend`]: Self::get_chain_spend + pub fn try_get_chain_spend( &self, chain: &C, - static_block: BlockId, + chain_tip: BlockId, outpoint: OutPoint, ) -> Result, Txid)>, C::Error> where C: ChainOracle, { if self - .try_get_chain_position(chain, static_block, outpoint.txid)? + .try_get_chain_position(chain, chain_tip, outpoint.txid)? .is_none() { return Ok(None); } if let Some(spends) = self.spends.get(&outpoint) { for &txid in spends { - if let Some(observed_at) = self.try_get_chain_position(chain, static_block, txid)? { + if let Some(observed_at) = self.try_get_chain_position(chain, chain_tip, txid)? { return Ok(Some((observed_at, txid))); } } @@ -701,6 +744,12 @@ impl TxGraph { Ok(None) } + /// Get the txid of the spending transaction and where the spending transaction is observed in + /// the `chain` of `chain_tip`. + /// + /// This is the infallible version of [`try_get_chain_spend`] + /// + /// [`try_get_chain_spend`]: Self::try_get_chain_spend pub fn get_chain_spend( &self, chain: &C, @@ -710,9 +759,186 @@ impl TxGraph { where C: ChainOracle, { - self.try_get_spend_in_chain(chain, static_block, outpoint) + self.try_get_chain_spend(chain, static_block, outpoint) .expect("error is infallible") } + + /// List graph transactions that are in `chain` with `chain_tip`. + /// + /// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is + /// observed in-chain, and the [`TxNode`]. + /// + /// # Error + /// + /// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the + /// returned item. + /// + /// If the [`ChainOracle`] is infallible, [`list_chain_txs`] can be used instead. + /// + /// [`list_chain_txs`]: Self::list_chain_txs + pub fn try_list_chain_txs<'a, C>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator, C::Error>> + where + C: ChainOracle + 'a, + { + self.full_txs().filter_map(move |tx| { + self.try_get_chain_position(chain, chain_tip, tx.txid) + .map(|v| { + v.map(|observed_in| CanonicalTx { + observed_as: observed_in, + node: tx, + }) + }) + .transpose() + }) + } + + /// List graph transactions that are in `chain` with `chain_tip`. + /// + /// This is the infallible version of [`try_list_chain_txs`]. + /// + /// [`try_list_chain_txs`]: Self::try_list_chain_txs + pub fn list_chain_txs<'a, C>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + ) -> impl Iterator> + where + C: ChainOracle + 'a, + { + self.try_list_chain_txs(chain, chain_tip) + .map(|r| r.expect("oracle is infallible")) + } + + /// List outputs that are in `chain` with `chain_tip`. + /// + /// Floating ouputs are not iterated over. + /// + /// The `filter_predicate` should return true for outputs that we wish to iterate over. + /// + /// # Error + /// + /// A returned item can error if the [`ChainOracle`] implementation (`chain`) fails. + /// + /// If the [`ChainOracle`] is infallible, [`list_chain_txouts`] can be used instead. + /// + /// [`list_chain_txouts`]: Self::list_chain_txouts + pub fn try_list_chain_txouts<'a, C, P>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + mut filter_predicate: P, + ) -> impl Iterator>, C::Error>> + 'a + where + C: ChainOracle + 'a, + P: FnMut(OutPoint, &TxOut) -> bool + 'a, + { + self.try_list_chain_txs(chain, chain_tip) + .flat_map(move |tx_res| match tx_res { + Ok(canonical_tx) => canonical_tx + .node + .output + .iter() + .enumerate() + .filter_map(|(vout, txout)| { + let outpoint = OutPoint::new(canonical_tx.node.txid, vout as _); + if filter_predicate(outpoint, txout) { + Some(Ok((outpoint, txout.clone(), canonical_tx.clone()))) + } else { + None + } + }) + .collect::>(), + Err(err) => vec![Err(err)], + }) + .map(move |res| -> Result<_, C::Error> { + let ( + outpoint, + txout, + CanonicalTx { + observed_as, + node: tx_node, + }, + ) = res?; + let chain_position = observed_as.cloned(); + let spent_by = self + .try_get_chain_spend(chain, chain_tip, outpoint)? + .map(|(obs_as, txid)| (obs_as.cloned(), txid)); + let is_on_coinbase = tx_node.tx.is_coin_base(); + Ok(FullTxOut { + outpoint, + txout, + chain_position, + spent_by, + is_on_coinbase, + }) + }) + } + + /// List outputs that are in `chain` with `chain_tip`. + /// + /// This is the infallible version of [`try_list_chain_txouts`]. + /// + /// [`try_list_chain_txouts`]: Self::try_list_chain_txouts + pub fn list_chain_txouts<'a, C, P>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + filter_predicate: P, + ) -> impl Iterator>> + 'a + where + C: ChainOracle + 'a, + P: FnMut(OutPoint, &TxOut) -> bool + 'a, + { + self.try_list_chain_txouts(chain, chain_tip, filter_predicate) + .map(|r| r.expect("error in infallible")) + } + + /// List unspent outputs (UTXOs) that are in `chain` with `chain_tip`. + /// + /// Floating outputs are not iterated over. + /// + /// # Error + /// + /// An item can be an error if the [`ChainOracle`] implementation fails. If the oracle is + /// infallible, [`list_chain_unspents`] can be used instead. + /// + /// [`list_chain_unspents`]: Self::list_chain_unspents + pub fn try_list_chain_unspents<'a, C, P>( + &'a self, + chain: &'a C, + chain_tip: BlockId, + filter_txout: P, + ) -> impl Iterator>, C::Error>> + 'a + where + C: ChainOracle + 'a, + P: FnMut(OutPoint, &TxOut) -> bool + 'a, + { + self.try_list_chain_txouts(chain, chain_tip, filter_txout) + .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) + } + + /// List unspent outputs (UTXOs) that are in `chain` with `chain_tip`. + /// + /// This is the infallible version of [`try_list_chain_unspents`]. + /// + /// [`try_list_chain_unspents`]: Self::try_list_chain_unspents + pub fn list_chain_unspents<'a, C, P>( + &'a self, + chain: &'a C, + static_block: BlockId, + filter_txout: P, + ) -> impl Iterator>> + 'a + where + C: ChainOracle + 'a, + P: FnMut(OutPoint, &TxOut) -> bool + 'a, + { + self.try_list_chain_unspents(chain, static_block, filter_txout) + .map(|r| r.expect("error is infallible")) + } } /// A structure that represents changes to a [`TxGraph`]. diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index e3bc0ac8..965bb259 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -149,12 +149,12 @@ fn insert_txouts() { // Apply addition and check the new graph counts. graph.apply_additions(additions); assert_eq!(graph.all_txouts().count(), 4); - assert_eq!(graph.full_transactions().count(), 1); - assert_eq!(graph.partial_transactions().count(), 2); + assert_eq!(graph.full_txs().count(), 1); + assert_eq!(graph.floating_txouts().count(), 3); // Check TxOuts are fetched correctly from the graph. assert_eq!( - graph.txouts(h!("tx1")).expect("should exists"), + graph.tx_outputs(h!("tx1")).expect("should exists"), [ ( 1u32, @@ -175,7 +175,7 @@ fn insert_txouts() { ); assert_eq!( - graph.txouts(update_txs.txid()).expect("should exists"), + graph.tx_outputs(update_txs.txid()).expect("should exists"), [( 0u32, &TxOut { @@ -201,8 +201,8 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() { let mut graph = TxGraph::<()>::default(); let _ = graph.insert_tx(tx); - assert!(graph.outspends(OutPoint::null()).is_empty()); - assert!(graph.tx_outspends(Txid::all_zeros()).next().is_none()); + assert!(graph.output_spends(OutPoint::null()).is_empty()); + assert!(graph.tx_spends(Txid::all_zeros()).next().is_none()); } #[test] @@ -240,10 +240,10 @@ fn insert_tx_graph_keeps_track_of_spend() { let _ = graph2.insert_tx(tx1); assert_eq!( - graph1.outspends(op), + graph1.output_spends(op), &iter::once(tx2.txid()).collect::>() ); - assert_eq!(graph2.outspends(op), graph1.outspends(op)); + assert_eq!(graph2.output_spends(op), graph1.output_spends(op)); } #[test] From 8e36a2e5f6d0fe2813dca9a13005e0b1be7e94c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 19 Apr 2023 12:21:39 +0800 Subject: [PATCH 33/48] [bdk_chain_redesign] Remove incomplete logic `ObservedAs::ConfirmedImplicit` is incomplete, remove for now. `local_chain::ChangeSet` does not need to be a single-element tuple struct. --- crates/chain/src/chain_data.rs | 10 ------- crates/chain/src/indexed_tx_graph.rs | 2 +- crates/chain/src/local_chain.rs | 40 ++++++++++------------------ crates/chain/src/tx_graph.rs | 18 ------------- 4 files changed, 15 insertions(+), 55 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index d8ce8cda..33381ab7 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -12,9 +12,6 @@ use crate::{ pub enum ObservedAs { /// The chain data is seen as confirmed, and in anchored by `A`. Confirmed(A), - /// The chain data is assumed to be confirmed, because a transaction that spends it is anchored - /// by `A`. - ConfirmedImplicit(A), /// The chain data is seen in mempool at this given timestamp. Unconfirmed(u64), } @@ -23,7 +20,6 @@ impl ObservedAs<&A> { pub fn cloned(self) -> ObservedAs { match self { ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()), - ObservedAs::ConfirmedImplicit(a) => ObservedAs::ConfirmedImplicit(a.clone()), ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen), } } @@ -259,9 +255,6 @@ impl FullTxOut> { let tx_height = match &self.chain_position { ObservedAs::Confirmed(anchor) => anchor.confirmation_height(), - // although we do not know the exact confirm height, the returned height here is the - // "upper bound" so only false-negatives are possible - ObservedAs::ConfirmedImplicit(anchor) => anchor.confirmation_height(), ObservedAs::Unconfirmed(_) => { debug_assert!(false, "coinbase tx can never be unconfirmed"); return false; @@ -291,9 +284,6 @@ impl FullTxOut> { let confirmation_height = match &self.chain_position { ObservedAs::Confirmed(anchor) => anchor.confirmation_height(), - // although we do not know the exact confirm height, the returned height here is the - // "upper bound" so only false-negatives are possible - ObservedAs::ConfirmedImplicit(anchor) => anchor.confirmation_height(), ObservedAs::Unconfirmed(_) => return false, }; if confirmation_height > tip { diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 00137c41..fd5aa6d9 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -251,7 +251,7 @@ impl IndexedTxGraph { let txout = res?; match &txout.chain_position { - ObservedAs::Confirmed(_) | ObservedAs::ConfirmedImplicit(_) => { + ObservedAs::Confirmed(_) => { if txout.is_on_coinbase { if txout.is_observed_as_mature(tip) { confirmed += txout.txout.value; diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index e3385e1b..9ba64b28 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -1,21 +1,23 @@ -use core::{convert::Infallible, ops::Deref}; +use core::convert::Infallible; use alloc::collections::{BTreeMap, BTreeSet}; use bitcoin::BlockHash; use crate::{Append, BlockId, ChainOracle}; +/// This is a local implementation of [`ChainOracle`]. +/// +/// TODO: We need a cache/snapshot thing for chain oracle. +/// * Minimize calls to remotes. +/// * Can we cache it forever? Should we drop stuff? +/// * Assume anything deeper than (i.e. 10) blocks won't be reorged. +/// * Is this a cache on txs or block? or both? +/// TODO: Parents of children are confirmed if children are confirmed. #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct LocalChain { blocks: BTreeMap, } -// [TODO] We need a cache/snapshot thing for chain oracle. -// * Minimize calls to remotes. -// * Can we cache it forever? Should we drop stuff? -// * Assume anything deeper than (i.e. 10) blocks won't be reorged. -// * Is this a cache on txs or block? or both? -// [TODO] Parents of children are confirmed if children are confirmed. impl ChainOracle for LocalChain { type Error = Infallible; @@ -114,12 +116,12 @@ impl LocalChain { changeset.insert(*height, *new_hash); } } - Ok(ChangeSet(changeset)) + Ok(changeset) } /// Applies the given `changeset`. pub fn apply_changeset(&mut self, mut changeset: ChangeSet) { - self.blocks.append(&mut changeset.0) + self.blocks.append(&mut changeset) } /// Updates [`LocalChain`] with an update [`LocalChain`]. @@ -135,7 +137,7 @@ impl LocalChain { } pub fn initial_changeset(&self) -> ChangeSet { - ChangeSet(self.blocks.clone()) + self.blocks.clone() } pub fn heights(&self) -> BTreeSet { @@ -146,25 +148,11 @@ impl LocalChain { /// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`]. /// /// [`determine_changeset`]: LocalChain::determine_changeset -#[derive(Debug, Default, Clone, PartialEq)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde(crate = "serde_crate") -)] -pub struct ChangeSet(pub(crate) BTreeMap); - -impl Deref for ChangeSet { - type Target = BTreeMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} +type ChangeSet = BTreeMap; impl Append for ChangeSet { fn append(&mut self, mut other: Self) { - BTreeMap::append(&mut self.0, &mut other.0) + BTreeMap::append(self, &mut other) } } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 7c32606a..5d06bbda 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -644,24 +644,6 @@ impl TxGraph { } } - // If we cannot determine whether tx is in best chain, we can check whether a spending tx is - // confirmed and in best chain, and if so, it is guaranteed that this tx is in the best - // chain. - // - // [TODO] This logic is incomplete as we do not check spends of spends. - let spending_anchors = self - .spends - .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) - .flat_map(|(_, spending_txids)| spending_txids) - .filter_map(|spending_txid| self.txs.get(spending_txid)) - .flat_map(|(_, spending_anchors, _)| spending_anchors); - for spending_anchor in spending_anchors { - match chain.is_block_in_chain(spending_anchor.anchor_block(), chain_tip)? { - Some(true) => return Ok(Some(ObservedAs::ConfirmedImplicit(spending_anchor))), - _ => continue, - } - } - // The tx is not anchored to a block which is in the best chain, let's check whether we can // ignore it by checking conflicts! let tx = match tx_node { From 7175a82c04b0a3f0c5716f4a37e122da70b8ceac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 19 Apr 2023 16:14:52 +0800 Subject: [PATCH 34/48] [bdk_chain_redesign] Add tests for `TxGraph::relevant_heights` --- crates/chain/src/tx_graph.rs | 2 +- crates/chain/tests/test_tx_graph.rs | 68 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 5d06bbda..e16ae88d 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -602,7 +602,7 @@ impl TxGraph { impl TxGraph { /// Get all heights that are relevant to the graph. - pub fn relevant_heights(&self) -> impl Iterator + '_ { + pub fn relevant_heights(&self) -> impl DoubleEndedIterator + '_ { let mut visited = HashSet::new(); self.anchors .iter() diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 965bb259..20b3e27f 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -10,6 +10,7 @@ use bitcoin::{ hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid, }; use core::iter; +use std::vec; #[test] fn insert_txouts() { @@ -781,3 +782,70 @@ fn test_chain_spends() { .get_chain_position(&local_chain, tip, tx_2.txid()) .is_none()); } + +#[test] +fn test_relevant_heights() { + let mut graph = TxGraph::::default(); + + let tx1 = common::new_tx(1); + let tx2 = common::new_tx(2); + + let _ = graph.insert_tx(tx1.clone()); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![], + "no anchors in graph" + ); + + let _ = graph.insert_anchor( + tx1.txid(), + BlockId { + height: 3, + hash: h!("3a"), + }, + ); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![3], + "one anchor at height 3" + ); + + let _ = graph.insert_anchor( + tx1.txid(), + BlockId { + height: 3, + hash: h!("3b"), + }, + ); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![3], + "introducing duplicate anchor at height 3, must not iterate over duplicate heights" + ); + + let _ = graph.insert_anchor( + tx1.txid(), + BlockId { + height: 4, + hash: h!("4a"), + }, + ); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![3, 4], + "anchors in height 3 and now 4" + ); + + let _ = graph.insert_anchor( + tx2.txid(), + BlockId { + height: 5, + hash: h!("5a"), + }, + ); + assert_eq!( + graph.relevant_heights().collect::>(), + vec![3, 4, 5], + "anchor for non-existant tx is inserted at height 5, must still be in relevant heights", + ); +} From 1003fe2ee6167e110b0195e2431560b5b222e2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 20 Apr 2023 15:29:20 +0800 Subject: [PATCH 35/48] [bdk_chain_redesign] Test `LocalChain` This is mostly copying over the relevant tests from `SparseChain`. Changes are made to `local_chain::ChangeSet` to re-add the ability to remove blocks. --- crates/chain/src/local_chain.rs | 48 +++++-- crates/chain/tests/common/mod.rs | 8 ++ crates/chain/tests/test_local_chain.rs | 167 +++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 crates/chain/tests/test_local_chain.rs diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 9ba64b28..88c688fe 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -62,6 +62,15 @@ impl From> for LocalChain { } impl LocalChain { + pub fn from_blocks(blocks: B) -> Self + where + B: IntoIterator, + { + Self { + blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(), + } + } + pub fn tip(&self) -> Option { self.blocks .iter() @@ -109,19 +118,37 @@ impl LocalChain { } } - let mut changeset = BTreeMap::::new(); - for (height, new_hash) in update { + let mut changeset: BTreeMap> = match invalidate_from_height { + Some(first_invalid_height) => { + // the first block of height to invalidate should be represented in the update + if !update.contains_key(&first_invalid_height) { + return Err(UpdateNotConnectedError(first_invalid_height)); + } + self.blocks + .range(first_invalid_height..) + .map(|(height, _)| (*height, None)) + .collect() + } + None => BTreeMap::new(), + }; + for (height, update_hash) in update { let original_hash = self.blocks.get(height); - if Some(new_hash) != original_hash { - changeset.insert(*height, *new_hash); + if Some(update_hash) != original_hash { + changeset.insert(*height, Some(*update_hash)); } } + Ok(changeset) } /// Applies the given `changeset`. - pub fn apply_changeset(&mut self, mut changeset: ChangeSet) { - self.blocks.append(&mut changeset) + pub fn apply_changeset(&mut self, changeset: ChangeSet) { + for (height, blockhash) in changeset { + match blockhash { + Some(blockhash) => self.blocks.insert(height, blockhash), + None => self.blocks.remove(&height), + }; + } } /// Updates [`LocalChain`] with an update [`LocalChain`]. @@ -137,7 +164,10 @@ impl LocalChain { } pub fn initial_changeset(&self) -> ChangeSet { - self.blocks.clone() + self.blocks + .iter() + .map(|(&height, &hash)| (height, Some(hash))) + .collect() } pub fn heights(&self) -> BTreeSet { @@ -148,7 +178,7 @@ impl LocalChain { /// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`]. /// /// [`determine_changeset`]: LocalChain::determine_changeset -type ChangeSet = BTreeMap; +pub type ChangeSet = BTreeMap>; impl Append for ChangeSet { fn append(&mut self, mut other: Self) { @@ -163,7 +193,7 @@ impl Append for ChangeSet { /// connect to the existing chain. This error case contains the checkpoint height to include so /// that the chains can connect. #[derive(Clone, Debug, PartialEq)] -pub struct UpdateNotConnectedError(u32); +pub struct UpdateNotConnectedError(pub u32); impl core::fmt::Display for UpdateNotConnectedError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs index e9b7a101..7d7288bd 100644 --- a/crates/chain/tests/common/mod.rs +++ b/crates/chain/tests/common/mod.rs @@ -5,6 +5,14 @@ macro_rules! h { }}; } +#[allow(unused_macros)] +macro_rules! local_chain { + [ $(($height:expr, $block_hash:expr)), * ] => {{ + #[allow(unused_mut)] + bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*]) + }}; +} + #[allow(unused_macros)] macro_rules! chain { ($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) }; diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs new file mode 100644 index 00000000..1aea9850 --- /dev/null +++ b/crates/chain/tests/test_local_chain.rs @@ -0,0 +1,167 @@ +use bdk_chain::local_chain::{LocalChain, UpdateNotConnectedError}; + +#[macro_use] +mod common; + +#[test] +fn add_first_tip() { + let chain = LocalChain::default(); + assert_eq!( + chain.determine_changeset(&local_chain![(0, h!("A"))]), + Ok([(0, Some(h!("A")))].into()), + "add first tip" + ); +} + +#[test] +fn add_second_tip() { + let chain = local_chain![(0, h!("A"))]; + assert_eq!( + chain.determine_changeset(&local_chain![(0, h!("A")), (1, h!("B"))]), + Ok([(1, Some(h!("B")))].into()) + ); +} + +#[test] +fn two_disjoint_chains_cannot_merge() { + let chain1 = local_chain![(0, h!("A"))]; + let chain2 = local_chain![(1, h!("B"))]; + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateNotConnectedError(0)) + ); +} + +#[test] +fn duplicate_chains_should_merge() { + let chain1 = local_chain![(0, h!("A"))]; + let chain2 = local_chain![(0, h!("A"))]; + assert_eq!(chain1.determine_changeset(&chain2), Ok(Default::default())); +} + +#[test] +fn can_introduce_older_checkpoints() { + let chain1 = local_chain![(2, h!("C")), (3, h!("D"))]; + let chain2 = local_chain![(1, h!("B")), (2, h!("C"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([(1, Some(h!("B")))].into()) + ); +} + +#[test] +fn fix_blockhash_before_agreement_point() { + let chain1 = local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))]; + let chain2 = local_chain![(0, h!("fix")), (1, h!("we-agree"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([(0, Some(h!("fix")))].into()) + ) +} + +/// B and C are in both chain and update +/// ``` +/// | 0 | 1 | 2 | 3 | 4 +/// chain | B C +/// update | A B C D +/// ``` +/// This should succeed with the point of agreement being C and A should be added in addition. +#[test] +fn two_points_of_agreement() { + let chain1 = local_chain![(1, h!("B")), (2, h!("C"))]; + let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([(0, Some(h!("A"))), (3, Some(h!("D")))].into()), + ); +} + +/// Update and chain does not connect: +/// ``` +/// | 0 | 1 | 2 | 3 | 4 +/// chain | B C +/// update | A B D +/// ``` +/// This should fail as we cannot figure out whether C & D are on the same chain +#[test] +fn update_and_chain_does_not_connect() { + let chain1 = local_chain![(1, h!("B")), (2, h!("C"))]; + let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateNotConnectedError(2)), + ); +} + +/// Transient invalidation: +/// ``` +/// | 0 | 1 | 2 | 3 | 4 | 5 +/// chain | A B C E +/// update | A B' C' D +/// ``` +/// This should succeed and invalidate B,C and E with point of agreement being A. +#[test] +fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() { + let chain1 = local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))]; + let chain2 = local_chain![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([ + (2, Some(h!("B'"))), + (3, Some(h!("C'"))), + (4, Some(h!("D"))), + (5, None), + ] + .into()) + ); +} + +/// Transient invalidation: +/// ``` +/// | 0 | 1 | 2 | 3 | 4 +/// chain | B C E +/// update | B' C' D +/// ``` +/// +/// This should succeed and invalidate B, C and E with no point of agreement +#[test] +fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation_no_point_of_agreement() { + let chain1 = local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))]; + let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Ok([ + (1, Some(h!("B'"))), + (2, Some(h!("C'"))), + (3, Some(h!("D"))), + (4, None) + ] + .into()) + ) +} + +/// Transient invalidation: +/// ``` +/// | 0 | 1 | 2 | 3 | 4 +/// chain | A B C E +/// update | B' C' D +/// ``` +/// +/// This should fail since although it tells us that B and C are invalid it doesn't tell us whether +/// A was invalid. +#[test] +fn invalidation_but_no_connection() { + let chain1 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))]; + let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))]; + + assert_eq!( + chain1.determine_changeset(&chain2), + Err(UpdateNotConnectedError(0)) + ) +} From 6c495707423cc7ce26c3027893dc812281b73053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 20 Apr 2023 15:56:28 +0800 Subject: [PATCH 36/48] [bdk_chain_redesign] Rm `HashSet` from `TxGraph::relevant_heights` The `HashSet` was used for iterating without duplicate items. However, since `anchors` is a `BTreeSet`, heights are in order. So a single variable tracking last height will be sufficient. --- crates/chain/src/tx_graph.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index e16ae88d..a1ddbf87 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -603,11 +603,17 @@ impl TxGraph { impl TxGraph { /// Get all heights that are relevant to the graph. pub fn relevant_heights(&self) -> impl DoubleEndedIterator + '_ { - let mut visited = HashSet::new(); + let mut last_height = Option::::None; self.anchors .iter() .map(|(a, _)| a.anchor_block().height) - .filter(move |&h| visited.insert(h)) + .filter(move |&height| { + let is_unique = Some(height) != last_height; + if is_unique { + last_height = Some(height); + } + is_unique + }) } /// Get the position of the transaction in `chain` with tip `chain_tip`. From 34a7bf5afe2d78db47b149f4958f0a0365b017d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 20 Apr 2023 18:07:26 +0800 Subject: [PATCH 37/48] [bdk_chain_redesign] Rm unnecessary code and premature optimisation * Remove `chain_oracle::CacheBackend` for now as it is not used. * `SparseChain` does not need to implement `ChainOracle`. * Remove filter predicate for `list..` methods of `TxGraph` and `IndexedTxGraph` as this is premature optimisation. * `Append` can be implemented for all `BTreeMap`s and `BTreeSet`s, instead of only `local_chain::ChangeSet`. --- crates/chain/src/chain_oracle.rs | 56 ----------------- crates/chain/src/indexed_tx_graph.rs | 83 ++++++++++--------------- crates/chain/src/local_chain.rs | 8 +-- crates/chain/src/sparse_chain.rs | 23 +------ crates/chain/src/tx_data_traits.rs | 17 ++++- crates/chain/src/tx_graph.rs | 92 ++++++++-------------------- 6 files changed, 76 insertions(+), 203 deletions(-) diff --git a/crates/chain/src/chain_oracle.rs b/crates/chain/src/chain_oracle.rs index 7e975ad2..2b4ad36f 100644 --- a/crates/chain/src/chain_oracle.rs +++ b/crates/chain/src/chain_oracle.rs @@ -1,9 +1,3 @@ -use crate::collections::HashSet; -use core::marker::PhantomData; - -use alloc::{collections::VecDeque, vec::Vec}; -use bitcoin::BlockHash; - use crate::BlockId; /// Represents a service that tracks the blockchain. @@ -25,53 +19,3 @@ pub trait ChainOracle { static_block: BlockId, ) -> Result, Self::Error>; } - -/// A cache structure increases the performance of getting chain data. -/// -/// A simple FIFO cache replacement policy is used. Something more efficient and advanced can be -/// implemented later. -#[derive(Debug, Default)] -pub struct CacheBackend { - cache: HashSet<(BlockHash, BlockHash)>, - fifo: VecDeque<(BlockHash, BlockHash)>, - marker: PhantomData, -} - -impl CacheBackend { - /// Get the number of elements in the cache. - pub fn cache_size(&self) -> usize { - self.cache.len() - } - - /// Prunes the cache to reach the `max_size` target. - /// - /// Returns pruned elements. - pub fn prune(&mut self, max_size: usize) -> Vec<(BlockHash, BlockHash)> { - let prune_count = self.cache.len().saturating_sub(max_size); - (0..prune_count) - .filter_map(|_| self.fifo.pop_front()) - .filter(|k| self.cache.remove(k)) - .collect() - } - - pub fn contains(&self, static_block: BlockId, block: BlockId) -> bool { - if static_block.height < block.height - || static_block.height == block.height && static_block.hash != block.hash - { - return false; - } - - self.cache.contains(&(static_block.hash, block.hash)) - } - - pub fn insert(&mut self, static_block: BlockId, block: BlockId) -> bool { - let cache_key = (static_block.hash, block.hash); - - if self.cache.insert(cache_key) { - self.fifo.push_back(cache_key); - true - } else { - false - } - } -} diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index fd5aa6d9..5ce53111 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -120,15 +120,12 @@ where /// /// `anchors` can be provided to anchor the transactions to blocks. `seen_at` is a unix /// timestamp of when the transactions are last seen. - pub fn insert_relevant_txs<'t, T>( + pub fn insert_relevant_txs<'t, T: Iterator>( &mut self, txs: T, anchors: impl IntoIterator + Clone, seen_at: Option, - ) -> IndexedAdditions - where - T: Iterator, - { + ) -> IndexedAdditions { txs.filter_map(|tx| match self.index.is_tx_relevant(tx) { true => Some(self.insert_tx(tx, anchors.clone(), seen_at)), false => None, @@ -141,14 +138,11 @@ where } impl IndexedTxGraph { - pub fn try_list_owned_txs<'a, C>( + pub fn try_list_owned_txs<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator, C::Error>> - where - C: ChainOracle + 'a, - { + ) -> impl Iterator, C::Error>> { self.graph .full_txs() .filter(|node| tx_alters_owned_utxo_set(&self.graph, &self.index, node.txid, node.tx)) @@ -165,55 +159,55 @@ impl IndexedTxGraph { }) } - pub fn list_owned_txs<'a, C>( + pub fn list_owned_txs<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator> - where - C: ChainOracle + 'a, - { + ) -> impl Iterator> { self.try_list_owned_txs(chain, chain_tip) .map(|r| r.expect("chain oracle is infallible")) } - pub fn try_list_owned_txouts<'a, C>( + pub fn try_list_owned_txouts<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator>, C::Error>> + 'a - where - C: ChainOracle + 'a, - { + ) -> impl Iterator>, C::Error>> + 'a { self.graph() - .try_list_chain_txouts(chain, chain_tip, |_, txout| { - self.index.is_spk_owned(&txout.script_pubkey) + .try_list_chain_txouts(chain, chain_tip) + .filter(|r| { + if let Ok(full_txout) = r { + if !self.index.is_spk_owned(&full_txout.txout.script_pubkey) { + return false; + } + } + true }) } - pub fn list_owned_txouts<'a, C>( + pub fn list_owned_txouts<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator>> + 'a - where - C: ChainOracle + 'a, - { + ) -> impl Iterator>> + 'a { self.try_list_owned_txouts(chain, chain_tip) .map(|r| r.expect("oracle is infallible")) } - pub fn try_list_owned_unspents<'a, C>( + pub fn try_list_owned_unspents<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator>, C::Error>> + 'a - where - C: ChainOracle + 'a, - { + ) -> impl Iterator>, C::Error>> + 'a { self.graph() - .try_list_chain_unspents(chain, chain_tip, |_, txout| { - self.index.is_spk_owned(&txout.script_pubkey) + .try_list_chain_unspents(chain, chain_tip) + .filter(|r| { + if let Ok(full_txout) = r { + if !self.index.is_spk_owned(&full_txout.txout.script_pubkey) { + return false; + } + } + true }) } @@ -278,35 +272,26 @@ impl IndexedTxGraph { }) } - pub fn balance( - &self, - chain: &C, - static_block: BlockId, - tip: u32, - should_trust: F, - ) -> Balance + pub fn balance(&self, chain: &C, chain_tip: BlockId, tip: u32, should_trust: F) -> Balance where C: ChainOracle, F: FnMut(&Script) -> bool, { - self.try_balance(chain, static_block, tip, should_trust) + self.try_balance(chain, chain_tip, tip, should_trust) .expect("error is infallible") } pub fn try_balance_at( &self, chain: &C, - static_block: BlockId, + chain_tip: BlockId, height: u32, ) -> Result where C: ChainOracle, { let mut sum = 0; - for txo_res in self - .graph() - .try_list_chain_txouts(chain, static_block, |_, _| true) - { + for txo_res in self.try_list_owned_unspents(chain, chain_tip) { let txo = txo_res?; if txo.is_observed_as_confirmed_and_spendable(height) { sum += txo.txout.value; @@ -315,11 +300,11 @@ impl IndexedTxGraph { Ok(sum) } - pub fn balance_at(&self, chain: &C, static_block: BlockId, height: u32) -> u64 + pub fn balance_at(&self, chain: &C, chain_tip: BlockId, height: u32) -> u64 where C: ChainOracle, { - self.try_balance_at(chain, static_block, height) + self.try_balance_at(chain, chain_tip, height) .expect("error is infallible") } } diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 88c688fe..30dfe80b 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -3,7 +3,7 @@ use core::convert::Infallible; use alloc::collections::{BTreeMap, BTreeSet}; use bitcoin::BlockHash; -use crate::{Append, BlockId, ChainOracle}; +use crate::{BlockId, ChainOracle}; /// This is a local implementation of [`ChainOracle`]. /// @@ -180,12 +180,6 @@ impl LocalChain { /// [`determine_changeset`]: LocalChain::determine_changeset pub type ChangeSet = BTreeMap>; -impl Append for ChangeSet { - fn append(&mut self, mut other: Self) { - BTreeMap::append(self, &mut other) - } -} - /// Represents an update failure of [`LocalChain`] due to the update not connecting to the original /// chain. /// diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index fbcdcaa5..55121695 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -307,12 +307,11 @@ //! ); //! ``` use core::{ - convert::Infallible, fmt::Debug, ops::{Bound, RangeBounds}, }; -use crate::{collections::*, tx_graph::TxGraph, BlockId, ChainOracle, FullTxOut, TxHeight}; +use crate::{collections::*, tx_graph::TxGraph, BlockId, FullTxOut, TxHeight}; use bitcoin::{hashes::Hash, BlockHash, OutPoint, Txid}; /// This is a non-monotone structure that tracks relevant [`Txid`]s that are ordered by chain @@ -457,26 +456,6 @@ impl core::fmt::Display for UpdateError

{ #[cfg(feature = "std")] impl std::error::Error for UpdateError

{} -impl

ChainOracle for SparseChain

{ - type Error = Infallible; - - fn is_block_in_chain( - &self, - block: BlockId, - static_block: BlockId, - ) -> Result, Self::Error> { - Ok( - match ( - self.checkpoint_at(block.height), - self.checkpoint_at(static_block.height), - ) { - (Some(b), Some(static_b)) => Some(b == block && static_b == static_block), - _ => None, - }, - ) - } -} - impl

SparseChain

{ /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they /// are in the same chain. diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 9d41c549..bd7e66ac 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,6 +1,7 @@ -use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; - +use crate::collections::BTreeMap; +use crate::collections::BTreeSet; use crate::BlockId; +use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; /// Trait to do something with every txout contained in a structure. /// @@ -77,3 +78,15 @@ pub trait Append { impl Append for () { fn append(&mut self, _other: Self) {} } + +impl Append for BTreeMap { + fn append(&mut self, mut other: Self) { + BTreeMap::append(self, &mut other) + } +} + +impl Append for BTreeSet { + fn append(&mut self, mut other: Self) { + BTreeSet::append(self, &mut other) + } +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index a1ddbf87..07e4680e 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -627,15 +627,12 @@ impl TxGraph { /// [`ChainOracle`] is infallible, [`get_chain_position`] can be used instead. /// /// [`get_chain_position`]: Self::get_chain_position - pub fn try_get_chain_position( + pub fn try_get_chain_position( &self, chain: &C, chain_tip: BlockId, txid: Txid, - ) -> Result>, C::Error> - where - C: ChainOracle, - { + ) -> Result>, C::Error> { let (tx_node, anchors, &last_seen) = match self.txs.get(&txid) { Some((tx, anchors, last_seen)) if !(anchors.is_empty() && *last_seen == 0) => { (tx, anchors, last_seen) @@ -682,15 +679,12 @@ impl TxGraph { /// This is the infallible version of [`try_get_chain_position`]. /// /// [`try_get_chain_position`]: Self::try_get_chain_position - pub fn get_chain_position( + pub fn get_chain_position>( &self, chain: &C, chain_tip: BlockId, txid: Txid, - ) -> Option> - where - C: ChainOracle, - { + ) -> Option> { self.try_get_chain_position(chain, chain_tip, txid) .expect("error is infallible") } @@ -707,15 +701,12 @@ impl TxGraph { /// If the [`ChainOracle`] is infallible, [`get_chain_spend`] can be used instead. /// /// [`get_chain_spend`]: Self::get_chain_spend - pub fn try_get_chain_spend( + pub fn try_get_chain_spend( &self, chain: &C, chain_tip: BlockId, outpoint: OutPoint, - ) -> Result, Txid)>, C::Error> - where - C: ChainOracle, - { + ) -> Result, Txid)>, C::Error> { if self .try_get_chain_position(chain, chain_tip, outpoint.txid)? .is_none() @@ -738,15 +729,12 @@ impl TxGraph { /// This is the infallible version of [`try_get_chain_spend`] /// /// [`try_get_chain_spend`]: Self::try_get_chain_spend - pub fn get_chain_spend( + pub fn get_chain_spend>( &self, chain: &C, static_block: BlockId, outpoint: OutPoint, - ) -> Option<(ObservedAs<&A>, Txid)> - where - C: ChainOracle, - { + ) -> Option<(ObservedAs<&A>, Txid)> { self.try_get_chain_spend(chain, static_block, outpoint) .expect("error is infallible") } @@ -764,14 +752,11 @@ impl TxGraph { /// If the [`ChainOracle`] is infallible, [`list_chain_txs`] can be used instead. /// /// [`list_chain_txs`]: Self::list_chain_txs - pub fn try_list_chain_txs<'a, C>( + pub fn try_list_chain_txs<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator, C::Error>> - where - C: ChainOracle + 'a, - { + ) -> impl Iterator, C::Error>> { self.full_txs().filter_map(move |tx| { self.try_get_chain_position(chain, chain_tip, tx.txid) .map(|v| { @@ -789,14 +774,11 @@ impl TxGraph { /// This is the infallible version of [`try_list_chain_txs`]. /// /// [`try_list_chain_txs`]: Self::try_list_chain_txs - pub fn list_chain_txs<'a, C>( + pub fn list_chain_txs<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator> - where - C: ChainOracle + 'a, - { + ) -> impl Iterator> { self.try_list_chain_txs(chain, chain_tip) .map(|r| r.expect("oracle is infallible")) } @@ -814,16 +796,11 @@ impl TxGraph { /// If the [`ChainOracle`] is infallible, [`list_chain_txouts`] can be used instead. /// /// [`list_chain_txouts`]: Self::list_chain_txouts - pub fn try_list_chain_txouts<'a, C, P>( + pub fn try_list_chain_txouts<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - mut filter_predicate: P, - ) -> impl Iterator>, C::Error>> + 'a - where - C: ChainOracle + 'a, - P: FnMut(OutPoint, &TxOut) -> bool + 'a, - { + ) -> impl Iterator>, C::Error>> + 'a { self.try_list_chain_txs(chain, chain_tip) .flat_map(move |tx_res| match tx_res { Ok(canonical_tx) => canonical_tx @@ -831,13 +808,9 @@ impl TxGraph { .output .iter() .enumerate() - .filter_map(|(vout, txout)| { + .map(|(vout, txout)| { let outpoint = OutPoint::new(canonical_tx.node.txid, vout as _); - if filter_predicate(outpoint, txout) { - Some(Ok((outpoint, txout.clone(), canonical_tx.clone()))) - } else { - None - } + Ok((outpoint, txout.clone(), canonical_tx.clone())) }) .collect::>(), Err(err) => vec![Err(err)], @@ -871,17 +844,12 @@ impl TxGraph { /// This is the infallible version of [`try_list_chain_txouts`]. /// /// [`try_list_chain_txouts`]: Self::try_list_chain_txouts - pub fn list_chain_txouts<'a, C, P>( + pub fn list_chain_txouts<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - filter_predicate: P, - ) -> impl Iterator>> + 'a - where - C: ChainOracle + 'a, - P: FnMut(OutPoint, &TxOut) -> bool + 'a, - { - self.try_list_chain_txouts(chain, chain_tip, filter_predicate) + ) -> impl Iterator>> + 'a { + self.try_list_chain_txouts(chain, chain_tip) .map(|r| r.expect("error in infallible")) } @@ -895,17 +863,12 @@ impl TxGraph { /// infallible, [`list_chain_unspents`] can be used instead. /// /// [`list_chain_unspents`]: Self::list_chain_unspents - pub fn try_list_chain_unspents<'a, C, P>( + pub fn try_list_chain_unspents<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - filter_txout: P, - ) -> impl Iterator>, C::Error>> + 'a - where - C: ChainOracle + 'a, - P: FnMut(OutPoint, &TxOut) -> bool + 'a, - { - self.try_list_chain_txouts(chain, chain_tip, filter_txout) + ) -> impl Iterator>, C::Error>> + 'a { + self.try_list_chain_txouts(chain, chain_tip) .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) } @@ -914,17 +877,12 @@ impl TxGraph { /// This is the infallible version of [`try_list_chain_unspents`]. /// /// [`try_list_chain_unspents`]: Self::try_list_chain_unspents - pub fn list_chain_unspents<'a, C, P>( + pub fn list_chain_unspents<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, static_block: BlockId, - filter_txout: P, - ) -> impl Iterator>> + 'a - where - C: ChainOracle + 'a, - P: FnMut(OutPoint, &TxOut) -> bool + 'a, - { - self.try_list_chain_unspents(chain, static_block, filter_txout) + ) -> impl Iterator>> + 'a { + self.try_list_chain_unspents(chain, static_block) .map(|r| r.expect("error is infallible")) } } From 03c128311a687249238fb109f804cde25ffddad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 21 Apr 2023 12:33:03 +0800 Subject: [PATCH 38/48] [bdk_chain_redesign] Revert changes to `SparseChain` --- crates/chain/src/sparse_chain.rs | 88 ++++++++++++++--------------- crates/chain/src/tx_graph.rs | 2 +- crates/chain/tests/test_tx_graph.rs | 6 +- 3 files changed, 47 insertions(+), 49 deletions(-) diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index 55121695..b9c1e24b 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -456,7 +456,7 @@ impl core::fmt::Display for UpdateError

{ #[cfg(feature = "std")] impl std::error::Error for UpdateError

{} -impl

SparseChain

{ +impl SparseChain

{ /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they /// are in the same chain. pub fn from_checkpoints(checkpoints: C) -> Self @@ -487,6 +487,13 @@ impl

SparseChain

{ .map(|&hash| BlockId { height, hash }) } + /// Return the [`ChainPosition`] of a `txid`. + /// + /// This returns [`None`] if the transaction does not exist. + pub fn tx_position(&self, txid: Txid) -> Option<&P> { + self.txid_to_pos.get(&txid) + } + /// Return a [`BTreeMap`] of all checkpoints (block hashes by height). pub fn checkpoints(&self) -> &BTreeMap { &self.checkpoints @@ -502,47 +509,6 @@ impl

SparseChain

{ .map(|(&height, &hash)| BlockId { height, hash }) } - /// Returns the value set as the checkpoint limit. - /// - /// Refer to [`set_checkpoint_limit`]. - /// - /// [`set_checkpoint_limit`]: Self::set_checkpoint_limit - pub fn checkpoint_limit(&self) -> Option { - self.checkpoint_limit - } - - /// Set the checkpoint limit. - /// - /// The checkpoint limit restricts the number of checkpoints that can be stored in [`Self`]. - /// Oldest checkpoints are pruned first. - pub fn set_checkpoint_limit(&mut self, limit: Option) { - self.checkpoint_limit = limit; - self.prune_checkpoints(); - } - - fn prune_checkpoints(&mut self) -> Option> { - let limit = self.checkpoint_limit?; - - // find the last height to be pruned - let last_height = *self.checkpoints.keys().rev().nth(limit)?; - // first height to be kept - let keep_height = last_height + 1; - - let mut split = self.checkpoints.split_off(&keep_height); - core::mem::swap(&mut self.checkpoints, &mut split); - - Some(split) - } -} - -impl SparseChain

{ - /// Return the [`ChainPosition`] of a `txid`. - /// - /// This returns [`None`] if the transaction does not exist. - pub fn tx_position(&self, txid: Txid) -> Option<&P> { - self.txid_to_pos.get(&txid) - } - /// Preview changes of updating [`Self`] with another chain that connects to it. /// /// If the `update` wishes to introduce confirmed transactions, it must contain a checkpoint @@ -933,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)?; @@ -953,6 +919,24 @@ impl SparseChain

{ }) } + /// Returns the value set as the checkpoint limit. + /// + /// Refer to [`set_checkpoint_limit`]. + /// + /// [`set_checkpoint_limit`]: Self::set_checkpoint_limit + pub fn checkpoint_limit(&self) -> Option { + self.checkpoint_limit + } + + /// Set the checkpoint limit. + /// + /// The checkpoint limit restricts the number of checkpoints that can be stored in [`Self`]. + /// Oldest checkpoints are pruned first. + pub fn set_checkpoint_limit(&mut self, limit: Option) { + self.checkpoint_limit = limit; + self.prune_checkpoints(); + } + /// Return [`Txid`]s that would be added to the sparse chain if this `changeset` was applied. pub fn changeset_additions<'a>( &'a self, @@ -968,15 +952,29 @@ impl SparseChain

{ .map(|(&txid, _)| txid) } + fn prune_checkpoints(&mut self) -> Option> { + let limit = self.checkpoint_limit?; + + // find the last height to be pruned + let last_height = *self.checkpoints.keys().rev().nth(limit)?; + // first height to be kept + let keep_height = last_height + 1; + + let mut split = self.checkpoints.split_off(&keep_height); + core::mem::swap(&mut self.checkpoints, &mut split); + + Some(split) + } + /// Finds the transaction in the chain that spends `outpoint`. /// /// [`TxGraph`] is used to provide the spend relationships. /// /// 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 - .output_spends(outpoint) + .outspends(outpoint) .iter() .find_map(|&txid| Some((self.tx_position(txid)?, txid))) } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 07e4680e..c7c496ef 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -282,7 +282,7 @@ impl TxGraph { /// /// `TxGraph` allows conflicting transactions within the graph. Obviously the transactions in /// the returned set will never be in the same active-chain. - pub fn output_spends(&self, outpoint: OutPoint) -> &HashSet { + pub fn outspends(&self, outpoint: OutPoint) -> &HashSet { self.spends.get(&outpoint).unwrap_or(&self.empty_outspends) } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 20b3e27f..c74a2e99 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -202,7 +202,7 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() { let mut graph = TxGraph::<()>::default(); let _ = graph.insert_tx(tx); - assert!(graph.output_spends(OutPoint::null()).is_empty()); + assert!(graph.outspends(OutPoint::null()).is_empty()); assert!(graph.tx_spends(Txid::all_zeros()).next().is_none()); } @@ -241,10 +241,10 @@ fn insert_tx_graph_keeps_track_of_spend() { let _ = graph2.insert_tx(tx1); assert_eq!( - graph1.output_spends(op), + graph1.outspends(op), &iter::once(tx2.txid()).collect::>() ); - assert_eq!(graph2.output_spends(op), graph1.output_spends(op)); + assert_eq!(graph2.outspends(op), graph1.outspends(op)); } #[test] From f3e7b67bf1195eadf48a8f26f960b3f39d6134f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 21 Apr 2023 13:29:44 +0800 Subject: [PATCH 39/48] [bdk_chain_redesign] Various tweaks and fixes The `ConfirmationHeight` trait has been removed in favour of a second method on the `Anchor` trait: `confirmation_height_upper_bound()`. Methods `try_balance_at()` and `balance_at()` of `IndexedTxGraph` have been removed as they do not provide additional functionality. `IndexedTxGraph::insert_relevant_txs` now uses two loops, the first loop indexes all transactions first. This is done as some indexes require ancestor transactions to be indexed first and we cannot guarantee that the input transactions are in topological order. --- crates/chain/src/chain_data.rs | 18 +++---- crates/chain/src/chain_graph.rs | 2 +- crates/chain/src/indexed_tx_graph.rs | 77 ++++++++++++---------------- crates/chain/src/tx_data_traits.rs | 29 ++++------- crates/chain/src/tx_graph.rs | 2 +- 5 files changed, 53 insertions(+), 75 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 33381ab7..e585d82d 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}, - Anchor, ConfirmationHeight, COINBASE_MATURITY, + Anchor, COINBASE_MATURITY, }; /// Represents an observation of some chain data. @@ -241,20 +241,20 @@ impl FullTxOut

{ } } -impl FullTxOut> { +impl FullTxOut> { /// Whether the `txout` is considered mature. /// /// This is the alternative version of [`is_mature`] which depends on `chain_position` being a /// [`ObservedAs`] where `A` implements [`Anchor`]. /// /// [`is_mature`]: Self::is_mature - pub fn is_observed_as_mature(&self, tip: u32) -> bool { + pub fn is_mature(&self, tip: u32) -> bool { if !self.is_on_coinbase { - return false; + return true; } let tx_height = match &self.chain_position { - ObservedAs::Confirmed(anchor) => anchor.confirmation_height(), + ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), ObservedAs::Unconfirmed(_) => { debug_assert!(false, "coinbase tx can never be unconfirmed"); return false; @@ -271,19 +271,19 @@ impl FullTxOut> { /// Whether the utxo is/was/will be spendable with chain `tip`. /// - /// Currently this method does not take into account the locktime. + /// This method does not take into account the locktime. /// /// This is the alternative version of [`is_spendable_at`] which depends on `chain_position` /// being a [`ObservedAs`] where `A` implements [`Anchor`]. /// /// [`is_spendable_at`]: Self::is_spendable_at - pub fn is_observed_as_confirmed_and_spendable(&self, tip: u32) -> bool { - if !self.is_observed_as_mature(tip) { + pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool { + if !self.is_mature(tip) { return false; } let confirmation_height = match &self.chain_position { - ObservedAs::Confirmed(anchor) => anchor.confirmation_height(), + ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), ObservedAs::Unconfirmed(_) => return false, }; if confirmation_height > tip { diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index 0e3e3439..acf104e7 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -465,7 +465,7 @@ where #[must_use] pub struct ChangeSet

{ pub chain: sparse_chain::ChangeSet

, - pub graph: tx_graph::Additions<()>, + pub graph: tx_graph::Additions, } impl

ChangeSet

{ diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 5ce53111..f44b7847 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,11 +1,12 @@ use core::convert::Infallible; +use alloc::vec::Vec; use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use crate::{ keychain::Balance, tx_graph::{Additions, CanonicalTx, TxGraph}, - Anchor, Append, BlockId, ChainOracle, ConfirmationHeight, FullTxOut, ObservedAs, + Anchor, Append, BlockId, ChainOracle, FullTxOut, ObservedAs, }; /// A struct that combines [`TxGraph`] and an [`Indexer`] implementation. @@ -120,20 +121,34 @@ where /// /// `anchors` can be provided to anchor the transactions to blocks. `seen_at` is a unix /// timestamp of when the transactions are last seen. - pub fn insert_relevant_txs<'t, T: Iterator>( + pub fn insert_relevant_txs<'t>( &mut self, - txs: T, + txs: impl IntoIterator, anchors: impl IntoIterator + Clone, seen_at: Option, ) -> IndexedAdditions { - txs.filter_map(|tx| match self.index.is_tx_relevant(tx) { - true => Some(self.insert_tx(tx, anchors.clone(), seen_at)), - false => None, - }) - .fold(Default::default(), |mut acc, other| { - acc.append(other); - acc - }) + // As mentioned by @LLFourn: This algorithm requires the transactions to be topologically + // sorted because most indexers cannot decide whether something is relevant unless you have + // first inserted its ancestors in the index. We can fix this if we instead do this: + // 1. insert all txs into the index. If they are irrelevant then that's fine it will just + // not store anything about them. + // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` + // returns true or not. (in a second loop). + let txs = txs + .into_iter() + .inspect(|tx| { + let _ = self.index.index_tx(tx); + }) + .collect::>(); + txs.into_iter() + .filter_map(|tx| match self.index.is_tx_relevant(tx) { + true => Some(self.insert_tx(tx, anchors.clone(), seen_at)), + false => None, + }) + .fold(Default::default(), |mut acc, other| { + acc.append(other); + acc + }) } } @@ -222,32 +237,31 @@ impl IndexedTxGraph { self.try_list_owned_unspents(chain, chain_tip) .map(|r| r.expect("oracle is infallible")) } -} -impl IndexedTxGraph { pub fn try_balance( &self, chain: &C, chain_tip: BlockId, - tip: u32, mut should_trust: F, ) -> Result where C: ChainOracle, F: FnMut(&Script) -> bool, { + let tip_height = chain_tip.anchor_block().height; + let mut immature = 0; let mut trusted_pending = 0; let mut untrusted_pending = 0; let mut confirmed = 0; - for res in self.try_list_owned_txouts(chain, chain_tip) { + for res in self.try_list_owned_unspents(chain, chain_tip) { let txout = res?; match &txout.chain_position { ObservedAs::Confirmed(_) => { if txout.is_on_coinbase { - if txout.is_observed_as_mature(tip) { + if txout.is_mature(tip_height) { confirmed += txout.txout.value; } else { immature += txout.txout.value; @@ -272,39 +286,12 @@ impl IndexedTxGraph { }) } - pub fn balance(&self, chain: &C, chain_tip: BlockId, tip: u32, should_trust: F) -> Balance + pub fn balance(&self, chain: &C, chain_tip: BlockId, should_trust: F) -> Balance where C: ChainOracle, F: FnMut(&Script) -> bool, { - self.try_balance(chain, chain_tip, tip, should_trust) - .expect("error is infallible") - } - - pub fn try_balance_at( - &self, - chain: &C, - chain_tip: BlockId, - height: u32, - ) -> Result - where - C: ChainOracle, - { - let mut sum = 0; - for txo_res in self.try_list_owned_unspents(chain, chain_tip) { - let txo = txo_res?; - if txo.is_observed_as_confirmed_and_spendable(height) { - sum += txo.txout.value; - } - } - Ok(sum) - } - - pub fn balance_at(&self, chain: &C, chain_tip: BlockId, height: u32) -> u64 - where - C: ChainOracle, - { - self.try_balance_at(chain, chain_tip, height) + self.try_balance(chain, chain_tip, should_trust) .expect("error is infallible") } } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index bd7e66ac..8ec695ad 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,7 +1,7 @@ use crate::collections::BTreeMap; use crate::collections::BTreeSet; use crate::BlockId; -use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut}; +use bitcoin::{Block, OutPoint, Transaction, TxOut}; /// Trait to do something with every txout contained in a structure. /// @@ -41,11 +41,17 @@ impl ForEachTxOut for Transaction { /// assume that transaction A is also confirmed in the best chain. This does not necessarily mean /// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a /// parent block of B. -pub trait Anchor: - core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash + Send + Sync + 'static -{ +pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash { /// Returns the [`BlockId`] that the associated blockchain data is "anchored" in. fn anchor_block(&self) -> BlockId; + + /// Get the upper bound of the chain data's confirmation height. + /// + /// The default definition gives a pessimistic answer. This can be overridden by the `Anchor` + /// implementation for a more accurate value. + fn confirmation_height_upper_bound(&self) -> u32 { + self.anchor_block().height + } } impl Anchor for &'static A { @@ -54,21 +60,6 @@ impl Anchor for &'static A { } } -impl Anchor for (u32, BlockHash) { - fn anchor_block(&self) -> BlockId { - (*self).into() - } -} - -/// A trait that returns a confirmation height. -/// -/// This is typically used to provide an [`Anchor`] implementation the exact confirmation height of -/// the data being anchored. -pub trait ConfirmationHeight { - /// Returns the confirmation height. - fn confirmation_height(&self) -> u32; -} - /// Trait that makes an object appendable. pub trait Append { /// Append another object of the same type onto `self`. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index c7c496ef..04c9fa27 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -908,7 +908,7 @@ impl TxGraph { ) )] #[must_use] -pub struct Additions { +pub struct Additions { pub tx: BTreeSet, pub txout: BTreeMap, pub anchors: BTreeSet<(A, Txid)>, From 165b874dfedb2e05a7ea5d56e6f80577ada48d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 21 Apr 2023 14:39:13 +0800 Subject: [PATCH 40/48] [bdk_chain_redesign] Add test for `insert_relevant_txs` Ensure `insert_relevant_txs` does not require transactions to be in topological order. Other changes: Rm `try_list_owned_txs` as it is useless --- crates/chain/src/indexed_tx_graph.rs | 52 +--------------- crates/chain/tests/test_indexed_tx_graph.rs | 69 +++++++++++++++++++++ 2 files changed, 71 insertions(+), 50 deletions(-) create mode 100644 crates/chain/tests/test_indexed_tx_graph.rs diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index f44b7847..60a6c646 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,11 +1,11 @@ use core::convert::Infallible; use alloc::vec::Vec; -use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; +use bitcoin::{OutPoint, Script, Transaction, TxOut}; use crate::{ keychain::Balance, - tx_graph::{Additions, CanonicalTx, TxGraph}, + tx_graph::{Additions, TxGraph}, Anchor, Append, BlockId, ChainOracle, FullTxOut, ObservedAs, }; @@ -153,36 +153,6 @@ where } impl IndexedTxGraph { - pub fn try_list_owned_txs<'a, C: ChainOracle + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - ) -> impl Iterator, C::Error>> { - self.graph - .full_txs() - .filter(|node| tx_alters_owned_utxo_set(&self.graph, &self.index, node.txid, node.tx)) - .filter_map(move |tx_node| { - self.graph - .try_get_chain_position(chain, chain_tip, tx_node.txid) - .map(|v| { - v.map(|observed_as| CanonicalTx { - observed_as, - node: tx_node, - }) - }) - .transpose() - }) - } - - pub fn list_owned_txs<'a, C: ChainOracle + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - ) -> impl Iterator> { - self.try_list_owned_txs(chain, chain_tip) - .map(|r| r.expect("chain oracle is infallible")) - } - pub fn try_list_owned_txouts<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, @@ -356,21 +326,3 @@ pub trait OwnedIndexer: Indexer { /// Determines whether a given script pubkey (`spk`) is owned. fn is_spk_owned(&self, spk: &Script) -> bool; } - -fn tx_alters_owned_utxo_set( - graph: &TxGraph, - index: &I, - txid: Txid, - tx: &Transaction, -) -> bool -where - A: Anchor, - I: OwnedIndexer, -{ - let prev_spends = (0..tx.input.len() as u32) - .map(|vout| OutPoint { txid, vout }) - .filter_map(|op| graph.get_txout(op)); - prev_spends - .chain(&tx.output) - .any(|txout| index.is_spk_owned(&txout.script_pubkey)) -} diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs new file mode 100644 index 00000000..e85d424e --- /dev/null +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -0,0 +1,69 @@ +mod common; + +use bdk_chain::{ + indexed_tx_graph::{IndexedAdditions, IndexedTxGraph}, + tx_graph::Additions, + BlockId, SpkTxOutIndex, +}; +use bitcoin::{hashes::hex::FromHex, OutPoint, Script, Transaction, TxIn, TxOut}; + +/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented +/// in topological order. +/// +/// Given 3 transactions (A, B, C), where A has 2 owned outputs. B and C spends an output each of A. +/// Typically, we would only know whether B and C are relevant if we have indexed A (A's outpoints +/// are associated with owned spks in the index). Ensure insertion and indexing is topological- +/// agnostic. +#[test] +fn insert_relevant_txs() { + let mut graph = IndexedTxGraph::>::default(); + + // insert some spks + let spk_0 = Script::from_hex("0014034f9515cace31713707dff8194b8f550eb6d336").unwrap(); + let spk_1 = Script::from_hex("0014beaa39ab2b4f47995c77107d8c3f481d3bd33941").unwrap(); + graph.index.insert_spk(0, spk_0.clone()); + graph.index.insert_spk(1, spk_1.clone()); + + let tx_a = Transaction { + output: vec![ + TxOut { + value: 10_000, + script_pubkey: spk_0, + }, + TxOut { + value: 20_000, + script_pubkey: spk_1, + }, + ], + ..common::new_tx(0) + }; + + let tx_b = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 0), + ..Default::default() + }], + ..common::new_tx(1) + }; + + let tx_c = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx_a.txid(), 1), + ..Default::default() + }], + ..common::new_tx(2) + }; + + let txs = [tx_c, tx_b, tx_a]; + + assert_eq!( + graph.insert_relevant_txs(&txs, None, None), + IndexedAdditions { + graph_additions: Additions { + tx: txs.into(), + ..Default::default() + }, + ..Default::default() + } + ) +} From ac336aa32f485cb253ac7ea5cae3b4bcc78cf507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sat, 22 Apr 2023 22:56:51 +0800 Subject: [PATCH 41/48] [bdk_chain_redesign] Make `insert_relevant_txs` topologically-agnostic The `insert_relevant_txs` test has also been changed to used `KeychainTxOutIndex` so that index additions can be checked (`SpkTxOutIndex` has no additions). Additionally, generic bounds of some `IndexedTxGraph` list methods have been fixed. --- crates/chain/src/indexed_tx_graph.rs | 50 ++++++++++----------- crates/chain/tests/test_indexed_tx_graph.rs | 22 +++++---- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 60a6c646..20d9958e 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -117,7 +117,7 @@ where /// Insert relevant transactions from the given `txs` iterator. /// /// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant - /// transactions in `txs` will be ignored. + /// transactions in `txs` will be ignored. Also, `txs` does not need to be in topological order. /// /// `anchors` can be provided to anchor the transactions to blocks. `seen_at` is a unix /// timestamp of when the transactions are last seen. @@ -127,28 +127,31 @@ where anchors: impl IntoIterator + Clone, seen_at: Option, ) -> IndexedAdditions { - // As mentioned by @LLFourn: This algorithm requires the transactions to be topologically - // sorted because most indexers cannot decide whether something is relevant unless you have - // first inserted its ancestors in the index. We can fix this if we instead do this: + // The algorithm below allows for non-topologically ordered transactions by using two loops. + // This is achieved by: // 1. insert all txs into the index. If they are irrelevant then that's fine it will just // not store anything about them. // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` // returns true or not. (in a second loop). - let txs = txs - .into_iter() - .inspect(|tx| { - let _ = self.index.index_tx(tx); - }) - .collect::>(); - txs.into_iter() - .filter_map(|tx| match self.index.is_tx_relevant(tx) { - true => Some(self.insert_tx(tx, anchors.clone(), seen_at)), - false => None, - }) - .fold(Default::default(), |mut acc, other| { - acc.append(other); - acc - }) + let mut additions = IndexedAdditions::::default(); + let mut transactions = Vec::new(); + for tx in txs.into_iter() { + additions.index_additions.append(self.index.index_tx(tx)); + transactions.push(tx); + } + additions.append( + transactions + .into_iter() + .filter_map(|tx| match self.index.is_tx_relevant(tx) { + true => Some(self.insert_tx(tx, anchors.clone(), seen_at)), + false => None, + }) + .fold(Default::default(), |mut acc, other| { + acc.append(other); + acc + }), + ); + additions } } @@ -170,7 +173,7 @@ impl IndexedTxGraph { }) } - pub fn list_owned_txouts<'a, C: ChainOracle + 'a>( + pub fn list_owned_txouts<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, @@ -196,14 +199,11 @@ impl IndexedTxGraph { }) } - pub fn list_owned_unspents<'a, C>( + pub fn list_owned_unspents<'a, C: ChainOracle + 'a>( &'a self, chain: &'a C, chain_tip: BlockId, - ) -> impl Iterator>> + 'a - where - C: ChainOracle + 'a, - { + ) -> impl Iterator>> + 'a { self.try_list_owned_unspents(chain, chain_tip) .map(|r| r.expect("oracle is infallible")) } diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index e85d424e..26a30cb8 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -2,10 +2,12 @@ mod common; use bdk_chain::{ indexed_tx_graph::{IndexedAdditions, IndexedTxGraph}, + keychain::{DerivationAdditions, KeychainTxOutIndex}, tx_graph::Additions, - BlockId, SpkTxOutIndex, + BlockId, }; -use bitcoin::{hashes::hex::FromHex, OutPoint, Script, Transaction, TxIn, TxOut}; +use bitcoin::{secp256k1::Secp256k1, OutPoint, Transaction, TxIn, TxOut}; +use miniscript::Descriptor; /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented /// in topological order. @@ -16,13 +18,15 @@ use bitcoin::{hashes::hex::FromHex, OutPoint, Script, Transaction, TxIn, TxOut}; /// agnostic. #[test] fn insert_relevant_txs() { - let mut graph = IndexedTxGraph::>::default(); + const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)"; + let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR) + .expect("must be valid"); + let spk_0 = descriptor.at_derivation_index(0).script_pubkey(); + let spk_1 = descriptor.at_derivation_index(9).script_pubkey(); - // insert some spks - let spk_0 = Script::from_hex("0014034f9515cace31713707dff8194b8f550eb6d336").unwrap(); - let spk_1 = Script::from_hex("0014beaa39ab2b4f47995c77107d8c3f481d3bd33941").unwrap(); - graph.index.insert_spk(0, spk_0.clone()); - graph.index.insert_spk(1, spk_1.clone()); + let mut graph = IndexedTxGraph::>::default(); + graph.index.add_keychain((), descriptor); + graph.index.set_lookahead(&(), 10); let tx_a = Transaction { output: vec![ @@ -63,7 +67,7 @@ fn insert_relevant_txs() { tx: txs.into(), ..Default::default() }, - ..Default::default() + index_additions: DerivationAdditions([((), 9_u32)].into()), } ) } From ecc74ce4cd1ab0c51c2e9bcaa4f7d53390dd4d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sat, 22 Apr 2023 23:39:49 +0800 Subject: [PATCH 42/48] [bdk_chain_redesign] Docs for `is_mature` and `is_confirmed_and_spendable` Docs are updated to explain why `is_mature` and `is_confirmed_and_spendable` may return false-negatives. --- crates/chain/src/chain_data.rs | 35 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index e585d82d..a360b304 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -247,23 +247,25 @@ impl FullTxOut> { /// This is the alternative version of [`is_mature`] which depends on `chain_position` being a /// [`ObservedAs`] where `A` implements [`Anchor`]. /// + /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this + /// method may return false-negatives. In other words, interpretted confirmation count may be + /// less than the actual value. + /// /// [`is_mature`]: Self::is_mature + /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound pub fn is_mature(&self, tip: u32) -> bool { - if !self.is_on_coinbase { - return true; - } - - let tx_height = match &self.chain_position { - ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), - ObservedAs::Unconfirmed(_) => { - debug_assert!(false, "coinbase tx can never be unconfirmed"); + if self.is_on_coinbase { + let tx_height = match &self.chain_position { + ObservedAs::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), + ObservedAs::Unconfirmed(_) => { + debug_assert!(false, "coinbase tx can never be unconfirmed"); + return false; + } + }; + let age = tip.saturating_sub(tx_height); + if age + 1 < COINBASE_MATURITY { return false; } - }; - - let age = tip.saturating_sub(tx_height); - if age + 1 < COINBASE_MATURITY { - return false; } true @@ -276,7 +278,12 @@ impl FullTxOut> { /// This is the alternative version of [`is_spendable_at`] which depends on `chain_position` /// being a [`ObservedAs`] where `A` implements [`Anchor`]. /// + /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this + /// method may return false-negatives. In other words, interpretted confirmation count may be + /// less than the actual value. + /// /// [`is_spendable_at`]: Self::is_spendable_at + /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool { if !self.is_mature(tip) { return false; @@ -300,5 +307,3 @@ impl FullTxOut> { true } } - -// TODO: make test From 1b152647c557d2ff492cd241999c5320f7c4f6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sun, 23 Apr 2023 00:12:41 +0800 Subject: [PATCH 43/48] [bdk_chain_redesign] Change `insert_relevant_txs` method Instead of forcing all transactions inserted to use the same anchors, we change the API to have unique anchors per transaction. This allows for more flexibility in general. For example, use `Anchor` implementations that contain the position in a block of a transaction. --- crates/chain/src/indexed_tx_graph.rs | 13 ++++++------- crates/chain/tests/test_indexed_tx_graph.rs | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 20d9958e..c4ee3209 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -117,14 +117,13 @@ where /// Insert relevant transactions from the given `txs` iterator. /// /// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant - /// transactions in `txs` will be ignored. Also, `txs` does not need to be in topological order. + /// transactions in `txs` will be ignored. `txs` do not need to be in topological order. /// /// `anchors` can be provided to anchor the transactions to blocks. `seen_at` is a unix /// timestamp of when the transactions are last seen. pub fn insert_relevant_txs<'t>( &mut self, - txs: impl IntoIterator, - anchors: impl IntoIterator + Clone, + txs: impl IntoIterator)>, seen_at: Option, ) -> IndexedAdditions { // The algorithm below allows for non-topologically ordered transactions by using two loops. @@ -135,15 +134,15 @@ where // returns true or not. (in a second loop). let mut additions = IndexedAdditions::::default(); let mut transactions = Vec::new(); - for tx in txs.into_iter() { + for (tx, anchors) in txs.into_iter() { additions.index_additions.append(self.index.index_tx(tx)); - transactions.push(tx); + transactions.push((tx, anchors)); } additions.append( transactions .into_iter() - .filter_map(|tx| match self.index.is_tx_relevant(tx) { - true => Some(self.insert_tx(tx, anchors.clone(), seen_at)), + .filter_map(|(tx, anchors)| match self.index.is_tx_relevant(tx) { + true => Some(self.insert_tx(tx, anchors, seen_at)), false => None, }) .fold(Default::default(), |mut acc, other| { diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 26a30cb8..4ca340d1 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -61,7 +61,7 @@ fn insert_relevant_txs() { let txs = [tx_c, tx_b, tx_a]; assert_eq!( - graph.insert_relevant_txs(&txs, None, None), + graph.insert_relevant_txs(txs.iter().map(|tx| (tx, None)), None), IndexedAdditions { graph_additions: Additions { tx: txs.into(), From f101dde09b6067c17b0b64ea3e3efd358a32a820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 26 Apr 2023 01:09:19 +0800 Subject: [PATCH 44/48] [bdk_chain_redesign] Fix `tx_graph::Additions::append` logic --- crates/chain/src/tx_graph.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 04c9fa27..4f549dc8 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -947,9 +947,14 @@ 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) + where + A: Ord, + { self.tx.append(&mut other.tx); self.txout.append(&mut other.txout); + self.anchors.append(&mut other.anchors); + self.last_seen.append(&mut other.last_seen); } } From e536307e5c5532fc0cac0657a26609cfd26115bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 28 Apr 2023 18:54:36 +0800 Subject: [PATCH 45/48] [bdk_chain_redesign] Fix `tx_graph::Additions::append` logic * `Additions` now implements `Append` and uses `Append` to implement `append()`. * `append()` logic enforces that `last_seen` values should only increase. * Test written for `append()` with `last_seen` behaviour. --- crates/chain/src/chain_graph.rs | 2 +- crates/chain/src/tx_graph.rs | 36 +++++++++++++---------------- crates/chain/tests/test_tx_graph.rs | 33 +++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index acf104e7..47845c5a 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -3,7 +3,7 @@ use crate::{ collections::HashSet, sparse_chain::{self, ChainPosition, SparseChain}, tx_graph::{self, TxGraph}, - BlockId, ForEachTxOut, FullTxOut, TxHeight, + Append, BlockId, ForEachTxOut, FullTxOut, TxHeight, }; use alloc::{string::ToString, vec::Vec}; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 4f549dc8..ca6d0788 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,9 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, Anchor, BlockId, ChainOracle, ForEachTxOut, FullTxOut, ObservedAs}; +use crate::{ + collections::*, Anchor, Append, BlockId, ChainOracle, ForEachTxOut, FullTxOut, ObservedAs, +}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::{ @@ -112,17 +114,6 @@ impl<'a, T, A> Deref for TxNode<'a, T, A> { } } -impl<'a, A> TxNode<'a, Transaction, A> { - pub fn from_tx(tx: &'a Transaction, anchors: &'a BTreeSet) -> Self { - Self { - txid: tx.txid(), - tx, - anchors, - last_seen_unconfirmed: 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 @@ -602,7 +593,7 @@ impl TxGraph { impl TxGraph { /// Get all heights that are relevant to the graph. - pub fn relevant_heights(&self) -> impl DoubleEndedIterator + '_ { + pub fn relevant_heights(&self) -> impl Iterator + '_ { let mut last_height = Option::::None; self.anchors .iter() @@ -944,17 +935,22 @@ impl Additions { }) .chain(self.txout.iter().map(|(op, txout)| (*op, txout))) } +} - /// 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) - where - A: Ord, - { +impl Append for Additions { + fn append(&mut self, mut other: Self) { self.tx.append(&mut other.tx); self.txout.append(&mut other.txout); self.anchors.append(&mut other.anchors); - self.last_seen.append(&mut other.last_seen); + + // last_seen timestamps should only increase + self.last_seen.extend( + other + .last_seen + .into_iter() + .filter(|(txid, update_ls)| self.last_seen.get(txid) < Some(update_ls)) + .collect::>(), + ); } } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index c74a2e99..41b2ae02 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -4,7 +4,7 @@ use bdk_chain::{ collections::*, local_chain::LocalChain, tx_graph::{Additions, TxGraph}, - BlockId, ObservedAs, + Append, BlockId, ObservedAs, }; use bitcoin::{ hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid, @@ -849,3 +849,34 @@ fn test_relevant_heights() { "anchor for non-existant tx is inserted at height 5, must still be in relevant heights", ); } + +/// Ensure that `last_seen` values only increase during [`Append::append`]. +#[test] +fn test_additions_last_seen_append() { + let txid: Txid = h!("test txid"); + + let test_cases: &[(Option, Option)] = &[ + (Some(5), Some(6)), + (Some(5), Some(5)), + (Some(6), Some(5)), + (None, Some(5)), + (Some(5), None), + ]; + + for (original_ls, update_ls) in test_cases { + let mut original = Additions::<()> { + last_seen: original_ls.map(|ls| (txid, ls)).into_iter().collect(), + ..Default::default() + }; + let update = Additions::<()> { + last_seen: update_ls.map(|ls| (txid, ls)).into_iter().collect(), + ..Default::default() + }; + + original.append(update); + assert_eq!( + &original.last_seen.get(&txid).cloned(), + Ord::max(original_ls, update_ls), + ); + } +} From 911af34f509ad0b4c86c0be819d79b202679e3e5 Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Thu, 27 Apr 2023 19:38:25 +0530 Subject: [PATCH 46/48] [bdk_chain_redesign] Fix calculation bugs. * `IndexedTxGraph::try_balance` should include "confirmed and spendable" into `confirmed` balance. * `TxGraph::try_list_chain_unspents` filter logic should be reversed. --- crates/chain/src/indexed_tx_graph.rs | 10 ++++------ crates/chain/src/tx_graph.rs | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index c4ee3209..6d8c16ff 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -229,12 +229,10 @@ impl IndexedTxGraph { match &txout.chain_position { ObservedAs::Confirmed(_) => { - if txout.is_on_coinbase { - if txout.is_mature(tip_height) { - confirmed += txout.txout.value; - } else { - immature += txout.txout.value; - } + if txout.is_confirmed_and_spendable(tip_height) { + confirmed += txout.txout.value; + } else if !txout.is_mature(tip_height) { + immature += txout.txout.value; } } ObservedAs::Unconfirmed(_) => { diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index ca6d0788..cee688be 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -860,7 +860,7 @@ impl TxGraph { chain_tip: BlockId, ) -> impl Iterator>, C::Error>> + 'a { self.try_list_chain_txouts(chain, chain_tip) - .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) + .filter(|r| matches!(r, Ok(txo) if txo.spent_by.is_none())) } /// List unspent outputs (UTXOs) that are in `chain` with `chain_tip`. From 8cd0328eec0b55f10dd085597a435b689b37444a Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Thu, 27 Apr 2023 19:38:35 +0530 Subject: [PATCH 47/48] [bdk_chain_redesign] Implement `OwnedIndexer` for indexers `SpkTxOutIndex` and `KeychainTxOutIndex` now both implement `OwnedIndexer`. --- crates/chain/src/keychain/txout_index.rs | 8 +++++++- crates/chain/src/spk_txout_index.rs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/chain/src/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index e4ac3ef4..fbe67d1f 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,6 +1,6 @@ use crate::{ collections::*, - indexed_tx_graph::Indexer, + indexed_tx_graph::{Indexer, OwnedIndexer}, miniscript::{Descriptor, DescriptorPublicKey}, ForEachTxOut, SpkTxOutIndex, }; @@ -111,6 +111,12 @@ impl Indexer for KeychainTxOutIndex { } } +impl OwnedIndexer for KeychainTxOutIndex { + fn is_spk_owned(&self, spk: &Script) -> bool { + self.inner().is_spk_owned(spk) + } +} + impl KeychainTxOutIndex { /// Scans an object for relevant outpoints, which are stored and indexed internally. /// diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 3e89ba39..9fdf3bc0 100644 --- a/crates/chain/src/spk_txout_index.rs +++ b/crates/chain/src/spk_txout_index.rs @@ -2,7 +2,7 @@ use core::ops::RangeBounds; use crate::{ collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap}, - indexed_tx_graph::Indexer, + indexed_tx_graph::{Indexer, OwnedIndexer}, ForEachTxOut, }; use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid}; @@ -75,6 +75,12 @@ impl Indexer for SpkTxOutIndex { } } +impl OwnedIndexer for SpkTxOutIndex { + fn is_spk_owned(&self, spk: &Script) -> bool { + self.spk_indices.get(spk).is_some() + } +} + /// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a /// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a /// reference out of the `ForEachTxOut` closure during scanning. From b799a5728b7e08a18059ffee7fe7ee6078354977 Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Thu, 27 Apr 2023 19:39:21 +0530 Subject: [PATCH 48/48] [bdk_chain_redesign] Add tests for `IndexedTxGraph` with `LocalChain` These tests cover list_txout, list_utxo and balance methods. --- crates/chain/tests/test_indexed_tx_graph.rs | 473 +++++++++++++++++- .../chain/tests/test_keychain_txout_index.rs | 3 +- 2 files changed, 471 insertions(+), 5 deletions(-) diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 4ca340d1..4e847aec 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -1,12 +1,15 @@ +#[macro_use] mod common; +use std::collections::{BTreeMap, BTreeSet}; + use bdk_chain::{ indexed_tx_graph::{IndexedAdditions, IndexedTxGraph}, - keychain::{DerivationAdditions, KeychainTxOutIndex}, + keychain::{Balance, DerivationAdditions, KeychainTxOutIndex}, tx_graph::Additions, - BlockId, + BlockId, ObservedAs, }; -use bitcoin::{secp256k1::Secp256k1, OutPoint, Transaction, TxIn, TxOut}; +use bitcoin::{secp256k1::Secp256k1, BlockHash, OutPoint, Script, Transaction, TxIn, TxOut}; use miniscript::Descriptor; /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented @@ -71,3 +74,467 @@ fn insert_relevant_txs() { } ) } + +#[test] +fn test_list_owned_txouts() { + let mut local_chain = local_chain![ + (0, h!("Block 0")), + (1, h!("Block 1")), + (2, h!("Block 2")), + (3, h!("Block 3")) + ]; + + let desc_1 : &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)"; + let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), desc_1).unwrap(); + let desc_2 : &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)"; + let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), desc_2).unwrap(); + + let mut graph = IndexedTxGraph::>::default(); + + graph.index.add_keychain("keychain_1".into(), desc_1); + graph.index.add_keychain("keychain_2".into(), desc_2); + + graph.index.set_lookahead_for_all(10); + + let mut trusted_spks = Vec::new(); + let mut untrusted_spks = Vec::new(); + + { + for _ in 0..10 { + let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string()); + trusted_spks.push(script.clone()); + } + } + + { + for _ in 0..10 { + let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string()); + untrusted_spks.push(script.clone()); + } + } + + let trust_predicate = |spk: &Script| trusted_spks.contains(spk); + + // tx1 is coinbase transaction received at trusted keychain at block 0. + let tx1 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut { + value: 70000, + script_pubkey: trusted_spks[0].clone(), + }], + ..common::new_tx(0) + }; + + // tx2 is an incoming transaction received at untrusted keychain at block 1. + let tx2 = Transaction { + output: vec![TxOut { + value: 30000, + script_pubkey: untrusted_spks[0].clone(), + }], + ..common::new_tx(0) + }; + + // tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2. + let tx3 = Transaction { + input: vec![TxIn { + previous_output: OutPoint::new(tx2.txid(), 0), + ..Default::default() + }], + output: vec![TxOut { + value: 10000, + script_pubkey: trusted_spks[1].clone(), + }], + ..common::new_tx(0) + }; + + // tx4 is an external transaction receiving at untrusted keychain, unconfirmed. + let tx4 = Transaction { + output: vec![TxOut { + value: 20000, + script_pubkey: untrusted_spks[1].clone(), + }], + ..common::new_tx(0) + }; + + // tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed. + let tx5 = Transaction { + output: vec![TxOut { + value: 15000, + script_pubkey: trusted_spks[2].clone(), + }], + ..common::new_tx(0) + }; + + // tx6 is an unrelated transaction confirmed at 3. + let tx6 = common::new_tx(0); + + let _ = graph.insert_relevant_txs( + [&tx1, &tx2, &tx3, &tx6] + .iter() + .enumerate() + .map(|(i, tx)| (*tx, [local_chain.get_block(i as u32).unwrap()])), + None, + ); + + let _ = graph.insert_relevant_txs([&tx4, &tx5].iter().map(|tx| (*tx, None)), Some(100)); + + // AT Block 0 + { + let txouts = graph + .list_owned_txouts(&local_chain, local_chain.get_block(0).unwrap()) + .collect::>(); + + let utxos = graph + .list_owned_unspents(&local_chain, local_chain.get_block(0).unwrap()) + .collect::>(); + + let balance = graph.balance( + &local_chain, + local_chain.get_block(0).unwrap(), + trust_predicate, + ); + + let confirmed_txouts_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txout_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + assert_eq!(txouts.len(), 5); + assert_eq!(utxos.len(), 4); + + assert_eq!(confirmed_txouts_txid, [tx1.txid()].into()); + assert_eq!( + unconfirmed_txout_txid, + [tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into() + ); + + assert_eq!(confirmed_utxos_txid, [tx1.txid()].into()); + assert_eq!( + unconfirmed_utxos_txid, + [tx3.txid(), tx4.txid(), tx5.txid()].into() + ); + + assert_eq!( + balance, + Balance { + immature: 70000, // immature coinbase + trusted_pending: 25000, // tx3 + tx5 + untrusted_pending: 20000, // tx4 + confirmed: 0 // Nothing is confirmed yet + } + ); + } + + // AT Block 1 + { + let txouts = graph + .list_owned_txouts(&local_chain, local_chain.get_block(1).unwrap()) + .collect::>(); + + let utxos = graph + .list_owned_unspents(&local_chain, local_chain.get_block(1).unwrap()) + .collect::>(); + + let balance = graph.balance( + &local_chain, + local_chain.get_block(1).unwrap(), + trust_predicate, + ); + + let confirmed_txouts_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txout_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + assert_eq!(txouts.len(), 5); + assert_eq!(utxos.len(), 4); + + // tx2 gets into confirmed txout set + assert_eq!(confirmed_txouts_txid, [tx1.txid(), tx2.txid()].into()); + assert_eq!( + unconfirmed_txout_txid, + [tx3.txid(), tx4.txid(), tx5.txid()].into() + ); + + // tx2 doesn't get into confirmed utxos set + assert_eq!(confirmed_utxos_txid, [tx1.txid()].into()); + assert_eq!( + unconfirmed_utxos_txid, + [tx3.txid(), tx4.txid(), tx5.txid()].into() + ); + + // Balance breakup remains same + assert_eq!( + balance, + Balance { + immature: 70000, // immature coinbase + trusted_pending: 25000, // tx3 + tx5 + untrusted_pending: 20000, // tx4 + confirmed: 0 // Nothing is confirmed yet + } + ); + } + + // AT Block 2 + { + let txouts = graph + .list_owned_txouts(&local_chain, local_chain.get_block(2).unwrap()) + .collect::>(); + + let utxos = graph + .list_owned_unspents(&local_chain, local_chain.get_block(2).unwrap()) + .collect::>(); + + let balance = graph.balance( + &local_chain, + local_chain.get_block(2).unwrap(), + trust_predicate, + ); + + let confirmed_txouts_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txout_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + assert_eq!(txouts.len(), 5); + assert_eq!(utxos.len(), 4); + + // tx3 now gets into the confirmed txout set + assert_eq!( + confirmed_txouts_txid, + [tx1.txid(), tx2.txid(), tx3.txid()].into() + ); + assert_eq!(unconfirmed_txout_txid, [tx4.txid(), tx5.txid()].into()); + + // tx3 also gets into confirmed utxo set + assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into()); + assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into()); + + assert_eq!( + balance, + Balance { + immature: 70000, // immature coinbase + trusted_pending: 15000, // tx5 + untrusted_pending: 20000, // tx4 + confirmed: 10000 // tx3 got confirmed + } + ); + } + + // AT Block 110 + { + let mut local_chain_extension = (4..150) + .map(|i| (i as u32, h!("random"))) + .collect::>(); + + local_chain_extension.insert(3, h!("Block 3")); + + local_chain + .apply_update(local_chain_extension.into()) + .unwrap(); + + let txouts = graph + .list_owned_txouts(&local_chain, local_chain.get_block(110).unwrap()) + .collect::>(); + + let utxos = graph + .list_owned_unspents(&local_chain, local_chain.get_block(110).unwrap()) + .collect::>(); + + let balance = graph.balance( + &local_chain, + local_chain.get_block(110).unwrap(), + trust_predicate, + ); + + let confirmed_txouts_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txout_txid = txouts + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|full_txout| { + if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + println!("TxOuts : {:#?}", txouts); + println!("UTXOS {:#?}", utxos); + println!("{:#?}", balance); + + assert_eq!(txouts.len(), 5); + assert_eq!(utxos.len(), 4); + + assert_eq!( + confirmed_txouts_txid, + [tx1.txid(), tx2.txid(), tx3.txid()].into() + ); + assert_eq!(unconfirmed_txout_txid, [tx4.txid(), tx5.txid()].into()); + + assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into()); + assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into()); + + assert_eq!( + balance, + Balance { + immature: 0, // immature coinbase + trusted_pending: 15000, // tx5 + untrusted_pending: 20000, // tx4 + confirmed: 80000 // tx1 got matured + } + ); + } +} diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs index 07c7f48d..5f586584 100644 --- a/crates/chain/tests/test_keychain_txout_index.rs +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -293,7 +293,6 @@ fn test_wildcard_derivations() { let _ = txout_index.reveal_to_target(&TestKeychain::External, 25); (0..=15) - .into_iter() .chain(vec![17, 20, 23].into_iter()) .for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index))); @@ -310,7 +309,7 @@ fn test_wildcard_derivations() { // - Use all the derived till 26. // - next_unused() = ((27, ), DerivationAdditions) - (0..=26).into_iter().for_each(|index| { + (0..=26).for_each(|index| { txout_index.mark_used(&TestKeychain::External, index); });