Skip to content

Commit 6e59dce

Browse files
committed
[bdk_chain_redesign] chain_oracle::Cache
Introduce `chain_oracle::Cache` which is a cache for requests to the chain oracle. `ChainOracle` has also been moved to the `chain_oracle` module. Introduce `get_tip_in_best_chain` method to the `ChainOracle` trait. This allows for guaranteeing that chain state can be consistent across operations with `IndexedTxGraph`.
1 parent a7eaebb commit 6e59dce

File tree

6 files changed

+186
-36
lines changed

6 files changed

+186
-36
lines changed

crates/chain/src/chain_oracle.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use core::{convert::Infallible, marker::PhantomData};
2+
3+
use alloc::collections::BTreeMap;
4+
use bitcoin::BlockHash;
5+
6+
use crate::BlockId;
7+
8+
/// Represents a service that tracks the best chain history.
9+
/// TODO: How do we ensure the chain oracle is consistent across a single call?
10+
/// * We need to somehow lock the data! What if the ChainOracle is remote?
11+
/// * Get tip method! And check the tip still exists at the end! And every internal call
12+
/// does not go beyond the initial tip.
13+
pub trait ChainOracle {
14+
/// Error type.
15+
type Error: core::fmt::Debug;
16+
17+
/// Get the height and hash of the tip in the best chain.
18+
fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error>;
19+
20+
/// Returns the block hash (if any) of the given `height`.
21+
fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error>;
22+
23+
/// Determines whether the block of [`BlockId`] exists in the best chain.
24+
fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
25+
Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash))
26+
}
27+
}
28+
29+
// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this?
30+
// Box<dyn ChainOracle>, Arc<dyn ChainOracle> ????? I will figure it out
31+
impl<C: ChainOracle> ChainOracle for &C {
32+
type Error = C::Error;
33+
34+
fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error> {
35+
<C as ChainOracle>::get_tip_in_best_chain(self)
36+
}
37+
38+
fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
39+
<C as ChainOracle>::get_block_in_best_chain(self, height)
40+
}
41+
42+
fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
43+
<C as ChainOracle>::is_block_in_best_chain(self, block_id)
44+
}
45+
}
46+
47+
/// This structure increases the performance of getting chain data.
48+
#[derive(Debug)]
49+
pub struct Cache<C> {
50+
assume_final_depth: u32,
51+
tip_height: u32,
52+
cache: BTreeMap<u32, BlockHash>,
53+
marker: PhantomData<C>,
54+
}
55+
56+
impl<C> Cache<C> {
57+
/// Creates a new [`Cache`].
58+
///
59+
/// `assume_final_depth` represents the minimum number of blocks above the block in question
60+
/// when we can assume the block is final (reorgs cannot happen). I.e. a value of 0 means the
61+
/// tip is assumed to be final. The cache only caches blocks that are assumed to be final.
62+
pub fn new(assume_final_depth: u32) -> Self {
63+
Self {
64+
assume_final_depth,
65+
tip_height: 0,
66+
cache: Default::default(),
67+
marker: Default::default(),
68+
}
69+
}
70+
}
71+
72+
impl<C: ChainOracle> Cache<C> {
73+
/// This is the topmost (highest) block height that we assume as final (no reorgs possible).
74+
///
75+
/// Blocks higher than this height are not cached.
76+
pub fn assume_final_height(&self) -> u32 {
77+
self.tip_height.saturating_sub(self.assume_final_depth)
78+
}
79+
80+
/// Update the `tip_height` with the [`ChainOracle`]'s tip.
81+
///
82+
/// `tip_height` is used with `assume_final_depth` to determine whether we should cache a
83+
/// certain block height (`tip_height` - `assume_final_depth`).
84+
pub fn try_update_tip_height(&mut self, chain: C) -> Result<(), C::Error> {
85+
let tip = chain.get_tip_in_best_chain()?;
86+
if let Some(BlockId { height, .. }) = tip {
87+
self.tip_height = height;
88+
}
89+
Ok(())
90+
}
91+
92+
/// Get a block from the cache with the [`ChainOracle`] as fallback.
93+
///
94+
/// If the block does not exist in cache, the logic fallbacks to fetching from the internal
95+
/// [`ChainOracle`]. If the block is at or below the "assume final height", we will also store
96+
/// the missing block in the cache.
97+
pub fn try_get_block(&mut self, chain: C, height: u32) -> Result<Option<BlockHash>, C::Error> {
98+
if let Some(&hash) = self.cache.get(&height) {
99+
return Ok(Some(hash));
100+
}
101+
102+
let hash = chain.get_block_in_best_chain(height)?;
103+
104+
if hash.is_some() && height > self.tip_height {
105+
self.tip_height = height;
106+
}
107+
108+
// only cache block if at least as deep as `assume_final_depth`
109+
let assume_final_height = self.tip_height.saturating_sub(self.assume_final_depth);
110+
if height <= assume_final_height {
111+
if let Some(hash) = hash {
112+
self.cache.insert(height, hash);
113+
}
114+
}
115+
116+
Ok(hash)
117+
}
118+
119+
/// Determines whether the block of `block_id` is in the chain using the cache.
120+
///
121+
/// This uses [`try_get_block`] internally.
122+
///
123+
/// [`try_get_block`]: Self::try_get_block
124+
pub fn try_is_block_in_chain(&mut self, chain: C, block_id: BlockId) -> Result<bool, C::Error> {
125+
match self.try_get_block(chain, block_id.height)? {
126+
Some(hash) if hash == block_id.hash => Ok(true),
127+
_ => Ok(false),
128+
}
129+
}
130+
}
131+
132+
impl<C: ChainOracle<Error = Infallible>> Cache<C> {
133+
/// Updates the `tip_height` with the [`ChainOracle`]'s tip.
134+
///
135+
/// This is the no-error version of [`try_update_tip_height`].
136+
///
137+
/// [`try_update_tip_height`]: Self::try_update_tip_height
138+
pub fn update_tip_height(&mut self, chain: C) {
139+
self.try_update_tip_height(chain)
140+
.expect("chain oracle error is infallible")
141+
}
142+
143+
/// Get a block from the cache with the [`ChainOracle`] as fallback.
144+
///
145+
/// This is the no-error version of [`try_get_block`].
146+
///
147+
/// [`try_get_block`]: Self::try_get_block
148+
pub fn get_block(&mut self, chain: C, height: u32) -> Option<BlockHash> {
149+
self.try_get_block(chain, height)
150+
.expect("chain oracle error is infallible")
151+
}
152+
153+
/// Determines whether the block at `block_id` is in the chain using the cache.
154+
///
155+
/// This is the no-error version of [`try_is_block_in_chain`].
156+
///
157+
/// [`try_is_block_in_chain`]: Self::try_is_block_in_chain
158+
pub fn is_block_in_best_chain(&mut self, chain: C, block_id: BlockId) -> bool {
159+
self.try_is_block_in_chain(chain, block_id)
160+
.expect("chain oracle error is infallible")
161+
}
162+
}

