Skip to content

Commit

Permalink
[bdk_chain_redesign] Consistent ChainOracle
Browse files Browse the repository at this point in the history
The problem with the previous `ChainOracle` interface is that it had no
guarantee for consistency. For example, a block deemed to be part of the
"best chain" can be reorged out. So when `ChainOracle` is called
multiple times for an operation (such as getting the UTXO set), the
returned result may be inconsistent.

This PR changes `ChainOracle::is_block_in_chain` to take in another
input `static_block`, ensuring `block` is an ancestor of `static_block`.
Thus, if `static_block` is consistent across the operation, the result
will be consistent also.

`is_block_in_chain` now returns `Option<bool>`. The `None` case means
that the oracle implementation cannot determine whether block is an
ancestor of static block. `IndexedTxGraph::list_chain_txouts` handles
this case by checking child spends that are in chain, and if so, the
parent tx must be in chain too.
  • Loading branch information
evanlinjin committed Apr 10, 2023
1 parent bff80ec commit 611d2e3
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 203 deletions.
6 changes: 3 additions & 3 deletions crates/chain/src/chain_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ impl<A: BlockAnchor> FullTxOut<ObservedAs<A>> {
/// [`ObservedAs<A>`] 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;
}
Expand All @@ -275,8 +275,8 @@ impl<A: BlockAnchor> FullTxOut<ObservedAs<A>> {
/// being a [`ObservedAs<A>`] 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;
}

Expand Down
187 changes: 51 additions & 136 deletions crates/chain/src/chain_oracle.rs
Original file line number Diff line number Diff line change
@@ -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<Option<BlockId>, Self::Error>;

/// Returns the block hash (if any) of the given `height`.
fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error>;

/// Determines whether the block of [`BlockId`] exists in the best chain.
fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash))
}
}

// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this?
// Box<dyn ChainOracle>, Arc<dyn ChainOracle> ????? I will figure it out
impl<C: ChainOracle> ChainOracle for &C {
type Error = C::Error;

fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error> {
<C as ChainOracle>::get_tip_in_best_chain(self)
}

fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
<C as ChainOracle>::get_block_in_best_chain(self, height)
}

fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
<C as ChainOracle>::is_block_in_best_chain(self, block_id)
}
/// Determines whether `block` of [`BlockId`] exists as an ancestor of `static_block`.
///
/// If `None` is returned, it means the implementation cannot determine whether `block` exists.
fn is_block_in_chain(
&self,
block: BlockId,
static_block: BlockId,
) -> Result<Option<bool>, Self::Error>;
}

/// This structure increases the performance of getting chain data.
#[derive(Debug)]
pub struct Cache<C> {
assume_final_depth: u32,
tip_height: u32,
cache: BTreeMap<u32, BlockHash>,
/// 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<C> {
cache: HashSet<(BlockHash, BlockHash)>,
fifo: VecDeque<(BlockHash, BlockHash)>,
marker: PhantomData<C>,
}

impl<C> Cache<C> {
/// Creates a new [`Cache`].
///
/// `assume_final_depth` represents the minimum number of blocks above the block in question
/// when we can assume the block is final (reorgs cannot happen). I.e. a value of 0 means the
/// tip is assumed to be final. The cache only caches blocks that are assumed to be final.
pub fn new(assume_final_depth: u32) -> Self {
Self {
assume_final_depth,
tip_height: 0,
cache: Default::default(),
marker: Default::default(),
}
}
}

