Skip to content

Commit 6bb02af

Browse files
committed
feat!: [WIP] More IntentTracker tweaks
1 parent 55474a6 commit 6bb02af

File tree

2 files changed

+292
-163
lines changed

2 files changed

+292
-163
lines changed

wallet/src/wallet/intent_tracker.rs

Lines changed: 229 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
//! Unbroadcasted transaction queue.
22
3+
use core::convert::Infallible;
4+
35
use alloc::sync::Arc;
46

57
use alloc::vec::Vec;
68
use bitcoin::OutPoint;
79
use bitcoin::Transaction;
810
use chain::tx_graph;
11+
use chain::tx_graph::TxNode;
912
use chain::Anchor;
13+
use chain::BlockId;
1014
use chain::CanonicalIter;
1115
use chain::CanonicalReason;
1216
use chain::ChainOracle;
1317
use chain::ChainPosition;
18+
use chain::ObservedIn;
1419
use chain::TxGraph;
1520

1621
use crate::collections::BTreeMap;
@@ -21,10 +26,11 @@ use crate::collections::VecDeque;
2126
use bdk_chain::bdk_core::Merge;
2227
use bitcoin::Txid;
2328

29+
/// A consistent view of transactions.
2430
#[derive(Debug)]
2531
pub struct CanonicalView<A> {
26-
pub txs: HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>,
27-
pub spends: HashMap<OutPoint, Txid>,
32+
pub(crate) txs: HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>,
33+
pub(crate) spends: HashMap<OutPoint, Txid>,
2834
}
2935

