Skip to content

Commit 88e4310

Browse files
evanlinjinLagginTimes
authored andcommitted
feat(chain)!: IndexedTxGraph::expected_unconfirmed_spk_txids
Make this method work when the indexer is `KeychainTxOutIndex`. We reintroduce the ability to get the internal `SpkTxOutIndex` from `KeychainTxOutIndex` so that `SpkTxOutIndex::relevant_spks_of_tx` is callable from `KeychainTxOutIndex`. This commit renames `iter_spks_with_expected_txids` to `expected_unconfirmed_spk_txids` for `TxGraph`, `IndexedTxGraph` and `SyncRequestBuilder`. Docs are also improved to explain how these methods are useful. Remove unused `SyncRequestBuilder` methods.
1 parent 25a4ceb commit 88e4310

9 files changed

Lines changed: 119 additions & 113 deletions

File tree

crates/bitcoind_rpc/tests/test_emitter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
808808

809809
// We evict the expected txs that are missing from mempool.
810810
let exp_txids = graph
811-
.iter_spks_with_expected_txids(&chain, ..)
811+
.expected_unconfirmed_spk_txids(&chain, chain_tip, ..)?
812812
.collect::<Vec<_>>();
813813
assert_eq!(exp_txids, vec![(txid_1, spk)]);
814814
let mempool = emitter

crates/chain/src/indexed_tx_graph.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the
22
//! [`IndexedTxGraph`] documentation for more.
33
4-
use core::fmt;
54
use core::ops::RangeBounds;
65

76
use alloc::{sync::Arc, vec::Vec};
@@ -345,24 +344,50 @@ where
345344
impl<A, I> IndexedTxGraph<A, SpkTxOutIndex<I>>
346345
where
347346
A: Anchor,
348-
I: fmt::Debug + Clone + Ord,
347+
I: core::fmt::Debug + Clone + Ord,
349348
{
350-
/// Returns an iterator over unconfirmed transactions and their associated script pubkeys,
351-
/// filtered within the specified `range`.
349+
/// Iterate over unconfirmed txids that we expect to exist in a chain source's spk history
350+
/// response.
352351
///
353-
/// This function delegates the transaction filtering to [`TxGraph::iter_spks_with_expected_txids`],
354-
/// using the [`SpkTxOutIndex`] stored in [`IndexedTxGraph`]. The [`TxGraph`] internally scans
355-
/// for unconfirmed transactions relevant to the indexed outputs.
356-
pub fn iter_spks_with_expected_txids<'a, O>(
352+
/// This is used to fill [`SyncRequestBuilder::expected_unconfirmed_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_unconfirmed_spk_txids).
353+
///
354+
/// The spk range can be contrained with `range`.
355+
pub fn expected_unconfirmed_spk_txids<'a, O>(
357356
&'a self,
358357
chain: &'a O,
358+
chain_tip: BlockId,
359359
range: impl RangeBounds<I> + 'a,
360-
) -> impl Iterator<Item = (Txid, ScriptBuf)> + 'a
360+
) -> Result<impl Iterator<Item = (Txid, ScriptBuf)> + 'a, O::Error>
361+
where
362+
O: ChainOracle,
363+
{
364+
self.graph
365+
.expected_unconfirmed_spk_txids(chain, chain_tip, &self.index, range)
366+
}
367+
}
368+
369+
impl<A, K> IndexedTxGraph<A, crate::keychain_txout::KeychainTxOutIndex<K>>
370+
where
371+
A: Anchor,
372+
K: core::fmt::Debug + Clone + Ord,
373+
{
374+
/// Iterate over unconfirmed txids that we expect to exist in a chain source's spk history
375+
/// response.
376+
///
377+
/// This is used to fill [`SyncRequestBuilder::expected_unconfirmed_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_unconfirmed_spk_txids).
378+
///
379+
/// The spk range can be contrained with `range`.
380+
pub fn expected_unconfirmed_spk_txids<'a, O>(
381+
&'a self,
382+
chain: &'a O,
383+
chain_tip: BlockId,
384+
range: impl RangeBounds<(K, u32)> + 'a,
385+
) -> Result<impl Iterator<Item = (Txid, ScriptBuf)> + 'a, O::Error>
361386
where
362-
O: ChainOracle<Error = core::convert::Infallible>,
387+
O: ChainOracle,
363388
{
364389
self.graph
365-
.iter_spks_with_expected_txids(chain, &self.index, range)
390+
.expected_unconfirmed_spk_txids(chain, chain_tip, &self.index, range)
366391
}
367392
}
368393