impl<C: ChainOracle> Cache<C> {
/// This is the topmost (highest) block height that we assume as final (no reorgs possible).
///
/// Blocks higher than this height are not cached.
pub fn assume_final_height(&self) -> u32 {
self.tip_height.saturating_sub(self.assume_final_depth)
impl<C> CacheBackend<C> {
/// Get the number of elements in the cache.
pub fn cache_size(&self) -> usize {
self.cache.len()
}

/// Update the `tip_height` with the [`ChainOracle`]'s tip.
/// Prunes the cache to reach the `max_size` target.
///
/// `tip_height` is used with `assume_final_depth` to determine whether we should cache a
/// certain block height (`tip_height` - `assume_final_depth`).
pub fn try_update_tip_height(&mut self, chain: C) -> Result<(), C::Error> {
let tip = chain.get_tip_in_best_chain()?;
if let Some(BlockId { height, .. }) = tip {
self.tip_height = height;
}
Ok(())
/// Returns pruned elements.
pub fn prune(&mut self, max_size: usize) -> Vec<(BlockHash, BlockHash)> {
let prune_count = self.cache.len().saturating_sub(max_size);
(0..prune_count)
.filter_map(|_| self.fifo.pop_front())
.filter(|k| self.cache.remove(k))
.collect()
}

/// Get a block from the cache with the [`ChainOracle`] as fallback.
///
/// If the block does not exist in cache, the logic fallbacks to fetching from the internal
/// [`ChainOracle`]. If the block is at or below the "assume final height", we will also store
/// the missing block in the cache.
pub fn try_get_block(&mut self, chain: C, height: u32) -> Result<Option<BlockHash>, C::Error> {
if let Some(&hash) = self.cache.get(&height) {
return Ok(Some(hash));
pub fn contains(&self, static_block: BlockId, block: BlockId) -> bool {
if static_block.height < block.height
|| static_block.height == block.height && static_block.hash != block.hash
{
return false;
}

let hash = chain.get_block_in_best_chain(height)?;

if hash.is_some() && height > self.tip_height {
self.tip_height = height;
}

// only cache block if at least as deep as `assume_final_depth`
let assume_final_height = self.tip_height.saturating_sub(self.assume_final_depth);
if height <= assume_final_height {
if let Some(hash) = hash {
self.cache.insert(height, hash);
}
}

Ok(hash)
}

/// Determines whether the block of `block_id` is in the chain using the cache.
///
/// This uses [`try_get_block`] internally.
///
/// [`try_get_block`]: Self::try_get_block
pub fn try_is_block_in_chain(&mut self, chain: C, block_id: BlockId) -> Result<bool, C::Error> {
match self.try_get_block(chain, block_id.height)? {
Some(hash) if hash == block_id.hash => Ok(true),
_ => Ok(false),
}
}
}

impl<C: ChainOracle<Error = Infallible>> Cache<C> {
/// Updates the `tip_height` with the [`ChainOracle`]'s tip.
///
/// This is the no-error version of [`try_update_tip_height`].
///
/// [`try_update_tip_height`]: Self::try_update_tip_height
pub fn update_tip_height(&mut self, chain: C) {
self.try_update_tip_height(chain)
.expect("chain oracle error is infallible")
self.cache.contains(&(static_block.hash, block.hash))
}

/// Get a block from the cache with the [`ChainOracle`] as fallback.
///
/// This is the no-error version of [`try_get_block`].
///
/// [`try_get_block`]: Self::try_get_block
pub fn get_block(&mut self, chain: C, height: u32) -> Option<BlockHash> {
self.try_get_block(chain, height)
.expect("chain oracle error is infallible")
}
pub fn insert(&mut self, static_block: BlockId, block: BlockId) -> bool {
let cache_key = (static_block.hash, block.hash);

/// Determines whether the block at `block_id` is in the chain using the cache.
///
/// This is the no-error version of [`try_is_block_in_chain`].
///
/// [`try_is_block_in_chain`]: Self::try_is_block_in_chain
pub fn is_block_in_best_chain(&mut self, chain: C, block_id: BlockId) -> bool {
self.try_is_block_in_chain(chain, block_id)
.expect("chain oracle error is infallible")
if self.cache.insert(cache_key) {
self.fifo.push_back(cache_key);
true
} else {
false
}
}
}
Loading

0 comments on commit 611d2e3

Please sign in to comment.