Skip to content

Commit 1c2fabc

Browse files
committed
feat(chain)!: Better canonicalization algorithm
This is an O(n) algorithm to determine the canonical set of txids.
1 parent 2fc554b commit 1c2fabc

File tree

13 files changed

+560
-172
lines changed

13 files changed

+560
-172
lines changed

crates/bitcoind_rpc/tests/test_emitter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
389389
assert_eq!(
390390
get_balance(&recv_chain, &recv_graph)?,
391391
Balance {
392+
trusted_pending: SEND_AMOUNT * reorg_count as u64,
392393
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
393394
..Balance::default()
394395
},

crates/chain/src/canonical_iter.rs

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
use core::cmp::Reverse;
2+
3+
use crate::collections::{btree_set, hash_map, BTreeSet, HashMap, HashSet, VecDeque};
4+
use crate::tx_graph::{CanonicalTx, TxAncestors, TxDescendants, TxNode};
5+
use crate::{Anchor, ChainOracle, ChainPosition, TxGraph};
6+
use alloc::sync::Arc;
7+
use alloc::vec::Vec;
8+
use bdk_core::BlockId;
9+
use bitcoin::{Transaction, Txid};
10+
11+
/// A set of canonical transactions.
12+
pub type CanonicalSet<A> = HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>;
13+
14+
type ToProcess = btree_set::IntoIter<Reverse<(LastSeen, Txid)>>;
15+
16+
/// Iterates over canonical txs.
17+
pub struct CanonicalIter<'g, A, C> {
18+
tx_graph: &'g TxGraph<A>,
19+
chain: &'g C,
20+
chain_tip: BlockId,
21+
22+
to_process: ToProcess,
23+
canonical: CanonicalSet<A>,
24+
not_canonical: HashSet<Txid>,
25+
queue: VecDeque<Txid>,
26+
}
27+
28+
impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
29+
/// Constructs [`CanonicalIter`].
30+
pub fn new(tx_graph: &'g TxGraph<A>, chain: &'g C, chain_tip: BlockId) -> Self {
31+
let to_process = tx_graph
32+
.full_txs()
33+
.filter_map(|tx_node| {
34+
Some(Reverse((
35+
tx_graph.last_seen_in(tx_node.txid)?,
36+
tx_node.txid,
37+
)))
38+
})
39+
.collect::<BTreeSet<_>>()
40+
.into_iter();
41+
Self {
42+
tx_graph,
43+
chain,
44+
chain_tip,
45+
to_process,
46+
canonical: HashMap::default(),
47+
not_canonical: HashSet::default(),
48+
queue: VecDeque::default(),
49+
}
50+
}
51+
52+
fn canonicalize_by_traversing_backwards(
53+
&mut self,
54+
txid: Txid,
55+
last_seen: LastSeen,
56+
) -> Result<(), C::Error> {
57+
type TxWithId = (Txid, Arc<Transaction>);
58+
let tx = match self.tx_graph.get_tx(txid) {
59+
Some(tx) => tx,
60+
None => return Ok(()),
61+
};
62+
let maybe_canonical = TxAncestors::new_include_root(
63+
self.tx_graph,
64+
tx,
65+
|_: usize, tx: Arc<Transaction>| -> Option<Result<TxWithId, C::Error>> {
66+
let txid = tx.compute_txid();
67+
if self.canonical.contains_key(&txid) || self.not_canonical.contains(&txid) {
68+
return None;
69+
}
70+
if let Some(anchors) = self.tx_graph.all_anchors().get(&txid) {
71+
for anchor in anchors {
72+
let anchor_block = anchor.anchor_block();
73+
match self.chain.is_block_in_chain(anchor_block, self.chain_tip) {
74+
Ok(None) | Ok(Some(false)) => continue,
75+
Ok(Some(true)) => {
76+
let reason = CanonicalReason::from_anchor(anchor.clone());
77+
self.mark_canonical(tx, reason);
78+
return None;
79+
}
80+
Err(err) => return Some(Err(err)),
81+
};
82+
}
83+
}
84+
// Coinbase transactions cannot exist in mempool.
85+
if tx.is_coinbase() {
86+
return None;
87+
}
88+
for (_, conflicting_txid) in self.tx_graph.direct_conflicts(&tx) {
89+
if self.canonical.contains_key(&conflicting_txid) {
90+
self.mark_not_canonical(txid);
91+
return None;
92+
}
93+
}
94+
Some(Ok((txid, tx)))
95+
},
96+
)
97+
.collect::<Result<Vec<_>, C::Error>>()?;
98+
99+
// This assumes that `last_seen` values are fully transitive. I.e. if A is an ancestor of B,
100+
// then the most recent timestamp between A & B also applies to A.
101+
let starting_txid = txid;
102+
for (txid, tx) in maybe_canonical {
103+
if self.not_canonical.contains(&txid) {
104+
continue;
105+
}
106+
self.mark_canonical(
107+
tx,
108+
if txid == starting_txid {
109+
CanonicalReason::<A>::from_last_seen(last_seen)
110+
} else {
111+
CanonicalReason::<A>::from_descendant_last_seen(starting_txid, last_seen)
112+
},
113+
);
114+
}
115+
Ok(())
116+
}
117+
118+
fn mark_not_canonical(&mut self, txid: Txid) {
119+
TxDescendants::new_include_root(self.tx_graph, txid, |_: usize, txid: Txid| -> Option<()> {
120+
if self.not_canonical.insert(txid) {
121+
Some(())
122+
} else {
123+
None
124+
}
125+
})
126+
.for_each(|_| {})
127+
}
128+
129+
fn mark_canonical(&mut self, tx: Arc<Transaction>, reason: CanonicalReason<A>) {
130+
let starting_txid = tx.compute_txid();
131+
let mut is_root = true;
132+
TxAncestors::new_include_root(
133+
self.tx_graph,
134+
tx,
135+
|_: usize, tx: Arc<Transaction>| -> Option<()> {
136+
let this_txid = tx.compute_txid();
137+
let this_reason = if is_root {
138+
is_root = false;
139+
reason.clone()
140+
} else {
141+
reason.clone().with_descendant(starting_txid)
142+
};
143+
match self.canonical.entry(this_txid) {
144+
hash_map::Entry::Occupied(_) => None,
145+
hash_map::Entry::Vacant(entry) => {
146+
entry.insert((tx, this_reason));
147+
self.queue.push_back(this_txid);
148+
Some(())
149+
}
150+
}
151+
},
152+
)
153+
.for_each(|_| {})
154+
}
155+
}
156+
157+
impl<'g, A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'g, A, C> {
158+
type Item = Result<(Txid, Arc<Transaction>, CanonicalReason<A>), C::Error>;
159+
160+
fn next(&mut self) -> Option<Self::Item> {
161+
loop {
162+
if let Some(txid) = self.queue.pop_front() {
163+
let (tx, reason) = self
164+
.canonical
165+
.get(&txid)
166+
.cloned()
167+
.expect("reason must exist");
168+
return Some(Ok((txid, tx, reason)));
169+
}
170+
171+
let Reverse((last_seen, txid)) = self.to_process.next()?;
172+
if let Err(err) = self.canonicalize_by_traversing_backwards(txid, last_seen) {
173+
return Some(Err(err));
174+
}
175+
}
176+
}
177+
}
178+
179+
/// Represents when and where a given transaction is last seen.
180+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
181+
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
182+
pub enum LastSeen {
183+
/// The transaction was last seen in the mempool at the given unix timestamp.
184+
Mempool(u64),
185+
/// The transaction was last seen in a block of height.
186+
Block(u32),
187+
}
188+
189+
/// The reason why a transaction is canonical.
190+
#[derive(Debug, Clone, PartialEq, Eq)]
191+
pub enum CanonicalReason<A> {
192+
/// This transaction is anchored in the best chain by `A`, and therefore canonical.
193+
Anchor {
194+
/// The anchor that anchored the transaction in the chain.
195+
anchor: A,
196+
/// Whether the anchor is of the transaction's descendant.
197+
descendant: Option<Txid>,
198+
},
199+
/// This transaction does not conflict with any other transaction with a more recent `last_seen`
200+
/// value or one that is anchored in the best chain.
201+
LastSeen {
202+
/// The [`LastSeen`] value of the transaction.
203+
last_seen: LastSeen,
204+
/// Whether the [`LastSeen`] value is of the transaction's descendant.
205+
descendant: Option<Txid>,
206+
},
207+
}
208+
209+
impl<A> CanonicalReason<A> {
210+
/// Constructs a [`CanonicalReason`] from an `anchor`.
211+
pub fn from_anchor(anchor: A) -> Self {
212+
Self::Anchor {
213+
anchor,
214+
descendant: None,
215+
}
216+
}
217+
218+
/// Constructs a [`CanonicalReason`] from a `descendant`'s `anchor`.
219+
pub fn from_descendant_anchor(descendant: Txid, anchor: A) -> Self {
220+
Self::Anchor {
221+
anchor,
222+
descendant: Some(descendant),
223+
}
224+
}
225+
226+
/// Constructs a [`CanonicalReason`] from a `last_seen` value.
227+
pub fn from_last_seen(last_seen: LastSeen) -> Self {
228+
Self::LastSeen {
229+
last_seen,
230+
descendant: None,
231+
}
232+
}
233+
234+
/// Constructs a [`CanonicalReason`] from a `descendant`'s `last_seen` value.
235+
pub fn from_descendant_last_seen(descendant: Txid, last_seen: LastSeen) -> Self {
236+
Self::LastSeen {
237+
last_seen,
238+
descendant: Some(descendant),
239+
}
240+
}
241+
242+
/// Adds a `descendant` to the [`CanonicalReason`].
243+
///
244+
/// This signals that either the [`LastSeen`] or [`Anchor`] value belongs to the transaction's
245+
/// descendant.
246+
#[must_use]
247+
pub fn with_descendant(self, descendant: Txid) -> Self {
248+
match self {
249+
CanonicalReason::Anchor { anchor, .. } => Self::Anchor {
250+
anchor,
251+
descendant: Some(descendant),
252+
},
253+
CanonicalReason::LastSeen { last_seen, .. } => Self::LastSeen {
254+
last_seen,
255+
descendant: Some(descendant),
256+
},
257+
}
258+
}
259+
260+
/// This signals that either the [`LastSeen`] or [`Anchor`] value belongs to the transaction's
261+
/// descendant.
262+
pub fn descendant(&self) -> &Option<Txid> {
263+
match self {
264+
CanonicalReason::Anchor { descendant, .. } => descendant,
265+
CanonicalReason::LastSeen { descendant, .. } => descendant,
266+
}
267+
}
268+
}
269+
270+
/// Helper to create canonical tx.
271+
pub fn make_canonical_tx<'a, A: Anchor, C: ChainOracle>(
272+
chain: &C,
273+
chain_tip: BlockId,
274+
tx_node: TxNode<'a, Arc<Transaction>, A>,
275+
canonical_reason: CanonicalReason<A>,
276+
) -> Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error> {
277+
let chain_position = match canonical_reason {
278+
CanonicalReason::Anchor { anchor, descendant } => match descendant {
279+
Some(desc_txid) => {
280+
let direct_anchor = tx_node
281+
.anchors
282+
.iter()
283+
.find_map(|a| -> Option<Result<A, C::Error>> {
284+
match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
285+
Ok(Some(true)) => Some(Ok(a.clone())),
286+
Ok(Some(false)) | Ok(None) => None,
287+
Err(err) => Some(Err(err)),
288+
}
289+
})
290+
.transpose()?;
291+
match direct_anchor {
292+
Some(anchor) => ChainPosition::Confirmed(anchor),
293+
None => ChainPosition::ConfirmedByTransitivity(desc_txid, anchor),
294+
}
295+
}
296+
None => ChainPosition::Confirmed(anchor),
297+
},
298+
CanonicalReason::LastSeen { last_seen, .. } => match last_seen {
299+
LastSeen::Mempool(last_seen) => ChainPosition::Unconfirmed(last_seen),
300+
LastSeen::Block(_) => ChainPosition::UnconfirmedAndNotSeen,
301+
},
302+
};
303+
Ok(CanonicalTx {
304+
chain_position,
305+
tx_node,
306+
})
307+
}

