[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.
This commit is contained in:
志宇
2023-04-10 13:03:51 +08:00
parent bff80ec378
commit 611d2e3ea2
6 changed files with 191 additions and 210 deletions

View File

@@ -5,7 +5,7 @@ use bitcoin::{OutPoint, Script, Transaction, TxOut};
use crate::{
keychain::Balance,
tx_graph::{Additions, TxGraph, TxNode},
Append, BlockAnchor, ChainOracle, FullTxOut, ObservedAs, TxIndex,
Append, BlockAnchor, BlockId, ChainOracle, FullTxOut, ObservedAs, TxIndex,
};
/// An outwards-facing view of a transaction that is part of the *best chain*'s history.
@@ -220,7 +220,8 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
// [TODO] Have to methods, one for relevant-only, and one for any. Have one in `TxGraph`.
pub fn try_list_chain_txs<'a, C>(
&'a self,
chain: C,
chain: &'a C,
static_block: BlockId,
) -> impl Iterator<Item = Result<CanonicalTx<'a, Transaction, A>, C::Error>>
where
C: ChainOracle + 'a,
@@ -230,7 +231,7 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
.filter(|tx| self.index.is_tx_relevant(tx))
.filter_map(move |tx| {
self.graph
.try_get_chain_position(&chain, tx.txid)
.try_get_chain_position(chain, static_block, tx.txid)
.map(|v| {
v.map(|observed_in| CanonicalTx {
observed_as: observed_in,
@@ -243,18 +244,20 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
pub fn list_chain_txs<'a, C>(
&'a self,
chain: C,
chain: &'a C,
static_block: BlockId,
) -> impl Iterator<Item = CanonicalTx<'a, Transaction, A>>
where
C: ChainOracle<Error = Infallible> + 'a,
{
self.try_list_chain_txs(chain)
self.try_list_chain_txs(chain, static_block)
.map(|r| r.expect("error is infallible"))
}
pub fn try_list_chain_txouts<'a, C>(
&'a self,
chain: C,
chain: &'a C,
static_block: BlockId,
) -> impl Iterator<Item = Result<FullTxOut<ObservedAs<A>>, C::Error>> + 'a
where
C: ChainOracle + 'a,
@@ -267,13 +270,17 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
let is_on_coinbase = graph_tx.is_coin_base();
let chain_position = match self.graph.try_get_chain_position(&chain, op.txid) {
Ok(Some(observed_at)) => observed_at.cloned(),
Ok(None) => return None,
Err(err) => return Some(Err(err)),
};
let chain_position =
match self
.graph
.try_get_chain_position(chain, static_block, op.txid)
{
Ok(Some(observed_at)) => observed_at.cloned(),
Ok(None) => return None,
Err(err) => return Some(Err(err)),
};
let spent_by = match self.graph.try_get_spend_in_chain(&chain, op) {
let spent_by = match self.graph.try_get_spend_in_chain(chain, static_block, op) {
Ok(Some((obs, txid))) => Some((obs.cloned(), txid)),
Ok(None) => None,
Err(err) => return Some(Err(err)),
@@ -293,41 +300,45 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
pub fn list_chain_txouts<'a, C>(
&'a self,
chain: C,
chain: &'a C,
static_block: BlockId,
) -> impl Iterator<Item = FullTxOut<ObservedAs<A>>> + 'a
where
C: ChainOracle<Error = Infallible> + 'a,
{
self.try_list_chain_txouts(chain)
self.try_list_chain_txouts(chain, static_block)
.map(|r| r.expect("error in infallible"))
}
/// Return relevant unspents.
pub fn try_list_chain_utxos<'a, C>(
&'a self,
chain: C,
chain: &'a C,
static_block: BlockId,
) -> impl Iterator<Item = Result<FullTxOut<ObservedAs<A>>, C::Error>> + 'a
where
C: ChainOracle + 'a,
{
self.try_list_chain_txouts(chain)
self.try_list_chain_txouts(chain, static_block)
.filter(|r| !matches!(r, Ok(txo) if txo.spent_by.is_none()))
}
pub fn list_chain_utxos<'a, C>(
&'a self,
chain: C,
chain: &'a C,
static_block: BlockId,
) -> impl Iterator<Item = FullTxOut<ObservedAs<A>>> + 'a
where
C: ChainOracle<Error = Infallible> + 'a,
{
self.try_list_chain_utxos(chain)
self.try_list_chain_utxos(chain, static_block)
.map(|r| r.expect("error is infallible"))
}
pub fn try_balance<C, F>(
&self,
chain: C,
chain: &C,
static_block: BlockId,
tip: u32,
mut should_trust: F,
) -> Result<Balance, C::Error>
@@ -340,13 +351,13 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
let mut untrusted_pending = 0;
let mut confirmed = 0;
for res in self.try_list_chain_txouts(&chain) {
for res in self.try_list_chain_txouts(chain, static_block) {
let txout = res?;
match &txout.chain_position {
ObservedAs::Confirmed(_) => {
if txout.is_on_coinbase {
if txout.is_observed_as_mature(tip) {
if txout.is_observed_as_confirmed_and_mature(tip) {
confirmed += txout.txout.value;
} else {
immature += txout.txout.value;
@@ -371,34 +382,45 @@ impl<A: BlockAnchor, I: TxIndex> IndexedTxGraph<A, I> {
})
}
pub fn balance<C, F>(&self, chain: C, tip: u32, should_trust: F) -> Balance
pub fn balance<C, F>(
&self,
chain: &C,
static_block: BlockId,
tip: u32,
should_trust: F,
) -> Balance
where
C: ChainOracle<Error = Infallible>,
F: FnMut(&Script) -> bool,
{
self.try_balance(chain, tip, should_trust)
self.try_balance(chain, static_block, tip, should_trust)
.expect("error is infallible")
}
pub fn try_balance_at<C>(&self, chain: C, height: u32) -> Result<u64, C::Error>
pub fn try_balance_at<C>(
&self,
chain: &C,
static_block: BlockId,
height: u32,
) -> Result<u64, C::Error>
where
C: ChainOracle,
{
let mut sum = 0;
for txo_res in self.try_list_chain_txouts(chain) {
for txo_res in self.try_list_chain_txouts(chain, static_block) {
let txo = txo_res?;
if txo.is_observed_as_spendable(height) {
if txo.is_observed_as_confirmed_and_spendable(height) {
sum += txo.txout.value;
}
}
Ok(sum)
}
pub fn balance_at<C>(&self, chain: C, height: u32) -> u64
pub fn balance_at<C>(&self, chain: &C, static_block: BlockId, height: u32) -> u64
where
C: ChainOracle<Error = Infallible>,
{
self.try_balance_at(chain, height)
self.try_balance_at(chain, static_block, height)
.expect("error is infallible")
}
}