crates/chain/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub mod sparse_chain;
3131
mod tx_data_traits;
3232
pub mod tx_graph;
3333
pub use tx_data_traits::*;
34+
mod chain_oracle;
35+
pub use chain_oracle::*;
3436

3537
#[doc(hidden)]
3638
pub mod example_utils;

crates/chain/src/local_chain.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ pub struct LocalChain {
2222
impl ChainOracle for LocalChain {
2323
type Error = Infallible;
2424

25+
fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error> {
26+
Ok(self
27+
.blocks
28+
.iter()
29+
.last()
30+
.map(|(&height, &hash)| BlockId { height, hash }))
31+
}
32+
2533
fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
2634
Ok(self.blocks.get(&height).cloned())
2735
}
@@ -153,7 +161,7 @@ impl Deref for ChangeSet {
153161
}
154162
}
155163

156-
/// Represents an update failure of [`LocalChain`].j
164+
/// Represents an update failure of [`LocalChain`].
157165
#[derive(Clone, Debug, PartialEq)]
158166
pub enum UpdateError {
159167
/// The update cannot be applied to the chain because the chain suffix it represents did not

crates/chain/src/sparse_chain.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@
307307
//! );
308308
//! ```
309309
use core::{
310+
convert::Infallible,
310311
fmt::Debug,
311312
ops::{Bound, RangeBounds},
312313
};
@@ -457,7 +458,15 @@ impl<P: core::fmt::Debug> core::fmt::Display for UpdateError<P> {
457458
impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
458459

459460
impl<P: ChainPosition> ChainOracle for SparseChain<P> {
460-
type Error = ();
461+
type Error = Infallible;
462+
463+
fn get_tip_in_best_chain(&self) -> Result<Option<BlockId>, Self::Error> {
464+
Ok(self
465+
.checkpoints
466+
.iter()
467+
.last()
468+
.map(|(&height, &hash)| BlockId { height, hash }))
469+
}
461470

462471
fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
463472
Ok(self.checkpoint_at(height).map(|b| b.hash))

crates/chain/src/tx_data_traits.rs

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -56,38 +56,6 @@ impl BlockAnchor for (u32, BlockHash) {
5656
}
5757
}
5858

59-
/// Represents a service that tracks the best chain history.
60-
/// TODO: How do we ensure the chain oracle is consistent across a single call?
61-
/// * We need to somehow lock the data! What if the ChainOracle is remote?
62-
/// * Get tip method! And check the tip still exists at the end! And every internal call
63-
/// does not go beyond the initial tip.
64-
pub trait ChainOracle {
65-
/// Error type.
66-
type Error: core::fmt::Debug;
67-
68-
/// Returns the block hash (if any) of the given `height`.
69-
fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error>;
70-
71-
/// Determines whether the block of [`BlockId`] exists in the best chain.
72-
fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
73-
Ok(matches!(self.get_block_in_best_chain(block_id.height)?, Some(h) if h == block_id.hash))
74-
}
75-
}
76-
77-
// [TODO] We need stuff for smart pointers. Maybe? How does rust lib do this?
78-
// Box<dyn ChainOracle>, Arc<dyn ChainOracle> ????? I will figure it out
79-
impl<C: ChainOracle> ChainOracle for &C {
80-
type Error = C::Error;
81-
82-
fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
83-
<C as ChainOracle>::get_block_in_best_chain(self, height)
84-
}
85-
86-
fn is_block_in_best_chain(&self, block_id: BlockId) -> Result<bool, Self::Error> {
87-
<C as ChainOracle>::is_block_in_best_chain(self, block_id)
88-
}
89-
}
90-
9159
/// Represents an index of transaction data.
9260
pub trait TxIndex {
9361
/// The resultant "additions" when new transaction data is indexed.

crates/chain/src/tx_graph.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,11 +586,12 @@ impl<A: Clone + Ord> TxGraph<A> {
586586

587587
impl<A: BlockAnchor> TxGraph<A> {
588588
/// Get all heights that are relevant to the graph.
589-
pub fn relevant_heights(&self) -> BTreeSet<u32> {
589+
pub fn relevant_heights(&self) -> impl Iterator<Item = u32> + '_ {
590+
let mut visited = HashSet::new();
590591
self.anchors
591592
.iter()
592593
.map(|(a, _)| a.anchor_block().height)
593-
.collect()
594+
.filter(move |&h| visited.insert(h))
594595
}
595596

596597
/// Determines whether a transaction of `txid` is in the best chain.

0 commit comments

Comments
 (0)