Skip to content

Commit 0151699

Browse files
committed
WIP
1 parent f348e6d commit 0151699

File tree

13 files changed

+653
-331
lines changed

13 files changed

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

0 commit comments

Comments
 (0)