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/chain_data.rs b/crates/chain/src/chain_data.rs index 59444d7f9..aa1e74d48 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -2,9 +2,29 @@ use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; use crate::{ sparse_chain::{self, ChainPosition}, - COINBASE_MATURITY, + BlockAnchor, COINBASE_MATURITY, }; +/// 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 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. + Unconfirmed(u64), +} + +impl ObservedAs<&A> { + pub fn cloned(self) -> ObservedAs { + match self { + ObservedAs::Confirmed(a) => ObservedAs::Confirmed(a.clone()), + ObservedAs::Unconfirmed(last_seen) => ObservedAs::Unconfirmed(last_seen), + } + } +} + /// Represents the height at which a transaction is confirmed. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr( @@ -118,7 +138,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 +160,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 } @@ -162,21 +188,21 @@ impl From<(&u32, &BlockHash)> for BlockId { } /// A `TxOut` with as much data as we can retrieve about it -#[derive(Debug, Clone, PartialEq)] -pub struct FullTxOut { +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +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 @@ -215,4 +241,65 @@ impl FullTxOut { } } +impl FullTxOut> { + /// Whether the `txout` is considered mature. + /// + /// This is the alternative version of [`is_mature`] which depends on `chain_position` being a + /// [`ObservedAs`] where `A` implements [`BlockAnchor`]. + /// + /// [`is_mature`]: Self::is_mature + pub fn is_observed_as_confirmed_and_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`. + /// + /// 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`]. + /// + /// [`is_spendable_at`]: Self::is_spendable_at + pub fn is_observed_as_confirmed_and_spendable(&self, tip: u32) -> bool { + if !self.is_observed_as_confirmed_and_mature(tip) { + return false; + } + + match &self.chain_position { + ObservedAs::Confirmed(anchor) => { + if anchor.anchor_block().height > tip { + return false; + } + } + 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 + } +} + // TODO: make test diff --git a/crates/chain/src/chain_graph.rs b/crates/chain/src/chain_graph.rs index acf104e79..0e3e3439e 100644 --- a/crates/chain/src/chain_graph.rs +++ b/crates/chain/src/chain_graph.rs @@ -465,7 +465,7 @@ where #[must_use] pub struct ChangeSet

