Skip to content

Commit 611d2e3

Browse files
committed
[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.
1 parent bff80ec commit 611d2e3

File tree

6 files changed

+184
-203
lines changed

6 files changed

+184
-203
lines changed

crates/chain/src/chain_data.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ impl<A: BlockAnchor> FullTxOut<ObservedAs<A>> {
248248
/// [`ObservedAs<A>`] where `A` implements [`BlockAnchor`].
249249
///
250250
/// [`is_mature`]: Self::is_mature
251-
pub fn is_observed_as_mature(&self, tip: u32) -> bool {
251+
pub fn is_observed_as_confirmed_and_mature(&self, tip: u32) -> bool {
252252
if !self.is_on_coinbase {
253253
return false;
254254
}
@@ -275,8 +275,8 @@ impl<A: BlockAnchor> FullTxOut<ObservedAs<A>> {
275275
/// being a [`ObservedAs<A>`] where `A` implements [`BlockAnchor`].
276276
///
277277
/// [`is_spendable_at`]: Self::is_spendable_at
278-
pub fn is_observed_as_spendable(&self, tip: u32) -> bool {
279-
if !self.is_observed_as_mature(tip) {
278+
pub fn is_observed_as_confirmed_and_spendable(&self, tip: u32) -> bool {
279+
if !self.is_observed_as_confirmed_and_mature(tip) {
280280
return false;
281281
}
282282

crates/chain/src/chain_oracle.rs

Lines changed: 51 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,77 @@
1-
use core::{convert::Infallible, marker::PhantomData};
1+
use crate::collections::HashSet;
2+
use core::marker::PhantomData;
23

3-
use alloc::collections::BTreeMap;
4+
use alloc::{collections::VecDeque, vec::Vec};
45
use bitcoin::BlockHash;
56

67
use crate::BlockId;
78

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.
9+
/// Represents a service that tracks the blockchain.
10+
///
11+
/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
12+
/// is an ancestor of another "static block".
13+
///
14+
/// [`is_block_in_chain`]: Self::is_block_in_chain
1315
pub trait ChainOracle {
1416
/// Error type.
1517
type Error: core::fmt::Debug;
1618

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-
}
19+
/// Determines whether `block` of [`BlockId`] exists as an ancestor of `static_block`.
20+
///
21+
/// If `None` is returned, it means the implementation cannot determine whether `block` exists.
22+
fn is_block_in_chain(
23+
&self,
24+
block: BlockId,
25+
static_block: BlockId,
26+
) -> Result<Option<bool>, Self::Error>;
4527
}
4628

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>,
29+
/// A cache structure increases the performance of getting chain data.
30+
///
31+
/// A simple FIFO cache replacement policy is used. Something more efficient and advanced can be
32+
/// implemented later.
33+
#[derive(Debug, Default)]
34+
pub struct CacheBackend<C> {
35+
cache: HashSet<(BlockHash, BlockHash)>,
36+
fifo: VecDeque<(BlockHash, BlockHash)>,
5337
marker: PhantomData<C>,
5438
}
5539

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)
40+
impl<C> CacheBackend<C> {
41+
/// Get the number of elements in the cache.
42+
pub fn cache_size(&self) -> usize {
43+
self.cache.len()
7844
}
7945

80-
/// Update the `tip_height` with the [`ChainOracle`]'s tip.
46+
/// Prunes the cache to reach the `max_size` target.
8147
///
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(())
48+
/// Returns pruned elements.
49+
pub fn prune(&mut self, max_size: usize) -> Vec<(BlockHash, BlockHash)> {
50+
let prune_count = self.cache.len().saturating_sub(max_size);
51+
(0..prune_count)
52+
.filter_map(|_| self.fifo.pop_front())
53+
.filter(|k| self.cache.remove(k))
54+
.collect()
9055
}
9156

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));
57+
pub fn contains(&self, static_block: BlockId, block: BlockId) -> bool {
58+
if static_block.height < block.height
59+
|| static_block.height == block.height && static_block.hash != block.hash
60+
{
61+
return false;
10062
}
10163

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")
64+
self.cache.contains(&(static_block.hash, block.hash))
14165
}
14266

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-
}
67+
pub fn insert(&mut self, static_block: BlockId, block: BlockId) -> bool {
68+
let cache_key = (static_block.hash, block.hash);
15269

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")
70+
if self.cache.insert(cache_key) {
71+
self.fifo.push_back(cache_key);
72+
true
73+
} else {
74+
false
75+
}
16176
}
16277
}

0 commit comments

Comments
 (0)