Skip to content

Commit b5b442a

Browse files
evanlinjinValuedMammalLagginTimes
committed
feat(chain): Add TxGraph methods that handle expected spk txids
* `TxGraph::try_list_expected_spk_txids` * `TxGraph::list_expected_spk_txids` * `IndexedTxGraph::try_list_expected_spk_txids` * `IndexedTxGraph::list_expected_spk_txids` Co-authored-by: valued mammal <[email protected]> Co-authored-by: Wei Chen <[email protected]>
1 parent e692768 commit b5b442a

File tree

4 files changed

+173
-3
lines changed

4 files changed

+173
-3
lines changed

crates/chain/src/indexed_tx_graph.rs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the
22
//! [`IndexedTxGraph`] documentation for more.
3-
use core::fmt::Debug;
3+
use core::{
4+
convert::Infallible,
5+
fmt::{self, Debug},
6+
ops::RangeBounds,
7+
};
48

59
use alloc::{sync::Arc, vec::Vec};
6-
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
10+
use bitcoin::{Block, OutPoint, ScriptBuf, Transaction, TxOut, Txid};
711

812
use crate::{
13+
spk_txout::SpkTxOutIndex,
914
tx_graph::{self, TxGraph},
10-
Anchor, BlockId, Indexer, Merge, TxPosInBlock,
15+
Anchor, BlockId, ChainOracle, Indexer, Merge, TxPosInBlock,
1116
};
1217

1318
/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
@@ -127,6 +132,19 @@ where
127132
self.graph.insert_seen_at(txid, seen_at).into()
128133
}
129134

135+
/// Inserts the given `evicted_at` for `txid`.
136+
///
137+
/// The `evicted_at` timestamp represents the last known time when the transaction was observed
138+
/// to be missing from the mempool. If `txid` was previously recorded with an earlier
139+
/// `evicted_at` value, it is updated only if the new value is greater.
140+
pub fn insert_evicted_at(&mut self, txid: Txid, evicted_at: u64) -> ChangeSet<A, I::ChangeSet> {
141+
let tx_graph = self.graph.insert_evicted_at(txid, evicted_at);
142+
ChangeSet {
143+
tx_graph,
144+
..Default::default()
145+
}
146+
}
147+
130148
/// Batch insert transactions, filtering out those that are irrelevant.
131149
///
132150
/// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant
@@ -301,6 +319,58 @@ where
301319
}
302320
}
303321

322+
impl<A, X> IndexedTxGraph<A, X>
323+
where
324+
A: Anchor,
325+
{
326+
/// List txids that are expected to exist under the given spks.
327+
///
328+
/// This is used to fill [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids).
329+
///
330+
/// The spk index range can be contrained with `range`.
331+
///
332+
/// # Error
333+
///
334+
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
335+
/// returned item.
336+
///
337+
/// If the [`ChainOracle`] is infallible,
338+
/// [`list_expected_spk_txids`](Self::list_expected_spk_txids) can be used instead.
339+
pub fn try_list_expected_spk_txids<'a, C, I>(
340+
&'a self,
341+
chain: &'a C,
342+
chain_tip: BlockId,
343+
spk_index_range: impl RangeBounds<I> + 'a,
344+
) -> impl Iterator<Item = Result<(ScriptBuf, Txid), C::Error>> + 'a
345+
where
346+
C: ChainOracle,
347+
X: AsRef<SpkTxOutIndex<I>> + 'a,
348+
I: fmt::Debug + Clone + Ord + 'a,
349+
{
350+
self.graph
351+
.try_list_expected_spk_txids(chain, chain_tip, &self.index, spk_index_range)
352+
}
353+
354+
/// List txids that are expected to exist under the given spks.
355+
///
356+
/// This is the infallible version of
357+
/// [`try_list_expected_spk_txids`](Self::try_list_expected_spk_txids).
358+
pub fn list_expected_spk_txids<'a, C, I>(
359+
&'a self,
360+
chain: &'a C,
361+
chain_tip: BlockId,
362+
spk_index_range: impl RangeBounds<I> + 'a,
363+
) -> impl Iterator<Item = (ScriptBuf, Txid)> + 'a
364+
where
365+
C: ChainOracle<Error = Infallible>,
366+
X: AsRef<SpkTxOutIndex<I>> + 'a,
367+
I: fmt::Debug + Clone + Ord + 'a,
368+
{
369+
self.try_list_expected_spk_txids(chain, chain_tip, spk_index_range)
370+
.map(|r| r.expect("infallible"))
371+
}
372+
}
373+
304374
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
305375
fn as_ref(&self) -> &TxGraph<A> {
306376
&self.graph

crates/chain/src/indexer/keychain_txout.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ impl<K> Default for KeychainTxOutIndex<K> {
136136
}
137137
}
138138

139+
impl<K> AsRef<SpkTxOutIndex<(K, u32)>> for KeychainTxOutIndex<K> {
140+
fn as_ref(&self) -> &SpkTxOutIndex<(K, u32)> {
141+
&self.inner
142+
}
143+
}
144+
139145
impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
140146
type ChangeSet = ChangeSet;
141147

@@ -200,6 +206,11 @@ impl<K> KeychainTxOutIndex<K> {
200206
lookahead,
201207
}
202208
}
209+
210+
/// Get a reference to the internal [`SpkTxOutIndex`].
211+
pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> {
212+
&self.inner
213+
}
203214
}
204215

205216
/// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`].

crates/chain/src/indexer/spk_txout.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ impl<I> Default for SpkTxOutIndex<I> {
5454
}
5555
}
5656