crates/chain/src/indexer/keychain_txout.rs

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@ use crate::{
77
spk_client::{FullScanRequestBuilder, SyncRequestBuilder},
88
spk_iter::BIP32_MAX_INDEX,
99
spk_txout::SpkTxOutIndex,
10-
Anchor, CanonicalIter, CanonicalReason, ChainOracle, DescriptorExt, DescriptorId, Indexed,
11-
Indexer, KeychainIndexed, SpkIterator,
10+
DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator,
1211
};
1312
use alloc::{borrow::ToOwned, vec::Vec};
1413
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
1514
use core::{
16-
convert::Infallible,
1715
fmt::Debug,
1816
ops::{Bound, RangeBounds},
1917
};
@@ -138,6 +136,12 @@ impl<K> Default for KeychainTxOutIndex<K> {
138136
}
139137
}
140138

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

@@ -202,6 +206,11 @@ impl<K> KeychainTxOutIndex<K> {
202206
lookahead,
203207
}
204208
}
209+
210+
/// Get a reference to the internal [`SpkTxOutIndex`].
211+
pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> {
212+
&self.inner
213+
}
205214
}
206215

207216
/// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`].
@@ -881,20 +890,6 @@ pub trait SyncRequestBuilderExt<K> {
881890

882891
/// Add [`Script`](bitcoin::Script)s that are revealed by the `indexer` but currently unused.
883892
fn unused_spks_from_indexer(self, indexer: &KeychainTxOutIndex<K>) -> Self;
884-
885-
/// Add unconfirmed txids and their associated spks.
886-
///
887-
/// We expect that the chain source should include these txids in their spk histories. If not,
888-
/// the transaction has been evicted for some reason and we will inform the receiving
889-
/// structures in the response.
890-
fn check_unconfirmed_statuses<A, C>(
891-
self,
892-
indexer: &KeychainTxOutIndex<K>,
893-
canonical_iter: CanonicalIter<A, C>,
894-
) -> Self
895-
where
896-
A: Anchor,
897-
C: ChainOracle<Error = Infallible>;
898893
}
899894

900895
impl<K: Clone + Ord + core::fmt::Debug> SyncRequestBuilderExt<K> for SyncRequestBuilder<(K, u32)> {
@@ -908,29 +903,6 @@ impl<K: Clone + Ord + core::fmt::Debug> SyncRequestBuilderExt<K> for SyncRequest
908903
fn unused_spks_from_indexer(self, indexer: &KeychainTxOutIndex<K>) -> Self {
909904
self.spks_with_indexes(indexer.unused_spks())
910905
}
911-
912-
fn check_unconfirmed_statuses<A, C>(
913-
self,
914-
indexer: &KeychainTxOutIndex<K>,
915-
canonical_iter: CanonicalIter<A, C>,
916-
) -> Self
917-
where
918-
A: Anchor,
919-
C: ChainOracle<Error = Infallible>,
920-
{
921-
self.expected_txids_of_spk(
922-
canonical_iter
923-
.map(|res| res.expect("infallible"))
924-
.filter(|(_, _, reason)| matches!(reason, CanonicalReason::ObservedIn { .. }))
925-
.flat_map(|(txid, tx, _)| {
926-
indexer
927-
.inner
928-
.relevant_spks_of_tx(tx.as_ref())
929-
.into_iter()
930-
.map(move |spk| (txid, spk))
931-
}),
932-
)
933-
}
934906
}
935907

936908
/// Trait to extend [`FullScanRequestBuilder`].

crates/chain/src/indexer/spk_txout.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ pub struct SpkTxOutIndex<I> {
4242
spk_txouts: BTreeSet<(I, OutPoint)>,
4343
}
4444

