[bdk_chain_redesign] Introduce ChainOracle
and TxIndex
traits
The chain oracle keeps track of the best chain, while the transaction index indexes transaction data in relation to script pubkeys. This commit also includes initial work on `IndexedTxGraph`.
This commit is contained in:
parent
5ae5fe30eb
commit
61a8606fbc
@ -5,6 +5,15 @@ use crate::{
|
||||
BlockAnchor, COINBASE_MATURITY,
|
||||
};
|
||||
|
||||
/// Represents an observation of some chain data.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Observation<A> {
|
||||
/// The chain data is seen in a block identified by `A`.
|
||||
InBlock(A),
|
||||
/// The chain data is seen at this given unix timestamp.
|
||||
SeenAt(u64),
|
||||
}
|
||||
|
||||
/// Represents the height at which a transaction is confirmed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(
|
||||
|
@ -19,7 +19,7 @@ use crate::{
|
||||
collections::BTreeMap,
|
||||
sparse_chain::ChainPosition,
|
||||
tx_graph::TxGraph,
|
||||
ForEachTxOut,
|
||||
ForEachTxOut, TxIndexAdditions,
|
||||
};
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
@ -85,6 +85,12 @@ impl<K: Ord> DerivationAdditions<K> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord> TxIndexAdditions for DerivationAdditions<K> {
|
||||
fn append_additions(&mut self, other: Self) {
|
||||
self.append(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Default for DerivationAdditions<K> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
collections::*,
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
ForEachTxOut, SpkTxOutIndex,
|
||||
ForEachTxOut, SpkTxOutIndex, TxIndex,
|
||||
};
|
||||
use alloc::{borrow::Cow, vec::Vec};
|
||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, TxOut};
|
||||
@ -88,6 +88,22 @@ impl<K> Deref for KeychainTxOutIndex<K> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone + Ord + Debug> TxIndex for KeychainTxOutIndex<K> {
|
||||
type Additions = DerivationAdditions<K>;
|
||||
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
|
||||
self.scan_txout(outpoint, txout)
|
||||
}
|
||||
|
||||
fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::Additions {
|
||||
self.scan(tx)
|
||||
}
|
||||
|
||||
fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool {
|
||||
self.is_relevant(tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Scans an object for relevant outpoints, which are stored and indexed internally.
|
||||
///
|
||||
|
@ -311,7 +311,7 @@ use core::{
|
||||
ops::{Bound, RangeBounds},
|
||||
};
|
||||
|
||||
use crate::{collections::*, tx_graph::TxGraph, BlockId, FullTxOut, TxHeight};
|
||||
use crate::{collections::*, tx_graph::TxGraph, BlockId, ChainOracle, FullTxOut, TxHeight};
|
||||
use bitcoin::{hashes::Hash, BlockHash, OutPoint, Txid};
|
||||
|
||||
/// This is a non-monotone structure that tracks relevant [`Txid`]s that are ordered by chain
|
||||
@ -456,6 +456,14 @@ impl<P: core::fmt::Debug> core::fmt::Display for UpdateError<P> {
|
||||
#[cfg(feature = "std")]
|
||||
impl<P: core::fmt::Debug> std::error::Error for UpdateError<P> {}
|
||||
|
||||
impl<P: ChainPosition> ChainOracle for SparseChain<P> {
|
||||
type Error = ();
|
||||
|
||||
fn get_block_in_best_chain(&self, height: u32) -> Result<Option<BlockHash>, Self::Error> {
|
||||
Ok(self.checkpoint_at(height).map(|b| b.hash))
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: ChainPosition> SparseChain<P> {
|
||||
/// Creates a new chain from a list of block hashes and heights. The caller must guarantee they
|
||||
/// are in the same chain.
|
||||
|
@ -2,7 +2,7 @@ use core::ops::RangeBounds;
|
||||
|
||||
use crate::{
|
||||
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
|
||||
ForEachTxOut,
|
||||
ForEachTxOut, TxIndex,
|
||||
};
|
||||
use bitcoin::{self, OutPoint, Script, Transaction, TxOut, Txid};
|
||||
|
||||
@ -52,6 +52,25 @@ impl<I> Default for SpkTxOutIndex<I> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Clone + Ord> TxIndex for SpkTxOutIndex<I> {
|
||||
type Additions = BTreeSet<I>;
|
||||
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
|
||||
self.scan_txout(outpoint, txout)
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn index_tx(&mut self, tx: &Transaction) -> Self::Additions {
|
||||
self.scan(tx)
|
||||
}
|
||||
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool {
|
||||
self.is_relevant(tx)
|
||||
}
|
||||
}
|
||||
|
||||
/// This macro is used instead of a member function of `SpkTxOutIndex`, which would result in a
|
||||
/// compiler error[E0521]: "borrowed data escapes out of closure" when we attempt to take a
|
||||
/// reference out of the `ForEachTxOut` closure during scanning.
|
||||
|
@ -1,3 +1,4 @@
|
||||
use alloc::collections::BTreeSet;
|
||||
use bitcoin::{Block, BlockHash, OutPoint, Transaction, TxOut};
|
||||
|
||||
use crate::BlockId;
|
||||
@ -44,8 +45,79 @@ pub trait BlockAnchor:
|
||||
fn anchor_block(&self) -> BlockId;
|
||||
}
|
||||
|
||||
impl<A: BlockAnchor> BlockAnchor for &'static A {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
<A as BlockAnchor>::anchor_block(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockAnchor for (u32, BlockHash) {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
(*self).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a service that tracks the best chain history.
|
||||
pub trait ChainOracle {
|
||||
/// Error type.
|
||||
type Error: core::fmt::Debug;
|
||||
|
||||
/// 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))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: ChainOracle> ChainOracle for &C {
|
||||
type Error = C::Error;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents changes to a [`TxIndex`] implementation.
|
||||
pub trait TxIndexAdditions: Default {
|
||||
/// Append `other` on top of `self`.
|
||||
fn append_additions(&mut self, other: Self);
|
||||
}
|
||||
|
||||
impl<I: Ord> TxIndexAdditions for BTreeSet<I> {
|
||||
fn append_additions(&mut self, mut other: Self) {
|
||||
self.append(&mut other);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an index of transaction data.
|
||||
pub trait TxIndex {
|
||||
/// The resultant "additions" when new transaction data is indexed.
|
||||
type Additions: TxIndexAdditions;
|
||||
|
||||
/// Scan and index the given `outpoint` and `txout`.
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions;
|
||||
|
||||
/// Scan and index the given transaction.
|
||||
fn index_tx(&mut self, tx: &Transaction) -> Self::Additions {
|
||||
let txid = tx.txid();
|
||||
tx.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(vout, txout)| self.index_txout(OutPoint::new(txid, vout as _), txout))
|
||||
.reduce(|mut acc, other| {
|
||||
acc.append_additions(other);
|
||||
acc
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A transaction is relevant if it contains a txout with a script_pubkey that we own, or if it
|
||||
/// spends an already-indexed outpoint that we have previously indexed.
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
|
||||
}
|
||||
|
@ -55,7 +55,10 @@
|
||||
//! assert!(additions.is_empty());
|
||||
//! ```
|
||||
|
||||
use crate::{collections::*, BlockAnchor, BlockId, ForEachTxOut};
|
||||
use crate::{
|
||||
collections::*, BlockAnchor, BlockId, ChainOracle, ForEachTxOut, Observation, TxIndex,
|
||||
TxIndexAdditions,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{OutPoint, Transaction, TxOut, Txid};
|
||||
use core::ops::{Deref, RangeInclusive};
|
||||
@ -209,6 +212,12 @@ impl<A> TxGraph<A> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_anchors_and_last_seen(&self, txid: Txid) -> Option<(&BTreeSet<A>, u64)> {
|
||||
self.txs
|
||||
.get(&txid)
|
||||
.map(|(_, anchors, last_seen)| (anchors, *last_seen))
|
||||
}
|
||||
|
||||
/// Calculates the fee of a given transaction. Returns 0 if `tx` is a coinbase transaction.
|
||||
/// Returns `Some(_)` if we have all the `TxOut`s being spent by `tx` in the graph (either as
|
||||
/// the full transactions or individual txouts). If the returned value is negative, then the
|
||||
@ -462,6 +471,75 @@ impl<A: BlockAnchor> TxGraph<A> {
|
||||
*update_last_seen = seen_at;
|
||||
self.determine_additions(&update)
|
||||
}
|
||||
|
||||
/// Determines whether a transaction of `txid` is in the best chain.
|
||||
///
|
||||
/// TODO: Also return conflicting tx list, ordered by last_seen.
|
||||
pub fn is_txid_in_best_chain<C>(&self, chain: C, txid: Txid) -> Result<bool, C::Error>
|
||||
where
|
||||
C: ChainOracle,
|
||||
{
|
||||
let (tx_node, anchors, &last_seen) = match self.txs.get(&txid) {
|
||||
Some((tx, anchors, last_seen)) if !(anchors.is_empty() && *last_seen == 0) => {
|
||||
(tx, anchors, last_seen)
|
||||
}
|
||||
_ => return Ok(false),
|
||||
};
|
||||
|
||||
for block_id in anchors.iter().map(A::anchor_block) {
|
||||
if chain.is_block_in_best_chain(block_id)? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
// The tx is not anchored to a block which is in the best chain, let's check whether we can
|
||||
// ignore it by checking conflicts!
|
||||
let tx = match tx_node {
|
||||
TxNode::Whole(tx) => tx,
|
||||
TxNode::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 :/
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
// [TODO] Is this logic correct? I do not think so, but it should be good enough for now!
|
||||
let mut latest_last_seen = 0_u64;
|
||||
for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx(txid)) {
|
||||
for block_id in conflicting_tx.anchors.iter().map(A::anchor_block) {
|
||||
if chain.is_block_in_best_chain(block_id)? {
|
||||
// conflicting tx is in best chain, so the current tx cannot be in best chain!
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
if conflicting_tx.last_seen > latest_last_seen {
|
||||
latest_last_seen = conflicting_tx.last_seen;
|
||||
}
|
||||
}
|
||||
if last_seen >= latest_last_seen {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if `outpoint` exists in best chain and is unspent.
|
||||
pub fn is_unspent<C>(&self, chain: C, outpoint: OutPoint) -> Result<bool, C::Error>
|
||||
where
|
||||
C: ChainOracle,
|
||||
{
|
||||
if !self.is_txid_in_best_chain(&chain, outpoint.txid)? {
|
||||
return Ok(false);
|
||||
}
|
||||
if let Some(spends) = self.spends.get(&outpoint) {
|
||||
for &txid in spends {
|
||||
if self.is_txid_in_best_chain(&chain, txid)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> TxGraph<A> {
|
||||
@ -568,6 +646,108 @@ impl<A> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IndexedAdditions<A, D> {
|
||||
pub graph_additions: Additions<A>,
|
||||
pub index_delta: D,
|
||||
}
|
||||
|
||||
impl<A, D: Default> Default for IndexedAdditions<A, D> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
graph_additions: Default::default(),
|
||||
index_delta: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: BlockAnchor, D: TxIndexAdditions> TxIndexAdditions for IndexedAdditions<A, D> {
|
||||
fn append_additions(&mut self, other: Self) {
|
||||
let Self {
|
||||
graph_additions,
|
||||
index_delta,
|
||||
} = other;
|
||||
self.graph_additions.append(graph_additions);
|
||||
self.index_delta.append_additions(index_delta);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IndexedTxGraph<A, I> {
|
||||
graph: TxGraph<A>,
|
||||
index: I,
|
||||
}
|
||||
|
||||
impl<A, I: Default> Default for IndexedTxGraph<A, I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
index: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
|
||||
pub fn insert_txout(
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
txout: &TxOut,
|
||||
observation: Observation<A>,
|
||||
) -> IndexedAdditions<A, I::Additions> {
|
||||
IndexedAdditions {
|
||||
graph_additions: {
|
||||
let mut graph_additions = self.graph.insert_txout(outpoint, txout.clone());
|
||||
graph_additions.append(match observation {
|
||||
Observation::InBlock(anchor) => self.graph.insert_anchor(outpoint.txid, anchor),
|
||||
Observation::SeenAt(seen_at) => {
|
||||
self.graph.insert_seen_at(outpoint.txid, seen_at)
|
||||
}
|
||||
});
|
||||
graph_additions
|
||||
},
|
||||
index_delta: <I as TxIndex>::index_txout(&mut self.index, outpoint, txout),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_tx(
|
||||
&mut self,
|
||||
tx: &Transaction,
|
||||
observation: Observation<A>,
|
||||
) -> IndexedAdditions<A, I::Additions> {
|
||||
let txid = tx.txid();
|
||||
IndexedAdditions {
|
||||
graph_additions: {
|
||||
let mut graph_additions = self.graph.insert_tx(tx.clone());
|
||||
graph_additions.append(match observation {
|
||||
Observation::InBlock(anchor) => self.graph.insert_anchor(txid, anchor),
|
||||
Observation::SeenAt(seen_at) => self.graph.insert_seen_at(txid, seen_at),
|
||||
});
|
||||
graph_additions
|
||||
},
|
||||
index_delta: <I as TxIndex>::index_tx(&mut self.index, tx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_and_insert_txs<'t, T>(
|
||||
&mut self,
|
||||
txs: T,
|
||||
observation: Observation<A>,
|
||||
) -> IndexedAdditions<A, I::Additions>
|
||||
where
|
||||
T: Iterator<Item = &'t Transaction>,
|
||||
{
|
||||
txs.filter_map(|tx| {
|
||||
if self.index.is_tx_relevant(tx) {
|
||||
Some(self.insert_tx(tx, observation.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.fold(IndexedAdditions::default(), |mut acc, other| {
|
||||
acc.append_additions(other);
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure that represents changes to a [`TxGraph`].
|
||||
///
|
||||
/// It is named "additions" because [`TxGraph`] is monotone, so transactions can only be added and
|
||||
|
Loading…
x
Reference in New Issue
Block a user