Skip to content

Commit b4aea5d

Browse files
committed
WIP
1 parent f348e6d commit b4aea5d

File tree

10 files changed

+484
-233
lines changed

10 files changed

+484
-233
lines changed

crates/chain/src/canonical_iter.rs

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
use core::cmp::Reverse;
2+
3+
use crate::collections::{btree_set, hash_map, BTreeSet, HashMap, HashSet, VecDeque};
4+
use crate::tx_graph::{TxAncestors, TxDescendants};
5+
use crate::{Anchor, ChainOracle, 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, 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, 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(), this_reason) {
152+
Some(())
153+
} else {
154+
None
155+
}
156+
},
157+
)
158+
.for_each(|_| {})
159+
}
160+
161+
fn insert_canonical(&mut self, txid: Txid, reason: CanonicalReason<A>) -> bool {
162+
match self.canonical.entry(txid) {
163+
hash_map::Entry::Occupied(_) => false,
164+
hash_map::Entry::Vacant(entry) => {
165+
entry.insert(reason);
166+
self.queue.push_back(txid);
167+
true
168+
}
169+
}
170+
}
171+
}
172+
173+
impl<'g, A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'g, A, C> {
174+
type Item = Result<(Txid, CanonicalReason<A>), C::Error>;
175+
176+
fn next(&mut self) -> Option<Self::Item> {
177+
loop {
178+
if let Some(txid) = self.queue.pop_front() {
179+
let reason = self
180+
.canonical
181+
.get(&txid)
182+
.cloned()
183+
.expect("reason must exist");
184+
return Some(Ok((txid, reason)));
185+
}
186+
187+
let Reverse((last_seen, txid)) = self.to_process.next()?;
188+
if let Err(err) = self.canonicalize_by_traversing_backwards(txid, Some(last_seen)) {
189+
return Some(Err(err));
190+
}
191+
}
192+
}
193+
}
194+
195+
/// Represents when and where a given transaction is last seen.
196+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
197+
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
198+
pub enum LastSeen {
199+
/// The transaction was last seen in the mempool at the given unix timestamp.
200+
Mempool(u64),
201+
/// The transaction was last seen in a block of height.
202+
Block(u32),
203+
}
204+
205+
/// The reason why a transaction is canonical.
206+
#[derive(Debug, Clone, PartialEq, Eq)]
207+
pub enum CanonicalReason<A> {
208+
/// This transaction is anchored in the best chain by `A`, and therefore canonical.
209+
Anchor {
210+
/// The anchor that anchored the transaction in the chain.
211+
anchor: A,
212+
/// Whether the anchor is of the transaction's descendant.
213+
descendant: Option<Txid>,
214+
},
215+
/// This transaction does not conflict with any other transaction with a more recent `last_seen`
216+
/// value or one that is anchored in the best chain.
217+
LastSeen {
218+
/// The [`LastSeen`] value of the transaction.
219+
last_seen: LastSeen,
220+
/// Whether the [`LastSeen`] value is of the transaction's descendant.
221+
descendant: Option<Txid>,
222+
},
223+
}
224+
225+
impl<A> CanonicalReason<A> {
226+
/// Constructs a [`CanonicalReason`] from an `anchor`.
227+
pub fn from_anchor(anchor: A) -> Self {
228+
Self::Anchor {
229+
anchor,
230+
descendant: None,
231+
}
232+
}
233+
234+
/// Constructs a [`CanonicalReason`] from a `descendant`'s `anchor`.
235+
pub fn from_descendant_anchor(descendant: Txid, anchor: A) -> Self {
236+
Self::Anchor {
237+
anchor,
238+
descendant: Some(descendant),
239+
}
240+
}
241+
242+
/// Constructs a [`CanonicalReason`] from a `last_seen` value.
243+
pub fn from_last_seen(last_seen: LastSeen) -> Self {
244+
Self::LastSeen {
245+
last_seen,
246+
descendant: None,
247+
}
248+
}
249+
250+
/// Constructs a [`CanonicalReason`] from a `descendant`'s `last_seen` value.
251+
pub fn from_descendant_last_seen(descendant: Txid, last_seen: LastSeen) -> Self {
252+
Self::LastSeen {
253+
last_seen,
254+
descendant: Some(descendant),
255+
}
256+
}
257+
258+
/// Adds a `descendant` to the [`CanonicalReason`].
259+
///
260+
/// This signals that either the [`LastSeen`] or [`Anchor`] value belongs to the transaction's
261+
/// descendant.
262+
#[must_use]
263+
pub fn with_descendant(self, descendant: Txid) -> Self {
264+
match self {
265+
CanonicalReason::Anchor { anchor, .. } => Self::Anchor {
266+
anchor,
267+
descendant: Some(descendant),
268+
},
269+
CanonicalReason::LastSeen { last_seen, .. } => Self::LastSeen {
270+
last_seen,
271+
descendant: Some(descendant),
272+
},
273+
}
274+
}
275+
276+
/// This signals that either the [`LastSeen`] or [`Anchor`] value belongs to the transaction's
277+
/// descendant.
278+
pub fn descendant(&self) -> &Option<Txid> {
279+
match self {
280+
CanonicalReason::Anchor { descendant, .. } => descendant,
281+
CanonicalReason::LastSeen { descendant, .. } => descendant,
282+
}
283+
}
284+
}

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;

crates/chain/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub mod tx_graph;
4343
pub use tx_graph::TxGraph;
4444
mod chain_oracle;
4545
pub use chain_oracle::*;
46+
mod canonical_iter;
47+
pub use canonical_iter::*;
4648

4749
#[doc(hidden)]
4850
pub mod example_utils;

0 commit comments

Comments
 (0)