45+
impl<I> AsRef<SpkTxOutIndex<I>> for SpkTxOutIndex<I> {
46+
fn as_ref(&self) -> &SpkTxOutIndex<I> {
47+
self
48+
}
49+
}
50+
4551
impl<I> Default for SpkTxOutIndex<I> {
4652
fn default() -> Self {
4753
Self {

crates/chain/src/tx_graph.rs

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ use crate::spk_txout::SpkTxOutIndex;
9595
use crate::BlockId;
9696
use crate::CanonicalIter;
9797
use crate::CanonicalReason;
98-
use crate::Indexer;
9998
use crate::ObservedIn;
10099
use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge};
101100
use alloc::collections::vec_deque::VecDeque;
@@ -1164,48 +1163,46 @@ impl<A: Anchor> TxGraph<A> {
11641163
.expect("oracle is infallible")
11651164
}
11661165

1167-
/// Returns an iterator over unconfirmed transactions and their associated script pubkeys,
1168-
/// filtered within the specified `range`.
1166+
/// Iterate over unconfirmed txids that we expect to exist in a chain source's spk history
1167+
/// response.
11691168
///
1170-
/// This function scans the transaction graph for unconfirmed transactions relevant to the
1171-
/// provided [`SpkTxOutIndex`], determining which transactions should be considered based on
1172-
/// indexed outputs.
1173-
pub fn iter_spks_with_expected_txids<'a, C, I>(
1169+
/// This is used to fill [`SyncRequestBuilder::expected_unconfirmed_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_unconfirmed_spk_txids).
1170+
///
1171+
/// The spk range can be contrained with `range`.
1172+
pub fn expected_unconfirmed_spk_txids<'a, C, I>(
11741173
&'a self,
11751174
chain: &'a C,
1176-
indexer: &'a SpkTxOutIndex<I>,
1175+
chain_tip: BlockId,
1176+
indexer: &'a impl AsRef<SpkTxOutIndex<I>>,
11771177
range: impl RangeBounds<I> + 'a,
1178-
) -> impl Iterator<Item = (Txid, ScriptBuf)> + 'a
1178+
) -> Result<impl Iterator<Item = (Txid, ScriptBuf)> + 'a, C::Error>
11791179
where
1180-
C: ChainOracle<Error = core::convert::Infallible>,
1181-
I: fmt::Debug + Clone + Ord,
1180+
C: ChainOracle,
1181+
I: fmt::Debug + Clone + Ord + 'a,
11821182
{
1183-
let chain_tip = chain.get_chain_tip().unwrap();
1184-
1185-
self.list_canonical_txs(chain, chain_tip)
1186-
.filter(|c| !c.chain_position.is_confirmed() && indexer.is_tx_relevant(&c.tx_node))
1187-
.flat_map(move |c| {
1188-
let txid = c.tx_node.txid;
1189-
let spks = c
1190-
.tx_node
1191-
.input
1183+
let mut spk_txs = vec![];
1184+
for res in self.try_list_canonical_txs(chain, chain_tip) {
1185+
let canonical_tx = res?;
1186+
if canonical_tx.chain_position.is_confirmed() {
1187+
continue;
1188+
}
1189+
let txid = canonical_tx.tx_node.txid;
1190+
let tx = canonical_tx.tx_node.tx;
1191+
let outpoints = tx.input.iter().map(|txin| txin.previous_output).chain(
1192+
tx.output
11921193
.iter()
1193-
.map(|txin| txin.previous_output)
1194-
.chain(
1195-
c.tx_node
1196-
.output
1197-
.iter()
1198-
.enumerate()
1199-
.map(move |(vout, _)| OutPoint::new(txid, vout as u32)),
1200-
)
1201-
.flat_map(|op| match indexer.txout(op) {
1202-
Some((i, txo)) if range.contains(i) => Some(txo.script_pubkey.clone()),
1203-
_ => None,
1204-
})
1205-
.collect::<Vec<_>>();
1206-
1207-
core::iter::repeat(txid).zip(spks)
1208-
})
1194+
.enumerate()
1195+
.map(|(vout, _)| OutPoint::new(txid, vout as u32)),
1196+
);
1197+
for op in outpoints {
1198+
if let Some((index, txo)) = indexer.as_ref().txout(op) {
1199+
if range.contains(index) {
1200+
spk_txs.push((txid, txo.script_pubkey.clone()));
1201+
}
1202+
}
1203+
}
1204+
}
1205+
Ok(spk_txs.into_iter())
12091206
}
12101207
}
12111208

