diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 85f9107c..5615b094 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -248,7 +248,7 @@ impl FullTxOut> { /// [`ObservedAs`] where `A` implements [`BlockAnchor`]. /// /// [`is_mature`]: Self::is_mature - pub fn is_observed_as_mature(&self, tip: u32) -> bool { + pub fn is_observed_as_confirmed_and_mature(&self, tip: u32) -> bool { if !self.is_on_coinbase { return false; } @@ -275,8 +275,8 @@ impl FullTxOut> { /// being a [`ObservedAs`] where `A` implements [`BlockAnchor`]. /// /// [`is_spendable_at`]: Self::is_spendable_at - pub fn is_observed_as_spendable(&self, tip: u32) -> bool { - if !self.is_observed_as_mature(tip) { + pub fn is_observed_as_confirmed_and_spendable(&self, tip: u32) -> bool { + if !self.is_observed_as_confirmed_and_mature(tip) { return false; } diff --git a/crates/chain/src/chain_oracle.rs b/crates/chain/src/chain_oracle.rs index ccf3bc09..7e975ad2 100644 --- a/crates/chain/src/chain_oracle.rs +++ b/crates/chain/src/chain_oracle.rs @@ -1,162 +1,77 @@ -use core::{convert::Infallible, marker::PhantomData}; +use crate::collections::HashSet; +use core::marker::PhantomData; -use alloc::collections::BTreeMap; +use alloc::{collections::VecDeque, vec::Vec}; use bitcoin::BlockHash; use crate::BlockId; -/// Represents a service that tracks the best chain history. -/// TODO: How do we ensure the chain oracle is consistent across a single call? -/// * We need to somehow lock the data! What if the ChainOracle is remote? -/// * Get tip method! And check the tip still exists at the end! And every internal call -/// does not go beyond the initial tip. +/// Represents a service that tracks the blockchain. +/// +/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`] +/// is an ancestor of another "static block". +/// +/// [`is_block_in_chain`]: Self::is_block_in_chain pub trait ChainOracle { /// Error type. type Error: core::fmt::Debug; - /// Get the height and hash of the tip in the best chain. - fn get_tip_in_best_chain(&self) -> Result, Self::Error>; - - /// Returns the block hash (if any) of the given `height`. - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error>; - - /// Determines whether the block of [`BlockId`] exists in the best chain. - fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { - Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash)) - } + /// Determines whether `block` of [`BlockId`] exists as an ancestor of `static_block`. + /// + /// If `None` is returned, it means the implementation cannot determine whether `block` exists. + fn is_block_in_chain( + &self, + block: BlockId, + static_block: BlockId, + ) -> Result, Self::Error>; } -// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this? -// Box, Arc ????? I will figure it out -impl ChainOracle for &C { - type Error = C::Error; - - fn get_tip_in_best_chain(&self) -> Result, Self::Error> { - ::get_tip_in_best_chain(self) - } - - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { - ::get_block_in_best_chain(self, height) - } - - fn is_block_in_best_chain(&self, block_id: BlockId) -> Result { - ::is_block_in_best_chain(self, block_id) - } -} - -/// This structure increases the performance of getting chain data. -#[derive(Debug)] -pub struct Cache { - assume_final_depth: u32, - tip_height: u32, - cache: BTreeMap, +/// A cache structure increases the performance of getting chain data. +/// +/// A simple FIFO cache replacement policy is used. Something more efficient and advanced can be +/// implemented later. +#[derive(Debug, Default)] +pub struct CacheBackend { + cache: HashSet<(BlockHash, BlockHash)>, + fifo: VecDeque<(BlockHash, BlockHash)>, marker: PhantomData, } -impl Cache { - /// Creates a new [`Cache`]. +impl CacheBackend { + /// Get the number of elements in the cache. + pub fn cache_size(&self) -> usize { + self.cache.len() + } + + /// Prunes the cache to reach the `max_size` target. /// - /// `assume_final_depth` represents the minimum number of blocks above the block in question - /// when we can assume the block is final (reorgs cannot happen). I.e. a value of 0 means the - /// tip is assumed to be final. The cache only caches blocks that are assumed to be final. - pub fn new(assume_final_depth: u32) -> Self { - Self { - assume_final_depth, - tip_height: 0, - cache: Default::default(), - marker: Default::default(), + /// Returns pruned elements. + pub fn prune(&mut self, max_size: usize) -> Vec<(BlockHash, BlockHash)> { + let prune_count = self.cache.len().saturating_sub(max_size); + (0..prune_count) + .filter_map(|_| self.fifo.pop_front()) + .filter(|k| self.cache.remove(k)) + .collect() + } + + pub fn contains(&self, static_block: BlockId, block: BlockId) -> bool { + if static_block.height < block.height + || static_block.height == block.height && static_block.hash != block.hash + { + return false; + } + + self.cache.contains(&(static_block.hash, block.hash)) + } + + pub fn insert(&mut self, static_block: BlockId, block: BlockId) -> bool { + let cache_key = (static_block.hash, block.hash); + + if self.cache.insert(cache_key) { + self.fifo.push_back(cache_key); + true + } else { + false } } } - -impl Cache { - /// This is the topmost (highest) block height that we assume as final (no reorgs possible). - /// - /// Blocks higher than this height are not cached. - pub fn assume_final_height(&self) -> u32 { - self.tip_height.saturating_sub(self.assume_final_depth) - } - - /// Update the `tip_height` with the [`ChainOracle`]'s tip. - /// - /// `tip_height` is used with `assume_final_depth` to determine whether we should cache a - /// certain block height (`tip_height` - `assume_final_depth`). - pub fn try_update_tip_height(&mut self, chain: C) -> Result<(), C::Error> { - let tip = chain.get_tip_in_best_chain()?; - if let Some(BlockId { height, .. }) = tip { - self.tip_height = height; - } - Ok(()) - } - - /// Get a block from the cache with the [`ChainOracle`] as fallback. - /// - /// If the block does not exist in cache, the logic fallbacks to fetching from the internal - /// [`ChainOracle`]. If the block is at or below the "assume final height", we will also store - /// the missing block in the cache. - pub fn try_get_block(&mut self, chain: C, height: u32) -> Result, C::Error> { - if let Some(&hash) = self.cache.get(&height) { - return Ok(Some(hash)); - } - - let hash = chain.get_block_in_best_chain(height)?; - - if hash.is_some() && height > self.tip_height { - self.tip_height = height; - } - - // only cache block if at least as deep as `assume_final_depth` - let assume_final_height = self.tip_height.saturating_sub(self.assume_final_depth); - if height <= assume_final_height { - if let Some(hash) = hash { - self.cache.insert(height, hash); - } - } - - Ok(hash) - } - - /// Determines whether the block of `block_id` is in the chain using the cache. - /// - /// This uses [`try_get_block`] internally. - /// - /// [`try_get_block`]: Self::try_get_block - pub fn try_is_block_in_chain(&mut self, chain: C, block_id: BlockId) -> Result { - match self.try_get_block(chain, block_id.height)? { - Some(hash) if hash == block_id.hash => Ok(true), - _ => Ok(false), - } - } -} - -impl> Cache { - /// Updates the `tip_height` with the [`ChainOracle`]'s tip. - /// - /// This is the no-error version of [`try_update_tip_height`]. - /// - /// [`try_update_tip_height`]: Self::try_update_tip_height - pub fn update_tip_height(&mut self, chain: C) { - self.try_update_tip_height(chain) - .expect("chain oracle error is infallible") - } - - /// Get a block from the cache with the [`ChainOracle`] as fallback. - /// - /// This is the no-error version of [`try_get_block`]. - /// - /// [`try_get_block`]: Self::try_get_block - pub fn get_block(&mut self, chain: C, height: u32) -> Option { - self.try_get_block(chain, height) - .expect("chain oracle error is infallible") - } - - /// Determines whether the block at `block_id` is in the chain using the cache. - /// - /// This is the no-error version of [`try_is_block_in_chain`]. - /// - /// [`try_is_block_in_chain`]: Self::try_is_block_in_chain - pub fn is_block_in_best_chain(&mut self, chain: C, block_id: BlockId) -> bool { - self.try_is_block_in_chain(chain, block_id) - .expect("chain oracle error is infallible") - } -} diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index f50f454b..dac05e72 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -5,7 +5,7 @@ use bitcoin::{OutPoint, Script, Transaction, TxOut}; use crate::{ keychain::Balance, tx_graph::{Additions, TxGraph, TxNode}, - Append, BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex, + Append, BlockAnchor, BlockId, ChainOracle, FullTxOut, ObservedAs, TxIndex, }; /// An outwards-facing view of a transaction that is part of the *best chain*'s history. @@ -220,7 +220,8 @@ impl IndexedTxGraph { // [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`. pub fn try_list_chain_txs<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator, C::Error>> where C: ChainOracle + 'a, @@ -230,7 +231,7 @@ impl IndexedTxGraph { .filter(|tx| self.index.is_tx_relevant(tx)) .filter_map(move |tx| { self.graph - .try_get_chain_position(&chain, tx.txid) + .try_get_chain_position(chain, static_block, tx.txid) .map(|v| { v.map(|observed_in| CanonicalTx { observed_as: observed_in, @@ -243,18 +244,20 @@ impl IndexedTxGraph { pub fn list_chain_txs<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator> where C: ChainOracle + 'a, { - self.try_list_chain_txs(chain) + self.try_list_chain_txs(chain, static_block) .map(|r| r.expect("error is infallible")) } pub fn try_list_chain_txouts<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, @@ -267,13 +270,17 @@ impl IndexedTxGraph { let is_on_coinbase = graph_tx.is_coin_base(); - let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) { - Ok(Some(observed_at)) => observed_at.cloned(), - Ok(None) => return None, - Err(err) => return Some(Err(err)), - }; + let chain_position = + match self + .graph + .try_get_chain_position(chain, static_block, op.txid) + { + Ok(Some(observed_at)) => observed_at.cloned(), + Ok(None) => return None, + Err(err) => return Some(Err(err)), + }; - let spent_by = match self.graph.try_get_spend_in_chain(&chain, op) { + let spent_by = match self.graph.try_get_spend_in_chain(chain, static_block, op) { Ok(Some((obs, txid))) => Some((obs.cloned(), txid)), Ok(None) => None, Err(err) => return Some(Err(err)), @@ -293,41 +300,45 @@ impl IndexedTxGraph { pub fn list_chain_txouts<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, { - self.try_list_chain_txouts(chain) + self.try_list_chain_txouts(chain, static_block) .map(|r| r.expect("error in infallible")) } /// Return relevant unspents. pub fn try_list_chain_utxos<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator>, C::Error>> + 'a where C: ChainOracle + 'a, { - self.try_list_chain_txouts(chain) + self.try_list_chain_txouts(chain, static_block) .filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none())) } pub fn list_chain_utxos<'a, C>( &'a self, - chain: C, + chain: &'a C, + static_block: BlockId, ) -> impl Iterator>> + 'a where C: ChainOracle + 'a, { - self.try_list_chain_utxos(chain) + self.try_list_chain_utxos(chain, static_block) .map(|r| r.expect("error is infallible")) } pub fn try_balance( &self, - chain: C, + chain: &C, + static_block: BlockId, tip: u32, mut should_trust: F, ) -> Result @@ -340,13 +351,13 @@ impl IndexedTxGraph { let mut untrusted_pending = 0; let mut confirmed = 0; - for res in self.try_list_chain_txouts(&chain) { + for res in self.try_list_chain_txouts(chain, static_block) { let txout = res?; match &txout.chain_position { ObservedAs::Confirmed(_) => { if txout.is_on_coinbase { - if txout.is_observed_as_mature(tip) { + if txout.is_observed_as_confirmed_and_mature(tip) { confirmed += txout.txout.value; } else { immature += txout.txout.value; @@ -371,34 +382,45 @@ impl IndexedTxGraph { }) } - pub fn balance(&self, chain: C, tip: u32, should_trust: F) -> Balance + pub fn balance( + &self, + chain: &C, + static_block: BlockId, + tip: u32, + should_trust: F, + ) -> Balance where C: ChainOracle, F: FnMut(&Script) -> bool, { - self.try_balance(chain, tip, should_trust) + self.try_balance(chain, static_block, tip, should_trust) .expect("error is infallible") } - pub fn try_balance_at(&self, chain: C, height: u32) -> Result + pub fn try_balance_at( + &self, + chain: &C, + static_block: BlockId, + height: u32, + ) -> Result where C: ChainOracle, { let mut sum = 0; - for txo_res in self.try_list_chain_txouts(chain) { + for txo_res in self.try_list_chain_txouts(chain, static_block) { let txo = txo_res?; - if txo.is_observed_as_spendable(height) { + if txo.is_observed_as_confirmed_and_spendable(height) { sum += txo.txout.value; } } Ok(sum) } - pub fn balance_at(&self, chain: C, height: u32) -> u64 + pub fn balance_at(&self, chain: &C, static_block: BlockId, height: u32) -> u64 where C: ChainOracle, { - self.try_balance_at(chain, height) + self.try_balance_at(chain, static_block, height) .expect("error is infallible") } } diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index e1b24ad0..20b54a2f 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -22,16 +22,25 @@ pub struct LocalChain { impl ChainOracle for LocalChain { type Error = Infallible; - fn get_tip_in_best_chain(&self) -> Result, Self::Error> { - Ok(self - .blocks - .iter() - .last() - .map(|(&height, &hash)| BlockId { height, hash })) - } - - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { - Ok(self.blocks.get(&height).cloned()) + fn is_block_in_chain( + &self, + block: BlockId, + static_block: BlockId, + ) -> Result, Self::Error> { + if block.height > static_block.height { + return Ok(None); + } + Ok( + match ( + self.blocks.get(&block.height), + self.blocks.get(&static_block.height), + ) { + (Some(&hash), Some(&static_hash)) => { + Some(hash == block.hash && static_hash == static_block.hash) + } + _ => None, + }, + ) } } diff --git a/crates/chain/src/sparse_chain.rs b/crates/chain/src/sparse_chain.rs index b615f4aa..acc61601 100644 --- a/crates/chain/src/sparse_chain.rs +++ b/crates/chain/src/sparse_chain.rs @@ -460,16 +460,20 @@ impl std::error::Error for UpdateError

{} impl

ChainOracle for SparseChain

{ type Error = Infallible; - fn get_tip_in_best_chain(&self) -> Result, Self::Error> { - Ok(self - .checkpoints - .iter() - .last() - .map(|(&height, &hash)| BlockId { height, hash })) - } - - fn get_block_in_best_chain(&self, height: u32) -> Result, Self::Error> { - Ok(self.checkpoint_at(height).map(|b| b.hash)) + fn is_block_in_chain( + &self, + block: BlockId, + static_block: BlockId, + ) -> Result, Self::Error> { + Ok( + match ( + self.checkpoint_at(block.height), + self.checkpoint_at(static_block.height), + ) { + (Some(b), Some(static_b)) => Some(b == block && static_b == static_block), + _ => None, + }, + ) } } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 0959456d..e3afce0e 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -55,7 +55,7 @@ //! assert!(additions.is_empty()); //! ``` -use crate::{collections::*, BlockAnchor, ChainOracle, ForEachTxOut, ObservedAs}; +use crate::{collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, ObservedAs}; use alloc::vec::Vec; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; use core::{ @@ -596,7 +596,8 @@ impl TxGraph { /// TODO: Also return conflicting tx list, ordered by last_seen. pub fn try_get_chain_position( &self, - chain: C, + chain: &C, + static_block: BlockId, txid: Txid, ) -> Result>, C::Error> where @@ -610,8 +611,28 @@ impl TxGraph { }; for anchor in anchors { - if chain.is_block_in_best_chain(anchor.anchor_block())? { - return Ok(Some(ObservedAs::Confirmed(anchor))); + match chain.is_block_in_chain(anchor.anchor_block(), static_block)? { + Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), + Some(false) => continue, + // if we cannot determine whether block is in the best chain, we can check whether + // a spending transaction is confirmed in best chain, and if so, it is guaranteed + // that the tx being spent (this tx) is in the best chain + None => { + let spending_anchors = self + .spends + .range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX)) + .flat_map(|(_, spending_txids)| spending_txids) + .filter_map(|spending_txid| self.txs.get(spending_txid)) + .flat_map(|(_, spending_anchors, _)| spending_anchors); + for spending_anchor in spending_anchors { + match chain + .is_block_in_chain(spending_anchor.anchor_block(), static_block)? + { + Some(true) => return Ok(Some(ObservedAs::Confirmed(anchor))), + _ => continue, + } + } + } } } @@ -620,8 +641,7 @@ impl TxGraph { let tx = match tx_node { TxNodeInternal::Whole(tx) => tx, TxNodeInternal::Partial(_) => { - // [TODO] Unfortunately, we can't iterate over conflicts of partial txs right now! - // [TODO] So we just assume the partial tx does not exist in the best chain :/ + // Partial transactions (outputs only) cannot have conflicts. return Ok(None); } }; @@ -629,8 +649,8 @@ impl TxGraph { // If a conflicting tx is in the best chain, or has `last_seen` higher than this tx, then // this tx cannot exist in the best chain for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) { - for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) { - if chain.is_block_in_best_chain(block_id)? { + for block in conflicting_tx.anchors.iter().map(A::anchor_block) { + if chain.is_block_in_chain(block, static_block)? == Some(true) { // conflicting tx is in best chain, so the current tx cannot be in best chain! return Ok(None); } @@ -643,31 +663,37 @@ impl TxGraph { Ok(Some(ObservedAs::Unconfirmed(last_seen))) } - pub fn get_chain_position(&self, chain: C, txid: Txid) -> Option> + pub fn get_chain_position( + &self, + chain: &C, + static_block: BlockId, + txid: Txid, + ) -> Option> where C: ChainOracle, { - self.try_get_chain_position(chain, txid) + self.try_get_chain_position(chain, static_block, txid) .expect("error is infallible") } pub fn try_get_spend_in_chain( &self, - chain: C, + chain: &C, + static_block: BlockId, outpoint: OutPoint, ) -> Result, Txid)>, C::Error> where C: ChainOracle, { if self - .try_get_chain_position(&chain, outpoint.txid)? + .try_get_chain_position(chain, static_block, outpoint.txid)? .is_none() { return Ok(None); } if let Some(spends) = self.spends.get(&outpoint) { for &txid in spends { - if let Some(observed_at) = self.try_get_chain_position(&chain, txid)? { + if let Some(observed_at) = self.try_get_chain_position(chain, static_block, txid)? { return Ok(Some((observed_at, txid))); } } @@ -675,11 +701,16 @@ impl TxGraph { Ok(None) } - pub fn get_chain_spend(&self, chain: C, outpoint: OutPoint) -> Option<(ObservedAs<&A>, Txid)> + pub fn get_chain_spend( + &self, + chain: &C, + static_block: BlockId, + outpoint: OutPoint, + ) -> Option<(ObservedAs<&A>, Txid)> where C: ChainOracle, { - self.try_get_spend_in_chain(chain, outpoint) + self.try_get_spend_in_chain(chain, static_block, outpoint) .expect("error is infallible") } }