{ pub chain: sparse_chain::ChangeSet

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

ChangeSet

{ diff --git a/crates/chain/src/chain_oracle.rs b/crates/chain/src/chain_oracle.rs new file mode 100644 index 000000000..7e975ad23 --- /dev/null +++ b/crates/chain/src/chain_oracle.rs @@ -0,0 +1,77 @@ +use crate::collections::HashSet; +use core::marker::PhantomData; + +use alloc::{collections::VecDeque, vec::Vec}; +use bitcoin::BlockHash; + +use crate::BlockId; + +/// Represents a service that tracks the blockchain. +/// +/// 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; + + /// 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>; +} + +/// A cache structure increases the performance of getting chain data. +/// +/// A simple FIFO cache replacement policy is used. Something more efficient and advanced can be +/// implemented later. +#[derive(Debug, Default)] +pub struct CacheBackend { + cache: HashSet<(BlockHash, BlockHash)>, + fifo: VecDeque<(BlockHash, BlockHash)>, + marker: PhantomData, +} + +impl CacheBackend { + /// Get the number of elements in the cache. + pub fn cache_size(&self) -> usize { + self.cache.len() + } + + /// Prunes the cache to reach the `max_size` target. + /// + /// Returns pruned elements. + pub fn prune(&mut self, max_size: usize) -> Vec<(BlockHash, BlockHash)> { + let prune_count = self.cache.len().saturating_sub(max_size); + (0..prune_count) + .filter_map(|_| self.fifo.pop_front()) + .filter(|k| self.cache.remove(k)) + .collect() + } + + pub fn contains(&self, static_block: BlockId, block: BlockId) -> bool { + if static_block.height < block.height + || static_block.height == block.height && static_block.hash != block.hash + { + return false; + } + + self.cache.contains(&(static_block.hash, block.hash)) + } + + pub fn insert(&mut self, static_block: BlockId, block: BlockId) -> bool { + let cache_key = (static_block.hash, block.hash); + + if self.cache.insert(cache_key) { + self.fifo.push_back(cache_key); + true + } else { + false + } + } +} diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs new file mode 100644 index 000000000..0bf5be8af --- /dev/null +++ b/crates/chain/src/indexed_tx_graph.rs @@ -0,0 +1,375 @@ +use core::convert::Infallible; + +use bitcoin::{OutPoint, Script, Transaction, TxOut}; + +use crate::{ + keychain::Balance, + tx_graph::{Additions, TxGraph, TxNode}, + Append, BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex, Empty, BlockId, +}; + +/// An outwards-facing view of a transaction that is part of the *best chain*'s history. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct CanonicalTx<'a, T, A> { + /// Where the transaction is observed (in a block or in mempool). + pub observed_as: ObservedAs<&'a A>, + /// The transaction with anchors and last seen timestamp. + pub tx: TxNode<'a, T, A>, +} + +/// A structure that represents changes to an [`IndexedTxGraph`]. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + crate = "serde_crate", + bound( + deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize, IA: serde::Serialize" + ) + ) +)] +#[must_use] +pub struct IndexedAdditions { + /// [`TxGraph`] additions. + pub graph_additions: Additions, + /// [`TxIndex`] additions. + pub index_additions: IA, +} + +impl Default for IndexedAdditions { + fn default() -> Self { + Self { + graph_additions: Default::default(), + index_additions: Default::default(), + } + } +} + +impl Append for IndexedAdditions { + fn append(&mut self, other: Self) { + self.graph_additions.append(other.graph_additions); + self.index_additions.append(other.index_additions); + } +} + +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, + graph: TxGraph, +} + +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 + } + + /// Applies the [`IndexedAdditions`] to the [`IndexedTxGraph`]. + pub fn apply_additions(&mut self, additions: IndexedAdditions) { + let IndexedAdditions { + graph_additions, + index_additions, + } = additions; + + self.index.apply_additions(index_additions); + + 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`. + pub fn insert_txout( + &mut self, + outpoint: OutPoint, + txout: &TxOut, + observation: ObservedAs, + ) -> IndexedAdditions { + IndexedAdditions { + graph_additions: { + let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone()); + graph_additions.append(match observation { + ObservedAs::Confirmed(anchor) => { + self.graph.insert_anchor(outpoint.txid, anchor) + } + ObservedAs::Unconfirmed(seen_at) => { + self.graph.insert_seen_at(outpoint.txid, seen_at) + } + }); + graph_additions + }, + index_additions: ::index_txout(&mut self.index, outpoint, txout), + } + } + + pub fn insert_tx( + &mut self, + tx: &Transaction, + observation: ObservedAs, + ) -> IndexedAdditions { + let txid = tx.txid(); + + IndexedAdditions { + graph_additions: { + let mut graph_additions = self.graph.insert_tx(tx.clone()); + graph_additions.append(match observation { + ObservedAs::Confirmed(anchor) => self.graph.insert_anchor(txid, anchor), + ObservedAs::Unconfirmed(seen_at) => self.graph.insert_seen_at(txid, seen_at), + }); + graph_additions + }, + index_additions: ::index_tx(&mut self.index, tx), + } + } + + pub fn insert_relevant_txs<'t, T>( + &mut self, + txs: T, + observation: ObservedAs, + ) -> IndexedAdditions + where + T: Iterator, + I::Additions: Default + Append, + { + txs.filter_map(|tx| { + if self.index.is_tx_relevant(tx) { + Some(self.insert_tx(tx, observation.clone())) + } else { + None + } + }) + .fold(IndexedAdditions::default(), |mut acc, other| { + acc.append(other); + acc + }) + } + + // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`. + pub fn try_list_chain_txs<'a, C>( + &'a self, + chain: &'a C, + static_block: BlockId, + ) -> impl Iterator, C::Error>> + where + C: ChainOracle + 'a, + { + self.graph + .full_transactions() + .filter(|tx| self.index.is_tx_relevant(tx)) + .filter_map(move |tx| { + self.graph + .try_get_chain_position(chain, static_block, tx.txid) + .map(|v| { + v.map(|observed_in| CanonicalTx { + observed_as: observed_in, + tx, + }) + }) + .transpose() + }) + } + + pub fn list_chain_txs<'a, C>( + &'a self, + chain: &'a C, + static_block: BlockId, + ) -> impl Iterator> + where + C: ChainOracle + 'a, + { + 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: &'a C, + static_block: BlockId, + ) -> impl Iterator>, C::Error>> + 'a + where + C: ChainOracle + 'a, + { + self.graph + .all_txouts() + .filter(|&(op, txo)| self.index.is_txout_relevant(op, txo)) + .filter_map(move |(op, txout)| -> Option> { + let graph_tx = self.graph.get_tx(op.txid)?; + + let is_on_coinbase = graph_tx.is_coin_base(); + + let chain_position = + match self + .graph + .try_get_chain_position(chain, static_block, op.txid) + { + Ok(Some(observed_at)) => observed_at.cloned(), + Ok(None) => return None, + Err(err) => return Some(Err(err)), + }; + + let spent_by = match self.graph.try_get_spend_in_chain(chain, static_block, op) { + Ok(Some((obs, txid))) => Some((obs.cloned(), txid)), + Ok(None) => None, + Err(err) => return Some(Err(err)), + }; + + let full_txout = FullTxOut { + outpoint: op, + txout: txout.clone(), + chain_position, + spent_by, + is_on_coinbase, + }; + + Some(Ok(full_txout)) + }) + } + + pub fn list_chain_txouts<'a, C>( + &'a self, + chain: &'a C, + static_block: BlockId, + ) -> impl Iterator>> + 'a + where + C: ChainOracle + 'a, + { + 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: &'a C, + static_block: BlockId, + ) -> impl Iterator>, C::Error>> + 'a + where + C: ChainOracle + 'a, + { + self.try_list_chain_txouts(chain, static_block) + .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) + } + + pub fn list_chain_utxos<'a, C>( + &'a self, + chain: &'a C, + static_block: BlockId, + ) -> impl Iterator>> + 'a + where + C: ChainOracle + 'a, + { + self.try_list_chain_utxos(chain, static_block) + .map(|r| r.expect("error is infallible")) + } + + pub fn try_balance( + &self, + chain: &C, + static_block: BlockId, + tip: u32, + mut should_trust: F, + ) -> Result + where + C: ChainOracle, + F: FnMut(&Script) -> 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, static_block) { + let txout = res?; + + match &txout.chain_position { + ObservedAs::Confirmed(_) => { + if txout.is_on_coinbase { + if txout.is_observed_as_confirmed_and_mature(tip) { + confirmed += txout.txout.value; + } else { + immature += txout.txout.value; + } + } + } + ObservedAs::Unconfirmed(_) => { + if should_trust(&txout.txout.script_pubkey) { + 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, + static_block: BlockId, + tip: u32, + should_trust: F, + ) -> Balance + where + C: ChainOracle, + F: FnMut(&Script) -> bool, + { + self.try_balance(chain, static_block, tip, should_trust) + .expect("error is infallible") + } + + 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, static_block) { + let txo = txo_res?; + if txo.is_observed_as_confirmed_and_spendable(height) { + sum += txo.txout.value; + } + } + Ok(sum) + } + + pub fn balance_at(&self, chain: &C, static_block: BlockId, height: u32) -> u64 + where + C: ChainOracle, + { + self.try_balance_at(chain, static_block, height) + .expect("error is infallible") + } +} diff --git a/crates/chain/src/keychain.rs b/crates/chain/src/keychain.rs index 321769360..9f7b548cf 100644 --- a/crates/chain/src/keychain.rs +++ b/crates/chain/src/keychain.rs @@ -14,12 +14,13 @@ //! [`KeychainChangeSet`]s. //! //! [`SpkTxOutIndex`]: crate::SpkTxOutIndex + use crate::{ chain_graph::{self, ChainGraph}, collections::BTreeMap, sparse_chain::ChainPosition, tx_graph::TxGraph, - ForEachTxOut, + Append, ForEachTxOut, Empty, }; #[cfg(feature = "miniscript")] @@ -69,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); @@ -85,6 +86,12 @@ impl 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/keychain/txout_index.rs b/crates/chain/src/keychain/txout_index.rs index feb71edb4..7dd570a63 100644 --- a/crates/chain/src/keychain/txout_index.rs +++ b/crates/chain/src/keychain/txout_index.rs @@ -1,12 +1,14 @@ use crate::{ collections::*, miniscript::{Descriptor, DescriptorPublicKey}, - ForEachTxOut, SpkTxOutIndex, + ForEachTxOut, SpkTxOutIndex, TxIndex, }; 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. @@ -88,6 +90,30 @@ 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 apply_additions(&mut self, additions: Self::Additions) { + self.apply_additions(additions) + } + + fn is_txout_relevant(&self, _outpoint: OutPoint, txout: &TxOut) -> bool { + self.index_of_spk(&txout.script_pubkey).is_some() + } + + fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool { + self.is_relevant(tx) + } +} + impl KeychainTxOutIndex { /// Scans an object for relevant outpoints, which are stored and indexed internally. /// diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 4e49e34ed..9d813b392 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -24,11 +24,16 @@ 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 local_chain; pub mod sparse_chain; mod tx_data_traits; 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/local_chain.rs b/crates/chain/src/local_chain.rs new file mode 100644 index 000000000..58cf923b4 --- /dev/null +++ b/crates/chain/src/local_chain.rs @@ -0,0 +1,184 @@ +use core::{convert::Infallible, ops::Deref}; + +use alloc::collections::{BTreeMap, BTreeSet}; +use bitcoin::BlockHash; + +use crate::{Append, 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 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, + }, + ) + } +} + +impl AsRef> for LocalChain { + fn as_ref(&self) -> &BTreeMap { + &self.blocks + } +} + +impl From for BTreeMap { + fn from(value: LocalChain) -> Self { + value.blocks + } +} + +impl From> for LocalChain { + fn from(value: BTreeMap) -> Self { + Self { blocks: value } + } +} + +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: &Self) -> Result { + 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_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_height) = invalidate_from_height { + if !update.contains_key(&first_invalid_height) { + return Err(UpdateNotConnectedError(first_invalid_height)); + } + } + + 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)) + } + + /// 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 equivalent 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() + } +} + +/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`]. +/// +/// [`determine_changeset`]: LocalChain::determine_changeset +#[derive(Debug, Default, Clone, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(crate = "serde_crate") +)] +pub struct ChangeSet(pub(crate) BTreeMap); + +impl Deref for ChangeSet { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Append for ChangeSet { + fn append(&mut self, mut other: Self) { + BTreeMap::append(&mut self.0, &mut other.0) + } +} + +/// Represents an update failure of [`LocalChain`] due to the update not connecting to the original +/// chain. +/// +/// The update cannot be applied to the chain because the chain suffix it represents did not +/// connect to the existing chain. This error case contains the checkpoint height to include so +/// that the chains can connect. +#[derive(Clone, Debug, PartialEq)] +pub struct UpdateNotConnectedError(u32); + +impl core::fmt::Display for UpdateNotConnectedError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "the update cannot connect with the chain, try include block at height {}", + self.0 + ) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for UpdateNotConnectedError {} 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/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index b9c1e24ba..acc616015 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -307,11 +307,12 @@ //! ); //! ``` use core::{ + convert::Infallible, fmt::Debug, 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,7 +457,27 @@ impl core::fmt::Display for UpdateError

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

{} -impl SparseChain

{ +impl

ChainOracle for SparseChain

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

SparseChain

{ /// Creates a new chain from a list of block hashes and heights. The caller must guarantee they /// are in the same chain. pub fn from_checkpoints(checkpoints: C) -> Self @@ -487,13 +508,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 @@ -509,6 +523,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 @@ -899,7 +954,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)?; @@ -919,24 +974,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, @@ -952,27 +989,13 @@ impl SparseChain

{ .map(|(&txid, _)| txid) } - fn prune_checkpoints(&mut self) -> Option> { - let limit = self.checkpoint_limit?; - - // find the last height to be pruned - let last_height = *self.checkpoints.keys().rev().nth(limit)?; - // first height to be kept - let keep_height = last_height + 1; - - let mut split = self.checkpoints.split_off(&keep_height); - core::mem::swap(&mut self.checkpoints, &mut split); - - Some(split) - } - /// Finds the transaction in the chain that spends `outpoint`. /// /// [`TxGraph`] is used to provide the spend relationships. /// /// Note that the transaction including `outpoint` does not need to be in the `graph` or the /// `chain` for this to return `Some`. - pub fn spent_by(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<(&P, Txid)> { + pub fn spent_by(&self, graph: &TxGraph, outpoint: OutPoint) -> Option<(&P, Txid)> { graph .outspends(outpoint) .iter() diff --git a/crates/chain/src/spk_txout_index.rs b/crates/chain/src/spk_txout_index.rs index 7f46604fc..20be073ae 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,32 @@ impl Default for SpkTxOutIndex { } } +impl TxIndex for SpkTxOutIndex { + type Additions = (); + + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { + self.scan_txout(outpoint, txout); + Default::default() + } + + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions { + self.scan(tx); + Default::default() + } + + fn apply_additions(&mut self, _additions: Self::Additions) { + // This applies nothing. + } + + fn is_txout_relevant(&self, _outpoint: OutPoint, txout: &TxOut) -> bool { + self.index_of_spk(&txout.script_pubkey).is_some() + } + + fn is_tx_relevant(&self, tx: &Transaction) -> bool { + self.is_relevant(tx) + } +} + /// 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 432592b82..70dc37794 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,67 @@ impl ForEachTxOut for Transaction { } } } + +/// Trait that "anchors" blockchain data to a specific block of height and hash. +/// +/// 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 +{ + /// Returns the [`BlockId`] that the associated blockchain data is "anchored" in. + 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() + } +} + +/// 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) {} +} + +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. + type Additions; + + /// Scan and index the given `outpoint` and `txout`. + fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions; + + /// Scan and index the given transaction. + fn index_tx(&mut self, tx: &Transaction) -> Self::Additions; + + /// Apply additions to itself. + fn apply_additions(&mut self, additions: Self::Additions); + + /// Returns whether the txout is marked as relevant in the index. + fn is_txout_relevant(&self, outpoint: OutPoint, txout: &TxOut) -> bool; + + /// Returns whether the transaction is marked as relevant in the index. + fn is_tx_relevant(&self, tx: &Transaction) -> bool; +} diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 3326ac4a2..e3afce0e9 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 = 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 = TxGraph::default(); //! let update = TxGraph::new(vec![tx_a, tx_b]); //! //! // preview additions as the result of the update @@ -52,51 +54,102 @@ //! let additions = graph.apply_update(update); //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, ForEachTxOut}; + +use crate::{collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, ObservedAs}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; -use core::ops::RangeInclusive; +use core::{ + convert::Infallible, + 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 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. + 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 as unconfirmed. + pub last_seen_unconfirmed: u64, +} + +impl<'a, T, A> Deref for TxNode<'a, T, A> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.tx + } +} + +impl<'a, A> TxNode<'a, Transaction, A> { + pub fn from_tx(tx: &'a Transaction, anchors: &'a BTreeSet) -> Self { + Self { + txid: tx.txid(), + tx, + anchors, + last_seen_unconfirmed: 0, + } + } +} + +/// Internal representation of a transaction node of a [`TxGraph`]. +/// +/// This can either be a whole transaction, or a partial transaction (where we only have select +/// 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()) } } -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 { - TxNode::Whole(tx) => tx + self.txs.iter().flat_map(|(txid, (tx, _, _))| match 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::>(), @@ -104,11 +157,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 { + TxNodeInternal::Whole(tx) => Some(TxNode { + txid, + tx, + anchors, + last_seen_unconfirmed: *last_seen, + }), + TxNodeInternal::Partial(_) => None, + }) } /// Get a transaction by txid. This only returns `Some` for full transactions. @@ -117,30 +177,40 @@ impl TxGraph { /// /// [`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, + 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)? { + (TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode { + txid, + tx, + anchors, + last_seen_unconfirmed: *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)? { - TxNode::Whole(tx) => tx.output.get(outpoint.vout as usize), - TxNode::Partial(txouts) => txouts.get(&outpoint.vout), + match &self.txs.get(&outpoint.txid)?.0 { + 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)? { - TxNode::Whole(tx) => tx + Some(match &self.txs.get(&txid)?.0 { + 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::>(), @@ -176,9 +246,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 { + TxNodeInternal::Whole(_) => None, + TxNodeInternal::Partial(partial) => Some(TxNode { + txid, + tx: partial, + anchors, + last_seen_unconfirmed: *last_seen, + }), + }) + } + + /// Creates an iterator that filters and maps descendants from the starting `txid`. + /// + /// The supplied closure takes in two inputs `(depth, descendant_txid)`: + /// + /// * `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(); @@ -187,38 +359,113 @@ 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, + ( + TxNodeInternal::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 /// 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 } + /// 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(), (TxNodeInternal::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. - 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 } + /// 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. + /// 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 + } + + /// 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 + /// 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 +479,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 @ TxNodeInternal::Partial(_), _, _)) => { + *tx_node = TxNodeInternal::Whole(tx); + } + Some((TxNodeInternal::Whole(tx), _, _)) => { + debug_assert_eq!( + tx.txid(), + txid, + "tx should produce txid that is same as key" + ); + } + None => { + self.txs + .insert(txid, (TxNodeInternal::Whole(tx), BTreeSet::new(), 0)); + } } } @@ -245,163 +501,217 @@ 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) => { + (TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */ + } + (TxNodeInternal::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, TxNodeInternal::Whole(update_tx)) => { + additions.tx.insert(update_tx.clone()); + 0 } - TxNode::Partial(partial) => { - for (&vout, update_txout) in partial { - let outpoint = OutPoint::new(txid, vout); - - if self.get_txout(outpoint) != Some(update_txout) { - additions.txout.insert(outpoint, update_txout.clone()); - } - } + (None, TxNodeInternal::Partial(update_txos)) => { + additions.txout.extend( + update_txos + .iter() + .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), + ); + 0 + } + (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((TxNodeInternal::Partial(txos), _, last_seen)), + TxNodeInternal::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 - } - - /// 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)); - self.determine_additions(&update) - } + additions.anchors = update.anchors.difference(&self.anchors).cloned().collect(); - /// 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()), - ); - self.determine_additions(&update) + additions } } -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) +impl TxGraph { + /// Get all heights that are relevant to the graph. + pub fn relevant_heights(&self) -> impl Iterator + '_ { + let mut visited = HashSet::new(); + self.anchors + .iter() + .map(|(a, _)| a.anchor_block().height) + .filter(move |&h| visited.insert(h)) } - /// Iterates over the transactions spending from `txid`. - /// - /// The iterator item is a union of `(vout, txid-set)` where: + /// Determines whether a transaction of `txid` is in the best chain. /// - /// - `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( + /// TODO: Also return conflicting tx list, ordered by last_seen. + pub fn try_get_chain_position( &self, + chain: &C, + static_block: BlockId, txid: Txid, - ) -> impl DoubleEndedIterator)> + '_ { - let start = OutPoint { txid, vout: 0 }; - let end = OutPoint { - txid, - vout: u32::MAX, + ) -> Result>, C::Error> + 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(None), }; - 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)> { - self.txs.iter().filter_map(|(txid, tx)| match tx { - TxNode::Whole(_) => None, - TxNode::Partial(partial) => Some((*txid, partial)), - }) + for anchor in anchors { + 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, + } + } + } + } + } + + // 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 { + TxNodeInternal::Whole(tx) => tx, + TxNodeInternal::Partial(_) => { + // Partial transactions (outputs only) cannot have conflicts. + return Ok(None); + } + }; + + // 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 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); + } + } + if conflicting_tx.last_seen_unconfirmed > last_seen { + return Ok(None); + } + } + + Ok(Some(ObservedAs::Unconfirmed(last_seen))) } - /// Creates an iterator that filters and maps descendants from the starting `txid`. - /// - /// The supplied closure takes in two inputs `(depth, descendant_txid)`: - /// - /// * `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 + pub fn get_chain_position( + &self, + chain: &C, + static_block: BlockId, + txid: Txid, + ) -> Option> where - F: FnMut(usize, Txid) -> Option + 'g, + C: ChainOracle, { - TxDescendants::new_exclude_root(self, txid, walk_map) + self.try_get_chain_position(chain, static_block, txid) + .expect("error is infallible") } - /// 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 + pub fn try_get_spend_in_chain( + &self, + chain: &C, + static_block: BlockId, + outpoint: OutPoint, + ) -> Result, Txid)>, C::Error> where - F: FnMut(usize, Txid) -> Option + 'g, + C: ChainOracle, { - 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) + if self + .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, static_block, txid)? { + return Ok(Some((observed_at, txid))); + } + } + } + Ok(None) } - /// Whether the graph has any transactions or outputs in it. - pub fn is_empty(&self) -> bool { - self.txs.is_empty() + 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, static_block, outpoint) + .expect("error is infallible") } } @@ -413,19 +723,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 +775,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 +804,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 +824,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 +837,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 +856,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 +877,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 +889,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..b5cbf5b9b 100644 --- a/crates/chain/tests/test_chain_graph.rs +++ b/crates/chain/tests/test_chain_graph.rs @@ -136,6 +136,7 @@ fn update_evicts_conflicting_tx() { graph: tx_graph::Additions { tx: [tx_b2.clone()].into(), txout: [].into(), + ..Default::default() }, }; assert_eq!( @@ -215,6 +216,7 @@ fn update_evicts_conflicting_tx() { graph: tx_graph::Additions { tx: [tx_b2].into(), txout: [].into(), + ..Default::default() }, }; assert_eq!( @@ -363,7 +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, &tx)) + Some((&TxHeight::Unconfirmed, &tx,)) ); } @@ -395,9 +397,9 @@ 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), &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 3bf0a1d50..bd8c6e031 100644 --- a/crates/chain/tests/test_keychain_tracker.rs +++ b/crates/chain/tests/test_keychain_tracker.rs @@ -1,6 +1,7 @@ #![cfg(feature = "miniscript")] #[macro_use] mod common; + use bdk_chain::{ keychain::{Balance, KeychainTracker}, miniscript::{ @@ -40,7 +41,7 @@ fn test_insert_tx() { .chain_graph() .transactions_in_chain() .collect::>(), - vec![(&ConfirmationTime::Unconfirmed, &tx)] + vec![(&ConfirmationTime::Unconfirmed, &tx,)] ); assert_eq!( @@ -66,7 +67,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..279ddb74b 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -35,7 +35,7 @@ fn insert_txouts() { )]; let mut graph = { - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<()>::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -69,6 +69,7 @@ fn insert_txouts() { Additions { tx: [].into(), txout: update_ops.into(), + ..Default::default() } ); @@ -90,7 +91,7 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() { output: vec![], }; - let mut graph = TxGraph::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()); @@ -120,8 +121,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::<()>::default(); + let mut graph2 = TxGraph::<()>::default(); // insert in different order let _ = graph1.insert_tx(tx1.clone()); @@ -149,14 +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(&tx)); } #[test] fn insert_tx_displaces_txouts() { - let mut tx_graph = TxGraph::default(); + let mut tx_graph = TxGraph::<()>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -212,7 +213,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::<()>::default(); let tx = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -268,7 +269,7 @@ fn insert_txout_does_not_displace_tx() { #[test] fn test_calculate_fee() { - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<()>::default(); let intx1 = Transaction { version: 0x01, lock_time: PackedLockTime(0), @@ -362,7 +363,7 @@ fn test_calculate_fee_on_coinbase() { output: vec![TxOut::default()], }; - let graph = TxGraph::default(); + let graph = TxGraph::<()>::default(); assert_eq!(graph.calculate_fee(&tx), Some(0)); } @@ -404,7 +405,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::<()>::default(); let _ = graph.insert_tx(tx_a); let _ = graph.insert_tx(tx_b); @@ -480,7 +481,7 @@ fn test_descendants_no_repeat() { }) .collect::>(); - let mut graph = TxGraph::default(); + let mut graph = TxGraph::<()>::default(); let mut expected_txids = BTreeSet::new(); // these are NOT descendants of `tx_a` 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) + } +} + 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;