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/30] [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 67032cd3c..65d3008b7 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 dbd4811c1..150d33aa0 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 59444d7f9..ec76dbb7d 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 acf104e79..1a6ccb1e0 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 321769360..92d72841f 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 1a3ffab02..f0bc8d116 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 fff5ee2b4..db4e8d893 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 b9c1e24ba..a449638d8 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 432592b82..9b9facabe 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 3326ac4a2..824b68e20 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 + } + (None, TxNode::Partial(update_txos)) => { + additions.txout.extend( + update_txos + .iter() + .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.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()); - } - } + (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 Default for Additions { + fn default() -> Self { + Self { + tx: Default::default(), + txout: Default::default(), + anchors: Default::default(), + last_seen: Default::default(), + } + } } -impl Additions { +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 68f50b8f7..cd2a28943 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 3bf0a1d50..1c5e07956 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 04974bf30..2550d5568 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 bddbd8f25..d062cfdc3 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 266fd30b6..420f1197a 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 c22668a53..d4a511ac7 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 824e3ccc5..ba0dc21db 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 e33474194..a9673be94 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 c8b9e0684..08f29ceb7 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 cae5e9601..04d121d23 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 df42df1ac..e118cbf43 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/30] [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 ec76dbb7d..147ce2402 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 92d72841f..dd419db56 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 feb71edb4..b60e0584c 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 a449638d8..eb6e3e2ad 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 7f46604fc..3ce6c06c8 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 9b9facabe..43ce487e0 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 824b68e20..daa7e1ba8 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/30] [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 65d3008b7..194c6c901 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 147ce2402..43eb64f6e 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 1a6ccb1e0..fcb980433 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 b60e0584c..176254b4a 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 3ce6c06c8..3d2f783e3 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 43ce487e0..f412f4529 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 daa7e1ba8..3181ed2a7 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 cd2a28943..f7b39d2b0 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 1c5e07956..b4e51d850 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 2550d5568..107e106d5 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/30] [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 3181ed2a7..63b753246 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, + { + 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, { - let mut txouts = self.txouts_in_chain(chain)?; - txouts.retain(|txo| txo.txout.spent_by.is_none()); - Ok(txouts) + 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/30] [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 000000000..b0d547c78 --- /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 4e49e34ed..528440979 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 63b753246..ddeb5e13e 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/30] [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 b0d547c78..5071fb2c7 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 176254b4a..d19aada7a 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 3d2f783e3..3d1af9485 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 f412f4529..2ffb9a608 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/30] [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 43eb64f6e..df5a5e9c7 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 5071fb2c7..5361437e1 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/30] [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 5361437e1..91ecd5719 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/30] [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 91ecd5719..4245e57d9 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/30] [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 4245e57d9..08926f564 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/30] [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 08926f564..b2b206cfc 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/30] [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 194c6c901..6e4adf74b 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 150d33aa0..dbd4811c1 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 fcb980433..3c841965a 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 dd419db56..da2af6f25 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 f0bc8d116..1a3ffab02 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 db4e8d893..fff5ee2b4 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 f7b39d2b0..0514acc99 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 b4e51d850..c3fee3475 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 d062cfdc3..bddbd8f25 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 420f1197a..266fd30b6 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 d4a511ac7..c22668a53 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 ba0dc21db..824e3ccc5 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 a9673be94..e33474194 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 08f29ceb7..c8b9e0684 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 04d121d23..cae5e9601 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 e118cbf43..df42df1ac 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/30] [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 df5a5e9c7..6c1c2c3a6 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 b2b206cfc..21852150a 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 528440979..9319d4acc 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 000000000..5bcb524f3 --- /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 2ffb9a608..485e3f703 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/30] [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 6e4adf74b..84040d5fd 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 3c841965a..85a4c956e 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 ddeb5e13e..2236822b2 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/30] [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 84040d5fd..67032cd3c 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 85a4c956e..8c954f8da 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 21852150a..2e0315d8d 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 2236822b2..3aca6a6fd 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 0514acc99..b5cbf5b9b 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 c3fee3475..bd8c6e031 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 107e106d5..279ddb74b 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/30] [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 2e0315d8d..0b27150c6 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 da2af6f25..53da284f2 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 d19aada7a..101278b7a 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 3d1af9485..6c9739be8 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 485e3f703..0e2474c44 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/30] [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 3aca6a6fd..bbdd7bdbe 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/30] [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 0b27150c6..79e5105ce 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 5bcb524f3..5d459a15f 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/30] [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 79e5105ce..a3996c132 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/30] [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 000000000..ccf3bc09a --- /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 9319d4acc..265276234 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 5d459a15f..a1ca921b9 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 eb6e3e2ad..7f0b67e50 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 0e2474c44..366fc34b8 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 bbdd7bdbe..893060ae3 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/30] [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 6c1c2c3a6..85f9107c1 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 8c954f8da..0e3e3439e 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 a3996c132..e2d71af1c 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 a1ca921b9..fb7d008d1 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 7f0b67e50..b615f4aac 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 893060ae3..620a2dc3d 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/30] [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 e2d71af1c..450d02b82 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 53da284f2..81503049b 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 101278b7a..fc4c4e62f 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 366fc34b8..716b45f18 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 df42df1ac..702cc2a25 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/30] [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 450d02b82..28b95adfc 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 fc4c4e62f..7dd570a63 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 6c9739be8..20be073ae 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 716b45f18..d8cadd13a 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 620a2dc3d..c502d038d 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/30] [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 28b95adfc..f50f454b3 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 fb7d008d1..e1b24ad07 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 c502d038d..0959456d1 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/30] [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 d8cadd13a..1399ebeb1 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/30] [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 | 187 ++++++++------------------- crates/chain/src/indexed_tx_graph.rs | 80 +++++++----- crates/chain/src/local_chain.rs | 29 +++-- crates/chain/src/sparse_chain.rs | 24 ++-- crates/chain/src/tx_graph.rs | 61 ++++++--- 6 files changed, 184 insertions(+), 203 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 85f9107c1..5615b0947 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 ccf3bc09a..7e975ad23 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)) - } -} - -// [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) - } + /// 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>; } -/// 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`]. - /// - /// `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) +impl CacheBackend { + /// Get the number of elements in the cache. + pub fn cache_size(&self) -> usize { + self.cache.len() } - /// Update the `tip_height` with the [`ChainOracle`]'s tip. + /// Prunes the cache to reach the `max_size` target. /// - /// `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(()) + /// 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() } - /// 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)); + 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; } - 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") + self.cache.contains(&(static_block.hash, block.hash)) } - /// 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") - } + pub fn insert(&mut self, static_block: BlockId, block: BlockId) -> bool { + let cache_key = (static_block.hash, block.hash); - /// 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") + 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 f50f454b3..dac05e725 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 spent_by = match self.graph.try_get_spend_in_chain(&chain, op) { + 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)), @@ -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 e1b24ad07..20b54a2f2 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 b615f4aac..acc616015 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 0959456d1..e3afce0e9 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/30] [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 | 85 +++++++++++++-------------------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 20b54a2f2..58cf923b4 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,24 +96,9 @@ 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)); - } - } - - 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); + return Err(UpdateNotConnectedError(first_invalid_height)); } } - if !missing_heights.is_empty() { - return Err(UpdateError::MissingHeightsInUpdate(missing_heights)); - } let mut changeset = BTreeMap::::new(); for (height, new_hash) in update { @@ -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 Append for ChangeSet { + fn append(&mut self, mut other: Self) { + BTreeMap::append(&mut self.0, &mut other.0) + } } -impl core::fmt::Display for UpdateError { +/// 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 { - 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 - ), - } + 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/30] [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 5615b0947..aa1e74d48 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 dac05e725..2de8114a8 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/30] [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 2de8114a8..574afc1b8 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 0ff20d407ebb8814f149152e6b555acde8bb86bb Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Mon, 17 Apr 2023 21:43:58 +0530 Subject: [PATCH 30/30] trial --- crates/bdk/src/wallet/mod.rs | 14 +- crates/bdk/src/wallet/tx_builder.rs | 2 +- crates/chain/src/indexed_tx_graph.rs | 8 +- crates/chain/src/keychain.rs | 8 +- crates/chain/src/keychain/persist.rs | 6 +- crates/chain/src/lib.rs | 1 + crates/chain/src/persist.rs | 119 ++++++ crates/chain/src/tx_data_traits.rs | 8 + crates/file_store/src/Indexed_file_store.rs | 403 ++++++++++++++++++ .../{file_store.rs => keychain_file_store.rs} | 0 crates/file_store/src/lib.rs | 41 +- 11 files changed, 592 insertions(+), 18 deletions(-) create mode 100644 crates/chain/src/persist.rs create mode 100644 crates/file_store/src/Indexed_file_store.rs rename crates/file_store/src/{file_store.rs => keychain_file_store.rs} (100%) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 67032cd3c..e721b3d91 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -195,7 +195,7 @@ impl Wallet { network: Network, ) -> Result> where - D: persist::PersistBackend, + D: persist::PersistBackendOld, { let secp = Secp256k1::new(); @@ -257,7 +257,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::PersistBackendOld, { self._get_address(address_index, KeychainKind::External) } @@ -271,14 +271,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::PersistBackendOld, { self._get_address(address_index, KeychainKind::Internal) } fn _get_address(&mut self, address_index: AddressIndex, keychain: KeychainKind) -> AddressInfo where - D: persist::PersistBackend, + D: persist::PersistBackendOld, { let keychain = self.map_keychain(keychain); let txout_index = &mut self.keychain_tracker.txout_index; @@ -613,7 +613,7 @@ impl Wallet { params: TxParams, ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> where - D: persist::PersistBackend, + D: persist::PersistBackendOld, { let external_descriptor = self .keychain_tracker @@ -1687,7 +1687,7 @@ impl Wallet { /// [`commit`]: Self::commit pub fn apply_update(&mut self, update: Update) -> Result<(), UpdateError> where - D: persist::PersistBackend, + D: persist::PersistBackendOld, { let changeset = self.keychain_tracker.apply_update(update)?; self.persist.stage(changeset); @@ -1699,7 +1699,7 @@ impl Wallet { /// [`staged`]: Self::staged pub fn commit(&mut self) -> Result<(), D::WriteError> where - D: persist::PersistBackend, + D: persist::PersistBackendOld, { self.persist.commit() } diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index dbd4811c1..c13ac718f 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -526,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::PersistBackendOld, { self.wallet .borrow_mut() diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 574afc1b8..0bf5be8af 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, BlockId, ChainOracle, FullTxOut, ObservedAs, TxIndex, + Append, BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex, Empty, BlockId, }; /// An outwards-facing view of a transaction that is part of the *best chain*'s history. @@ -54,6 +54,12 @@ impl Append for IndexedAdditions { } } +impl Empty for IndexedAdditions { + fn is_empty(&self) -> bool { + self.graph_additions.is_empty() && self.is_empty() + } +} + pub struct IndexedTxGraph { /// Transaction index. pub index: I, diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 81503049b..9f7b548cf 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -20,7 +20,7 @@ use crate::{ collections::BTreeMap, sparse_chain::ChainPosition, tx_graph::TxGraph, - Append, ForEachTxOut, + Append, ForEachTxOut, Empty, }; #[cfg(feature = "miniscript")] @@ -86,6 +86,12 @@ impl Append for DerivationAdditions { } } +impl Empty for DerivationAdditions { + fn is_empty(&self) -> bool { + self.is_empty() + } +} + impl Default for DerivationAdditions { fn default() -> Self { Self(Default::default()) diff --git a/crates/chain/src/keychain/persist.rs b/crates/chain/src/keychain/persist.rs index 1a3ffab02..0cc99b6ee 100644 --- a/crates/chain/src/keychain/persist.rs +++ b/crates/chain/src/keychain/persist.rs @@ -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: PersistBackendOld, { 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 PersistBackendOld { /// The error the backend returns when it fails to write. type WriteError: core::fmt::Debug; @@ -89,7 +89,7 @@ pub trait PersistBackend { ) -> Result<(), Self::LoadError>; } -impl PersistBackend for () { +impl PersistBackendOld for () { type WriteError = (); type LoadError = (); diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 265276234..9d813b392 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -33,6 +33,7 @@ pub mod tx_graph; pub use tx_data_traits::*; mod chain_oracle; pub use chain_oracle::*; +pub mod persist; #[doc(hidden)] pub mod example_utils; diff --git a/crates/chain/src/persist.rs b/crates/chain/src/persist.rs new file mode 100644 index 000000000..133dd2a46 --- /dev/null +++ b/crates/chain/src/persist.rs @@ -0,0 +1,119 @@ +//! Persistence for changes made to a [`KeychainTracker`]. +//! +//! BDK's [`KeychainTracker`] needs somewhere to persist changes it makes during operation. +//! Operations like giving out a new address are crucial to persist so that next time the +//! application is loaded, it can find transactions related to that address. +//! +//! Note that the [`KeychainTracker`] does not read this persisted data during operation since it +//! always has a copy in memory. +//! +//! [`KeychainTracker`]: crate::keychain::KeychainTracker + +use crate::Append; + +/// `Persist` wraps a [`PersistBackend`] to create a convenient staging area for changes before they +/// are persisted. Not all changes made to the [`KeychainTracker`] need to be written to disk right +/// away so you can use [`Persist::stage`] to *stage* it first and then [`Persist::commit`] to +/// finally, write it to disk. +/// +/// [`KeychainTracker`]: keychain::KeychainTracker +#[derive(Debug)] +pub struct Persist { + backend: B, + stage: A, + phanton_data: core::marker::PhantomData +} + +impl Persist { + /// Create a new `Persist` from a [`PersistBackend`]. + pub fn new(backend: B) -> Self + where + A: Default + { + Self { + backend, + stage: Default::default(), + phanton_data: Default::default() + } + } + + /// Stage a `changeset` to later persistence with [`commit`]. + /// + /// [`commit`]: Self::commit + pub fn stage(&mut self, addition: A) + where + A: Append + { + self.stage.append(addition) + } + + /// Get the changes that haven't been committed yet + pub fn staged(&self) -> &A { + &self.stage + } + + /// Commit the staged changes to the underlying persistence backend. + /// + /// Returns a backend-defined error if this fails. + pub fn commit(&mut self) -> Result<(), B::WriteError> + where + B: PersistBackend, + A: Default + Append + { + self.backend.append(&self.stage)?; + self.stage = Default::default(); + Ok(()) + } +} + +/// A persistence backend for [`Persist`]. +pub trait PersistBackend +where + A: Append + { + /// The error the backend returns when it fails to write. + type WriteError: core::fmt::Debug; + + /// The error the backend returns when it fails to load. + type LoadError: core::fmt::Debug; + + /// Appends a new changeset to the persistent backend. + /// + /// It is up to the backend what it does with this. It could store every changeset in a list or + /// it inserts the actual changes into a more structured database. All it needs to guarantee is + /// that [`load_into_keychain_tracker`] restores a keychain tracker to what it should be if all + /// changesets had been applied sequentially. + /// + /// [`load_into_keychain_tracker`]: Self::load_into_keychain_tracker + fn append( + &mut self, + additions: &A, + ) -> Result<(), Self::WriteError>; + + /// Applies all the changesets the backend has received to `tracker`. + fn load( + &mut self, + tracker: &mut T, + ) -> Result<(), Self::LoadError>; +} + +impl PersistBackend for () +where + A: Append +{ + type WriteError = (); + type LoadError = (); + + fn append( + &mut self, + _additions: &A, + ) -> Result<(), Self::WriteError> { + Ok(()) + } + fn load( + &mut self, + _tracker: &mut T, + ) -> Result<(), Self::LoadError> { + Ok(()) + } +} diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 1399ebeb1..70dc37794 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -69,6 +69,14 @@ impl Append for () { fn append(&mut self, _other: Self) {} } +pub trait Empty { + fn is_empty(&self) -> bool; +} + +impl Empty for () { + fn is_empty(&self) -> bool { true } +} + /// Represents an index of transaction data. pub trait TxIndex { /// The resultant "additions" when new transaction data is indexed. diff --git a/crates/file_store/src/Indexed_file_store.rs b/crates/file_store/src/Indexed_file_store.rs new file mode 100644 index 000000000..42cec388f --- /dev/null +++ b/crates/file_store/src/Indexed_file_store.rs @@ -0,0 +1,403 @@ +//! Module for persisting data on disk. +//! +//! The star of the show is [`KeychainStore`], which maintains an append-only file of +//! [`KeychainChangeSet`]s which can be used to restore a [`KeychainTracker`]. +use bdk_chain::{ + indexed_tx_graph::{IndexedAdditions, IndexedTxGraph}, BlockAnchor, Append, TxIndex, Empty, +}; +use bincode::{DefaultOptions, Options}; +use core::marker::PhantomData; +use std::{ + fs::{File, OpenOptions}, + io::{self, Read, Seek, Write}, + path::Path, +}; + +/// BDK File Store magic bytes length. +const MAGIC_BYTES_LEN: usize = 12; + +/// BDK File Store magic bytes. +const MAGIC_BYTES: [u8; MAGIC_BYTES_LEN] = [98, 100, 107, 102, 115, 48, 48, 48, 48, 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 IndexedTxGraphStore { + db_file: File, + changeset_type_params: core::marker::PhantomData<(BA, A, T)>, +} + +fn bincode() -> impl bincode::Options { + DefaultOptions::new().with_varint_encoding() +} + +impl IndexedTxGraphStore +where + BA: BlockAnchor, + A: Append + Default + Empty + serde::Serialize + serde::de::DeserializeOwned, + T: TxIndex + TxIndex, +{ + /// Creates a new store from a [`File`]. + /// + /// The file must have been opened with read and write permissions. + /// + /// [`File`]: std::fs::File + pub fn new(mut file: File) -> Result { + file.rewind()?; + + let mut magic_bytes = [0_u8; MAGIC_BYTES_LEN]; + file.read_exact(&mut magic_bytes)?; + + if magic_bytes != MAGIC_BYTES { + return Err(FileError::InvalidMagicBytes(magic_bytes)); + } + + Ok(Self { + db_file: file, + changeset_type_params: Default::default(), + }) + } + + /// Creates or loads a store from `db_path`. If no file exists there, it will be created. + pub fn new_from_path>(db_path: D) -> Result { + let already_exists = db_path.as_ref().exists(); + + let mut db_file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(db_path)?; + + if !already_exists { + db_file.write_all(&MAGIC_BYTES)?; + } + + Self::new(db_file) + } + + /// Iterates over the stored [`IndexedAdditions`] from first to last, changing the seek position at each + /// iteration. + /// + /// The iterator may fail to read an entry and therefore return an error. However, the first time + /// it returns an error will be the last. After doing so, the iterator will always yield `None`. + /// + /// **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_additions(&mut self) -> Result>, io::Error> { + self.db_file + .seek(io::SeekFrom::Start(MAGIC_BYTES_LEN as _))?; + + Ok(EntryIter::new(&mut self.db_file)) + } + + /// Loads all the additions that have been stored as one giant changeset. + /// + /// This function returns a tuple of the aggregate addition and a result that indicates + /// whether an error occurred while reading or deserializing one of the entries. If so the + /// addition will consist of all of those it was able to read. + /// + /// You should usually check the error. In many applications, it may make sense to do a full + /// wallet scan with a stop-gap after getting an error, since it is likely that one of the + /// changesets it was unable to read changed the derivation indices of the tracker. + /// + /// **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) -> (IndexedAdditions, Result<(), IterError>) { + let mut changeset = IndexedAdditions::default(); + let result = (|| { + let iter_changeset = self.iter_additions()?; + for next_changeset in iter_changeset { + changeset.append(next_changeset?); + } + Ok(()) + })(); + + (changeset, result) + } + + /// Reads and applies all the changesets stored sequentially to the tracker, stopping when it fails + /// to read the next one. + /// + /// **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 load_into_indexed_tx_graph( + &mut self, + indexed_tx_graph: &mut IndexedTxGraph, + ) -> Result<(), IterError> { + for addition in self.iter_additions()? { + indexed_tx_graph.apply_additions(addition?) + } + Ok(()) + } + + /// Append a new changeset to the file and truncate the file to the end of the appended changeset. + /// + /// The truncation is to avoid the possibility of having a valid but inconsistent changeset + /// directly after the appended changeset. + pub fn append_addition( + &mut self, + addition: &A, + ) -> Result<(), io::Error> { + if addition.is_empty() { + return Ok(()); + } + + bincode() + .serialize_into(&mut self.db_file, addition) + .map_err(|e| match *e { + bincode::ErrorKind::Io(inner) => inner, + unexpected_err => panic!("unexpected bincode error: {}", unexpected_err), + })?; + + // truncate file after this changeset addition + // if this is not done, data after this changeset may represent valid changesets, however + // applying those changesets on top of this one may result in an inconsistent state + let pos = self.db_file.stream_position()?; + self.db_file.set_len(pos)?; + + // We want to make sure that derivation indices changes are written to disk as soon as + // possible, so you know about the write failure before you give out the address in the application. + if !addition.index_additions.is_empty() { + self.db_file.sync_data()?; + } + + Ok(()) + } +} + +/// Error that occurs due to problems encountered with the file. +#[derive(Debug)] +pub enum FileError { + /// IO error, this may mean that the file is too short. + Io(io::Error), + /// Magic bytes do not match what is expected. + InvalidMagicBytes([u8; MAGIC_BYTES_LEN]), +} + +impl core::fmt::Display for FileError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Io(e) => write!(f, "io error trying to read file: {}", e), + Self::InvalidMagicBytes(b) => write!( + f, + "file has invalid magic bytes: expected={:?} got={:?}", + MAGIC_BYTES, b + ), + } + } +} + +impl From for FileError { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl std::error::Error for FileError {} + +/// Error type for [`EntryIter`]. +#[derive(Debug)] +pub enum IterError { + /// Failure to read from the file. + Io(io::Error), + /// Failure to decode data from the file. + Bincode(bincode::ErrorKind), +} + +impl core::fmt::Display for IterError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + IterError::Io(e) => write!(f, "io error trying to read entry {}", e), + IterError::Bincode(e) => write!(f, "bincode error while reading entry {}", e), + } + } +} + +impl std::error::Error for IterError {} + +/// Iterator over entries in a file store. +/// +/// Reads and returns an entry each time [`next`] is called. If an error occurs while reading the +/// iterator will yield a `Result::Err(_)` instead and then `None` for the next call to `next`. +/// +/// [`next`]: Self::next +pub struct EntryIter<'a, V> { + db_file: &'a mut File, + types: PhantomData, + error_exit: bool, +} + +impl<'a, V> EntryIter<'a, V> { + pub fn new(db_file: &'a mut File) -> Self { + Self { + db_file, + types: PhantomData, + error_exit: false, + } + } +} + +impl<'a, V> Iterator for EntryIter<'a, V> +where + V: serde::de::DeserializeOwned, +{ + type Item = Result; + + fn next(&mut self) -> Option { + let result = (|| { + let pos = self.db_file.stream_position()?; + + match bincode().deserialize_from(&mut self.db_file) { + Ok(changeset) => Ok(Some(changeset)), + Err(e) => { + if let bincode::ErrorKind::Io(inner) = &*e { + if inner.kind() == io::ErrorKind::UnexpectedEof { + let eof = self.db_file.seek(io::SeekFrom::End(0))?; + if pos == eof { + return Ok(None); + } + } + } + + self.db_file.seek(io::SeekFrom::Start(pos))?; + Err(IterError::Bincode(*e)) + } + } + })(); + + let result = result.transpose(); + + if let Some(Err(_)) = &result { + self.error_exit = true; + } + + result + } +} + +impl From for IterError { + fn from(value: io::Error) -> Self { + IterError::Io(value) + } +} + +#[cfg(test)] +mod test { + use super::*; + use bdk_chain::{ + keychain::{DerivationAdditions, KeychainTxOutIndex}, + BlockId, + }; + use std::{ + io::{Read, Write}, + vec::Vec, + }; + use tempfile::NamedTempFile; + #[derive( + Debug, + Clone, + Copy, + PartialOrd, + Ord, + PartialEq, + Eq, + Hash, + serde::Serialize, + serde::Deserialize, + )] + enum TestKeychain { + External, + Internal, + } + + impl core::fmt::Display for TestKeychain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::External => write!(f, "external"), + Self::Internal => write!(f, "internal"), + } + } + } + + #[test] + fn magic_bytes() { + assert_eq!(&MAGIC_BYTES, "bdkfs0000000".as_bytes()); + } + + #[test] + fn new_fails_if_file_is_too_short() { + let mut file = NamedTempFile::new().unwrap(); + file.write_all(&MAGIC_BYTES[..MAGIC_BYTES_LEN - 1]) + .expect("should write"); + + match IndexedTxGraphStore::, KeychainTxOutIndex>::new(file.reopen().unwrap()) { + Err(FileError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof), + unexpected => panic!("unexpected result: {:?}", unexpected), + }; + } + + #[test] + fn new_fails_if_magic_bytes_are_invalid() { + let invalid_magic_bytes = "ldkfs0000000"; + + let mut file = NamedTempFile::new().unwrap(); + file.write_all(invalid_magic_bytes.as_bytes()) + .expect("should write"); + + match IndexedTxGraphStore::, KeychainTxOutIndex>::new(file.reopen().unwrap()) { + Err(FileError::InvalidMagicBytes(b)) => { + assert_eq!(b, invalid_magic_bytes.as_bytes()) + } + unexpected => panic!("unexpected result: {:?}", unexpected), + }; + } + + #[test] + fn append_changeset_truncates_invalid_bytes() { + // initial data to write to file (magic bytes + invalid data) + let mut data = [255_u8; 2000]; + data[..MAGIC_BYTES_LEN].copy_from_slice(&MAGIC_BYTES); + + let additions = IndexedAdditions { + index_additions: DerivationAdditions( + vec![(TestKeychain::External, 42)].into_iter().collect(), + ), + ..Default::default() + }; + + let mut file = NamedTempFile::new().unwrap(); + file.write_all(&data).expect("should write"); + + let mut store = IndexedTxGraphStore::, KeychainTxOutIndex>::new(file.reopen().unwrap()) + .expect("should open"); + match store.iter_additions().expect("seek should succeed").next() { + Some(Err(IterError::Bincode(_))) => {} + unexpected_res => panic!("unexpected result: {:?}", unexpected_res), + } + + store.append_addition(&additions).expect("should append"); + + drop(store); + + let got_bytes = { + let mut buf = Vec::new(); + file.reopen() + .unwrap() + .read_to_end(&mut buf) + .expect("should read"); + buf + }; + + let expected_bytes = { + let mut buf = MAGIC_BYTES.to_vec(); + DefaultOptions::new() + .with_varint_encoding() + .serialize_into(&mut buf, &additions) + .expect("should encode"); + buf + }; + + assert_eq!(got_bytes, expected_bytes); + } +} diff --git a/crates/file_store/src/file_store.rs b/crates/file_store/src/keychain_file_store.rs similarity index 100% rename from crates/file_store/src/file_store.rs rename to crates/file_store/src/keychain_file_store.rs diff --git a/crates/file_store/src/lib.rs b/crates/file_store/src/lib.rs index e33474194..5ab316168 100644 --- a/crates/file_store/src/lib.rs +++ b/crates/file_store/src/lib.rs @@ -1,12 +1,16 @@ #![doc = include_str!("../README.md")] -mod file_store; +mod keychain_file_store; +mod Indexed_file_store; +use Indexed_file_store::IndexedTxGraphStore; use bdk_chain::{ - keychain::{KeychainChangeSet, KeychainTracker, PersistBackend}, - sparse_chain::ChainPosition, + keychain::{KeychainChangeSet, KeychainTracker, PersistBackendOld}, + sparse_chain::ChainPosition, BlockAnchor, Append, Empty, TxIndex, indexed_tx_graph::{IndexedAdditions, IndexedTxGraph}, }; -pub use file_store::*; -impl PersistBackend for KeychainStore +use bdk_chain::persist::PersistBackend; +use keychain_file_store::{KeychainStore, IterError}; + +impl PersistBackendOld for KeychainStore where K: Ord + Clone + core::fmt::Debug, P: ChainPosition, @@ -30,3 +34,30 @@ where KeychainStore::load_into_keychain_tracker(self, tracker) } } + +impl PersistBackend for IndexedTxGraphStore +where + BA: BlockAnchor, + A: Append + Default + Empty, + T: TxIndex + TxIndex, + IndexedAdditions: serde::Serialize + serde::de::DeserializeOwned, +{ + type WriteError = std::io::Error; + + type LoadError = Indexed_file_store::IterError; + + fn append( + &mut self, + changeset: &A, + ) -> Result<(), Self::WriteError> { + IndexedTxGraphStore::append_addition(self, changeset) + } + + fn load( + &mut self, + tracker: &mut T, + ) -> Result<(), Self::LoadError> { + IndexedTxGraphStore::load_into_indexed_tx_graph(self, tracker) + } +} +