crates/core/src/spk_client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ impl<I> SyncRequestBuilder<I> {
170170
/// Add transactions that are expected to exist under a given spk.
171171
///
172172
/// This is useful for detecting a malicious replacement of an incoming transaction.
173-
pub fn expected_txids_of_spk(
173+
pub fn expected_unconfirmed_spk_txids(
174174
mut self,
175175
txs: impl IntoIterator<Item = (Txid, ScriptBuf)>,
176176
) -> Self {

crates/electrum/tests/test_electrum.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,11 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
135135
let sync_request = SyncRequest::builder()
136136
.chain_tip(chain.tip())
137137
.revealed_spks_from_indexer(&graph.index, ..)
138-
.check_unconfirmed_statuses(
139-
&graph.index,
140-
graph.graph().canonical_iter(&chain, chain.tip().block_id()),
141-
);
138+
.expected_unconfirmed_spk_txids(graph.expected_unconfirmed_spk_txids(
139+
&chain,
140+
chain.tip().block_id(),
141+
..,
142+
)?);
142143
let sync_response = client.sync(sync_request, BATCH_SIZE, true)?;
143144
assert!(
144145
sync_response
@@ -163,10 +164,11 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
163164
let sync_request = SyncRequest::builder()
164165
.chain_tip(chain.tip())
165166
.revealed_spks_from_indexer(&graph.index, ..)
166-
.check_unconfirmed_statuses(
167-
&graph.index,
168-
graph.graph().canonical_iter(&chain, chain.tip().block_id()),
169-
);
167+
.expected_unconfirmed_spk_txids(graph.expected_unconfirmed_spk_txids(
168+
&chain,
169+
chain.tip().block_id(),
170+
..,
171+
)?);
170172
let sync_response = client.sync(sync_request, BATCH_SIZE, true)?;
171173
assert!(
172174
sync_response.tx_update.evicted.contains(&send_txid),

crates/esplora/tests/async_ext.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,11 @@ pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> {
9494
let sync_request = SyncRequest::builder()
9595
.chain_tip(chain.tip())
9696
.revealed_spks_from_indexer(&graph.index, ..)
97-
.check_unconfirmed_statuses(
98-
&graph.index,
99-
graph.graph().canonical_iter(&chain, chain.tip().block_id()),
100-
);
97+
.expected_unconfirmed_spk_txids(graph.expected_unconfirmed_spk_txids(
98+
&chain,
99+
chain.tip().block_id(),
100+
..,
101+
)?);
101102
let sync_response = client.sync(sync_request, 1).await?;
102103
assert!(
103104
sync_response
@@ -122,10 +123,11 @@ pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> {
122123
let sync_request = SyncRequest::builder()
123124
.chain_tip(chain.tip())
124125
.revealed_spks_from_indexer(&graph.index, ..)
125-
.check_unconfirmed_statuses(
126-
&graph.index,
127-
graph.graph().canonical_iter(&chain, chain.tip().block_id()),
128-
);
126+
.expected_unconfirmed_spk_txids(graph.expected_unconfirmed_spk_txids(
127+
&chain,
128+
chain.tip().block_id(),
129+
..,
130+
)?);
129131
let sync_response = client.sync(sync_request, 1).await?;
130132
assert!(
131133
sync_response.tx_update.evicted.contains(&send_txid),

crates/esplora/tests/blocking_ext.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,11 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
9494
let sync_request = SyncRequest::builder()
9595
.chain_tip(chain.tip())
9696
.revealed_spks_from_indexer(&graph.index, ..)
97-
.check_unconfirmed_statuses(
98-
&graph.index,
99-
graph.graph().canonical_iter(&chain, chain.tip().block_id()),
100-
);
97+
.expected_unconfirmed_spk_txids(graph.expected_unconfirmed_spk_txids(
98+
&chain,
99+
chain.tip().block_id(),
100+
..,
101+
)?);
101102
let sync_response = client.sync(sync_request, 1)?;
102103
assert!(
103104
sync_response
@@ -122,10 +123,11 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
122123
let sync_request = SyncRequest::builder()
123124
.chain_tip(chain.tip())
124125
.revealed_spks_from_indexer(&graph.index, ..)
125-
.check_unconfirmed_statuses(
126-
&graph.index,
127-
graph.graph().canonical_iter(&chain, chain.tip().block_id()),
128-
);
126+
.expected_unconfirmed_spk_txids(graph.expected_unconfirmed_spk_txids(
127+
&chain,
128+
chain.tip().block_id(),
129+
..,
130+
)?);
129131
let sync_response = client.sync(sync_request, 1)?;
130132
assert!(
131133
sync_response.tx_update.evicted.contains(&send_txid),

0 commit comments

Comments
 (0)