crates/chain/src/chain_data.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,24 @@ use crate::{Anchor, COINBASE_MATURITY};
1515
))
1616
)]
1717
pub enum ChainPosition<A> {
18-
/// The chain data is seen as confirmed, and in anchored by `A`.
18+
/// The chain data is confirmed because it is anchored by `A`.
1919
Confirmed(A),
20-
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
20+
/// The chain data is confirmed because it has a descendant that is anchored by `A`.
21+
ConfirmedByTransitivity(Txid, A),
22+
/// The chain data is not confirmed and is last seen in the mempool (or has a descendant that
23+
/// is last seen in the mempool) at this timestamp.
2124
Unconfirmed(u64),
25+
/// The chain data is not confirmed and we have never seen it in the mempool.
26+
UnconfirmedAndNotSeen,
2227
}
2328

2429
impl<A> ChainPosition<A> {
2530
/// Returns whether [`ChainPosition`] is confirmed or not.
2631
pub fn is_confirmed(&self) -> bool {
27-
matches!(self, Self::Confirmed(_))
32+
matches!(
33+
self,
34+
Self::Confirmed(_) | Self::ConfirmedByTransitivity(_, _)
35+
)
2836
}
2937
}
3038

@@ -33,7 +41,11 @@ impl<A: Clone> ChainPosition<&A> {
3341
pub fn cloned(self) -> ChainPosition<A> {
3442
match self {
3543
ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
44+
ChainPosition::ConfirmedByTransitivity(txid, a) => {
45+
ChainPosition::ConfirmedByTransitivity(txid, a.clone())
46+
}
3647
ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
48+
ChainPosition::UnconfirmedAndNotSeen => ChainPosition::UnconfirmedAndNotSeen,
3749
}
3850
}
3951
}
@@ -42,8 +54,10 @@ impl<A: Anchor> ChainPosition<A> {
4254
/// Determines the upper bound of the confirmation height.
4355
pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
4456
match self {
45-
ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
46-
ChainPosition::Unconfirmed(_) => None,
57+
ChainPosition::Confirmed(a) | ChainPosition::ConfirmedByTransitivity(_, a) => {
58+
Some(a.confirmation_height_upper_bound())
59+
}
60+
ChainPosition::Unconfirmed(_) | ChainPosition::UnconfirmedAndNotSeen => None,
4761
}
4862
}
4963
}
@@ -73,9 +87,10 @@ impl<A: Anchor> FullTxOut<A> {
7387
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
7488
pub fn is_mature(&self, tip: u32) -> bool {
7589
if self.is_on_coinbase {
76-
let tx_height = match &self.chain_position {
77-
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
78-
ChainPosition::Unconfirmed(_) => {
90+
let tx_height = self.chain_position.confirmation_height_upper_bound();
91+
let tx_height = match tx_height {
92+
Some(tx_height) => tx_height,
93+
None => {
7994
debug_assert!(false, "coinbase tx can never be unconfirmed");
8095
return false;
8196
}
@@ -103,9 +118,9 @@ impl<A: Anchor> FullTxOut<A> {
103118
return false;
104119
}
105120

106-
let confirmation_height = match &self.chain_position {
107-
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
108-
ChainPosition::Unconfirmed(_) => return false,
121+
let confirmation_height = match self.chain_position.confirmation_height_upper_bound() {
122+
Some(h) => h,
123+
None => return false,
109124
};
110125
if confirmation_height > tip {
111126
return false;

0 commit comments

Comments
 (0)