57+
impl<I> AsRef<SpkTxOutIndex<I>> for SpkTxOutIndex<I> {
58+
fn as_ref(&self) -> &SpkTxOutIndex<I> {
59+
self
60+
}
61+
}
62+
5763
impl<I: Clone + Ord + core::fmt::Debug> Indexer for SpkTxOutIndex<I> {
5864
type ChangeSet = ();
5965

@@ -334,4 +340,24 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
334340
.any(|output| self.spk_indices.contains_key(&output.script_pubkey));
335341
input_matches || output_matches
336342
}
343+
344+
/// Find relevant script pubkeys associated with a transaction for tracking and validation.
345+
///
346+
/// Returns a set of script pubkeys from [`SpkTxOutIndex`] that are relevant to the outputs and
347+
/// previous outputs of a given transaction. Inputs are only considered relevant if the parent
348+
/// transactions have been scanned.
349+
pub fn relevant_spks_of_tx(&self, tx: &Transaction) -> BTreeSet<(I, ScriptBuf)> {
350+
let spks_from_inputs = tx.input.iter().filter_map(|txin| {
351+
self.txouts
352+
.get(&txin.previous_output)
353+
.cloned()
354+
.map(|(i, prev_txo)| (i, prev_txo.script_pubkey))
355+
});
356+
let spks_from_outputs = tx
357+
.output
358+
.iter()
359+
.filter_map(|txout| self.spk_indices.get_key_value(&txout.script_pubkey))
360+
.map(|(spk, i)| (i.clone(), spk.clone()));
361+
spks_from_inputs.chain(spks_from_outputs).collect()
362+
}
337363
}

crates/chain/src/tx_graph.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
//! [`insert_txout`]: TxGraph::insert_txout
121121
122122
use crate::collections::*;
123+
use crate::spk_txout::SpkTxOutIndex;
123124
use crate::BlockId;
124125
use crate::CanonicalIter;
125126
use crate::CanonicalReason;
@@ -132,6 +133,7 @@ use bdk_core::ConfirmationBlockTime;
132133
pub use bdk_core::TxUpdate;
133134
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
134135
use core::fmt::{self, Formatter};
136+
use core::ops::RangeBounds;
135137
use core::{
136138
convert::Infallible,
137139
ops::{Deref, RangeInclusive},
@@ -1150,6 +1152,67 @@ impl<A: Anchor> TxGraph<A> {
11501152
self.try_balance(chain, chain_tip, outpoints, trust_predicate)
11511153
.expect("oracle is infallible")
11521154
}
1155+
1156+
/// List txids that are expected to exist under the given spks.
1157+
///
1158+
/// This is used to fill [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids).
1159+
///
1160+
/// The spk index range can be contrained with `range`.
1161+
///
1162+
/// # Error
1163+
///
1164+
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
1165+
/// returned item.
1166+
///
1167+
/// If the [`ChainOracle`] is infallible,
1168+
/// [`list_expected_spk_txids`](Self::list_expected_spk_txids) can be used instead.
1169+
pub fn try_list_expected_spk_txids<'a, C, I>(
1170+
&'a self,
1171+
chain: &'a C,
1172+
chain_tip: BlockId,
1173+
indexer: &'a impl AsRef<SpkTxOutIndex<I>>,
1174+
spk_index_range: impl RangeBounds<I> + 'a,
1175+
) -> impl Iterator<Item = Result<(ScriptBuf, Txid), C::Error>> + 'a
1176+
where
1177+
C: ChainOracle,
1178+
I: fmt::Debug + Clone + Ord + 'a,
1179+
{
1180+
let indexer = indexer.as_ref();
1181+
self.try_list_canonical_txs(chain, chain_tip).flat_map(
1182+
move |res| -> Vec<Result<(ScriptBuf, Txid), C::Error>> {
1183+
let range = &spk_index_range;
1184+
let c_tx = match res {
1185+
Ok(c_tx) => c_tx,
1186+
Err(err) => return vec![Err(err)],
1187+
};
1188+
let relevant_spks = indexer.relevant_spks_of_tx(&c_tx.tx_node);
1189+
relevant_spks
1190+
.into_iter()
1191+
.filter(|(i, _)| range.contains(i))
1192+
.map(|(_, spk)| Ok((spk, c_tx.tx_node.txid)))
1193+
.collect()
1194+
},
1195+
)
1196+
}
1197+
1198+
/// List txids that are expected to exist under the given spks.
1199+
///
1200+
/// This is the infallible version of
1201+
/// [`try_list_expected_spk_txids`](Self::try_list_expected_spk_txids).
1202+
pub fn list_expected_spk_txids<'a, C, I>(
1203+
&'a self,
1204+
chain: &'a C,
1205+
chain_tip: BlockId,
1206+
indexer: &'a impl AsRef<SpkTxOutIndex<I>>,
1207+
spk_index_range: impl RangeBounds<I> + 'a,
1208+
) -> impl Iterator<Item = (ScriptBuf, Txid)> + 'a
1209+
where
1210+
C: ChainOracle<Error = Infallible>,
1211+
I: fmt::Debug + Clone + Ord + 'a,
1212+
{
1213+
self.try_list_expected_spk_txids(chain, chain_tip, indexer, spk_index_range)
1214+
.map(|r| r.expect("infallible"))
1215+
}
11531216
}
11541217

11551218
/// The [`ChangeSet`] represents changes to a [`TxGraph`].

0 commit comments

Comments
 (0)