3036
impl<A> Default for CanonicalView<A> {
@@ -37,7 +43,7 @@ impl<A> Default for CanonicalView<A> {
3743
}
3844

3945
impl<A> CanonicalView<A> {
40-
pub fn from_iter<C>(iter: CanonicalIter<'_, A, C>) -> Result<Self, C::Error>
46+
pub(crate) fn from_iter<C>(iter: CanonicalIter<'_, A, C>) -> Result<Self, C::Error>
4147
where
4248
A: Anchor,
4349
C: ChainOracle,
@@ -53,11 +59,49 @@ impl<A> CanonicalView<A> {
5359
Ok(view)
5460
}
5561

62+
/// Return the transaction that spends the given `op`.
5663
pub fn spend(&self, op: OutPoint) -> Option<(Txid, Arc<Transaction>, &CanonicalReason<A>)> {
5764
let txid = self.spends.get(&op)?;
5865
let (tx, reason) = self.txs.get(txid)?;
5966
Some((*txid, tx.clone(), reason))
6067
}
68+
69+
/// Iterate all descendants of the given transaction in the [`CanonicalView`], avoiding
70+
/// duplicates.
71+
fn descendants(
72+
&self,
73+
tx: impl AsRef<Transaction>,
74+
) -> impl Iterator<Item = (Txid, Arc<Transaction>, &CanonicalReason<A>)> {
75+
let tx: &Transaction = tx.as_ref();
76+
let txid = tx.compute_txid();
77+
78+
let mut visited = HashSet::<Txid>::new();
79+
visited.insert(txid);
80+
81+
let mut outpoints = core::iter::repeat_n(txid, tx.output.len())
82+
.zip(0_u32..)
83+
.map(|(txid, vout)| OutPoint::new(txid, vout))
84+
.collect::<Vec<_>>();
85+
86+
core::iter::from_fn(move || {
87+
while let Some(op) = outpoints.pop() {
88+
let (txid, tx, reason) = match self.spend(op) {
89+
Some(spent_by) => spent_by,
90+
None => continue,
91+
};
92+
if !visited.insert(txid) {
93+
continue;
94+
}
95+
outpoints.extend(
96+
core::iter::repeat_n(txid, tx.output.len())
97+
.zip(0_u32..)
98+
.map(|(txid, vout)| OutPoint::new(txid, vout)),
99+
);
100+
return Some((txid, tx, reason));
101+
}
102+
None
103+
})
104+
}
61105
}
62106

63107
/// Indicates whether a transaction was observed in the network.
@@ -83,8 +127,8 @@ impl NetworkSeen {
83127
///
84128
/// This struct models an input that attempts to spend an output via a transaction path
85129
/// that is not part of the canonical network view (e.g., evicted, conflicted, or unknown).
86-
#[derive(Debug, Clone, Default)]
87-
pub struct UncanonicalSpendInfo<A> {
130+
#[derive(Debug, Clone)]
131+
pub struct SpendInfo<A> {
88132
/// Non-canonical ancestor transactions reachable from this input.
89133
///
90134
/// Each entry maps an ancestor `Txid` to its observed status in the network.
@@ -95,12 +139,177 @@ pub struct UncanonicalSpendInfo<A> {
95139

96140
/// Canonical transactions that conflict with this spend.
97141
///
98-
/// This may be a direct conflict or a conflict with one of the `uncanonical_ancestors`.
99-
/// The value is a tuple of (conflict distance, chain position).
142+
/// This may be a direct conflict, a conflict with one of the [`uncanonical_ancestors`], or a
143+
/// canonical descendant of a conflict (which are also conflicts). The value is the chain
144+
/// position of the conflict.
100145
///
101-
/// Descendants of conflicts are also conflicts. These transactions will have the same distance
102-
/// value as their conflicting parent.
103-
pub conflicting_txs: BTreeMap<Txid, (u32, ChainPosition<A>)>,
146+
/// [`uncanonical_ancestors`]: Self::uncanonical_ancestors
147+
pub conflicting_txs: BTreeMap<Txid, ChainPosition<A>>,
148+
}
149+
150+
impl<A> Default for SpendInfo<A> {
151+
fn default() -> Self {
152+
Self {
153+
uncanonical_ancestors: BTreeMap::new(),
154+
conflicting_txs: BTreeMap::new(),
155+
}
156+
}
157+
}
158+
159+
impl<A: Anchor> SpendInfo<A> {
160+
pub(crate) fn new<C>(
161+
chain: &C,
162+
chain_tip: BlockId,
163+
tx_graph: &TxGraph<A>,
164+
network_view: &CanonicalView<A>,
165+
op: OutPoint,
166+
) -> Self
167+
where
168+
C: ChainOracle<Error = Infallible>,
169+
{
170+
use crate::collections::btree_map::Entry;
171+
172+
let mut spend_info = Self::default();
173+
174+
let mut visited = HashSet::<OutPoint>::new();
175+
let mut stack = Vec::<OutPoint>::new();
176+
stack.push(op);
177+
178+
while let Some(prev_op) = stack.pop() {
179+
if !visited.insert(prev_op) {
180+
// Outpoint already visited.
181+
continue;
182+
}
183+
if network_view.txs.contains_key(&prev_op.txid) {
184+
// Tx is already canonical.
185+
continue;
186+
}
187+
188+
let prev_tx_node = match tx_graph.get_tx_node(prev_op.txid) {
189+
Some(prev_tx) => prev_tx,
190+
// Tx not known by tx-graph.
191+
None => continue,
192+
};
193+
194+
match spend_info.uncanonical_ancestors.entry(prev_op.txid) {
195+
Entry::Vacant(entry) => entry.insert(
196+
if !prev_tx_node.anchors.is_empty() || prev_tx_node.last_seen.is_some() {
197+
NetworkSeen::Seen
198+
} else {
199+
NetworkSeen::NeverSeen
200+
},
201+
),
202+
// Tx already visited.
203+
Entry::Occupied(_) => continue,
204+
};
205+
206+
// Find conflicts to populate `conflicting_txs`.
207+
if let Some((conflict_txid, conflict_tx, reason)) = network_view.spend(prev_op) {
208+
let conflict_tx_entry = match spend_info.conflicting_txs.entry(conflict_txid) {
209+
Entry::Vacant(vacant_entry) => vacant_entry,
210+
// Skip if conflicting tx already visited.
211+
Entry::Occupied(_) => continue,
212+
};
213+
let conflict_tx_node = match tx_graph.get_tx_node(conflict_txid) {
214+
Some(tx_node) => tx_node,
215+
// Skip if conflict tx does not exist in our graph.
216+
None => continue,
217+
};
218+
conflict_tx_entry.insert(Self::get_pos(
219+
chain,
220+
chain_tip,
221+
&conflict_tx_node,
222+
reason,
223+
));
224+
225+
// Find descendants of `conflict_tx` too.
226+
for (conflict_txid, _, reason) in network_view.descendants(conflict_tx) {
227+
let conflict_tx_entry = match spend_info.conflicting_txs.entry(conflict_txid) {
228+
Entry::Vacant(vacant_entry) => vacant_entry,
229+
// Skip if conflicting tx already visited.
230+
Entry::Occupied(_) => continue,
231+
};
232+
let conflict_tx_node = match tx_graph.get_tx_node(conflict_txid) {
233+
Some(tx_node) => tx_node,
234+
// Skip if conflict tx does not exist in our graph.
235+
None => continue,
236+
};
237+
conflict_tx_entry.insert(Self::get_pos(
238+
chain,
239+
chain_tip,
240+
&conflict_tx_node,
241+
reason,
242+
));
243+
}
244+
}
245+
246+
stack.extend(
247+
prev_tx_node
248+
.tx
249+
.input
250+
.iter()
251+
.map(|txin| txin.previous_output),
252+
);
253+
}
254+
255+
spend_info
256+
}
257+
258+
fn get_pos<C>(
259+
chain: &C,
260+
chain_tip: BlockId,
261+
tx_node: &TxNode<'_, Arc<Transaction>, A>,
262+
canonical_reason: &CanonicalReason<A>,
263+
) -> ChainPosition<A>
264+
where
265+
C: ChainOracle<Error = Infallible>,
266+
{
267+
let maybe_direct_anchor = tx_node
268+
.anchors
269+
.iter()
270+
.find(|a| {
271+
chain
272+
.is_block_in_chain(a.anchor_block(), chain_tip)
273+
.expect("infallible")
274+
.unwrap_or(false)
275+
})
276+
.cloned();
277+
match maybe_direct_anchor {
278+
Some(anchor) => ChainPosition::Confirmed {
279+
anchor,
280+
transitively: None,
281+
},
282+
None => match canonical_reason.clone() {
283+
CanonicalReason::Assumed { .. } => {
284+
debug_assert!(
285+
false,
286+
"network view must not have any assumed-canonical txs"
287+
);
288+
ChainPosition::Unconfirmed {
289+
first_seen: None,
290+
last_seen: None,
291+
}
292+
}
293+
CanonicalReason::Anchor { anchor, descendant } => ChainPosition::Confirmed {
294+
anchor,
295+
transitively: descendant,
296+
},
297+
CanonicalReason::ObservedIn { observed_in, .. } => ChainPosition::Unconfirmed {
298+
first_seen: tx_node.first_seen,
299+
last_seen: match observed_in {
300+
ObservedIn::Block(_) => None,
301+
ObservedIn::Mempool(last_seen) => Some(last_seen),
302+
},
303+
},
304+
},
305+
}
306+
}
307+
308+
/// If the spend info is empty, then it can belong in the canonical history without displacing
309+
/// existing transactions or need to add additional transactions other than itself.
310+
pub fn is_empty(&self) -> bool {
311+
self.uncanonical_ancestors.is_empty() && self.conflicting_txs.is_empty()
312+
}
104313
}
105314

106315
/// Tracked and uncanonical transaction.
@@ -113,7 +322,7 @@ pub struct UncanonicalTx<A> {
113322
/// Whether the transaction was one seen by the network.
114323
pub network_seen: NetworkSeen,
115324
/// Spends, identified by prevout, which are uncanonical.
116-
pub uncanonical_spends: BTreeMap<OutPoint, UncanonicalSpendInfo<A>>,
325+
pub uncanonical_spends: BTreeMap<OutPoint, SpendInfo<A>>,
117326
}
118327

119328
impl<A: Anchor> UncanonicalTx<A> {
@@ -154,20 +363,22 @@ impl<A: Anchor> UncanonicalTx<A> {
154363
self.uncanonical_spends
155364
.values()
156365
.flat_map(|spend| &spend.conflicting_txs)
157-
.map(|(&txid, (_, pos))| (txid, pos))
366+
.map(|(&txid, pos)| (txid, pos))
158367
.filter({
159368
let mut dedup = HashSet::<Txid>::new();
160369
move |(txid, _)| dedup.insert(*txid)
161370
})
162371
}
163372

373+
/// Iterate over confirmed, network-canonical txids which conflict with this transaction.
164374
pub fn confirmed_conflicts(&self) -> impl Iterator<Item = (Txid, &A)> {
165375
self.conflicts().filter_map(|(txid, pos)| match pos {
166376
ChainPosition::Confirmed { anchor, .. } => Some((txid, anchor)),
167377
ChainPosition::Unconfirmed { .. } => None,
168378
})
169379
}
170380

381+
/// Iterate over unconfirmed, network-canonical txids which conflict with this transaction.
171382
pub fn unconfirmed_conflicts(&self) -> impl Iterator<Item = Txid> + '_ {
172383
self.conflicts().filter_map(|(txid, pos)| match pos {
173384
ChainPosition::Confirmed { .. } => None,
@@ -185,20 +396,20 @@ impl<A: Anchor> UncanonicalTx<A> {
185396
.map(|(&txid, &network_seen)| (txid, network_seen))
186397
}
187398

399+
/// Whether this transaction conflicts with network-canonical transactions.
188400
pub fn contains_conflicts(&self) -> bool {
189401
self.conflicts().next().is_some()
190402
}
191403

404+
/// Whether this transaction conflicts with confirmed, network-canonical transactions.
192405
pub fn contains_confirmed_conflicts(&self) -> bool {
193406
self.confirmed_conflicts().next().is_some()
194407
}
195408
}
196409

197-
/// An ordered unbroadcasted staging area.
198-
///
199-
/// It is ordered in case of RBF txs.
410+
/// An ordered tracking area for uncanonical transactions.
200411
#[derive(Debug, Clone, Default)]
201-
pub struct CanonicalizationTracker {
412+
pub struct IntentTracker {
202413
/// Tracks the order that transactions are added.
203414
order: VecDeque<Txid>,
204415

@@ -233,10 +444,10 @@ impl Merge for ChangeSet {
233444
}
234445
}
235446

236-
impl CanonicalizationTracker {
447+
impl IntentTracker {
237448
/// Construct [`Unbroadcasted`] from the given `changeset`.
238449
pub fn from_changeset(changeset: ChangeSet) -> Self {
239-
let mut out = CanonicalizationTracker::default();
450+
let mut out = IntentTracker::default();
240451
out.apply_changeset(changeset);
241452
out
242453
}

0 commit comments

Comments
 (0)