-
Notifications
You must be signed in to change notification settings - Fork 307
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[bdk_chain_redesign] Consistent
ChainOracle
The problem with the previous `ChainOracle` interface is that it had no guarantee for consistency. For example, a block deemed to be part of the "best chain" can be reorged out. So when `ChainOracle` is called multiple times for an operation (such as getting the UTXO set), the returned result may be inconsistent. This PR changes `ChainOracle::is_block_in_chain` to take in another input `static_block`, ensuring `block` is an ancestor of `static_block`. Thus, if `static_block` is consistent across the operation, the result will be consistent also. `is_block_in_chain` now returns `Option<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
1 parent
bff80ec
commit 611d2e3
Showing
6 changed files
with
184 additions
and
203 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
Oops, something went wrong.