Skip to content

Commit 6f8ef26

Browse files
committed
fix(chain): position resolution for assumed txs
- add new `test_canonical_view_task.rs` to handle different scenarios of chain position resolution. - fixes the assumed canonical txs chain position resolution, especially for transitively assumed canonical transactions, where there's an anchored/confirmed descendant.
1 parent 470ccd1 commit 6f8ef26

File tree

3 files changed

+250
-11
lines changed

3 files changed

+250
-11
lines changed

crates/chain/src/canonical_task.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type NotCanonicalSet = HashSet<Txid>;
1414
/// Represents the current stage of canonicalization processing.
1515
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1616
enum CanonicalStage {
17-
/// Processing transctions assumed to be canonical.
17+
/// Processing transactions assumed to be canonical.
1818
#[default]
1919
AssumedTxs,
2020
/// Processing directly anchored transactions.

crates/chain/src/canonical_view_task.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::canonical_task::{CanonicalReason, ObservedIn};
44
use crate::collections::{HashMap, VecDeque};
5+
use crate::tx_graph::TxDescendants;
56
use alloc::collections::BTreeSet;
67
use alloc::sync::Arc;
78
use alloc::vec::Vec;
@@ -152,16 +153,53 @@ impl<'g, A: Anchor> ChainQuery for CanonicalViewTask<'g, A> {
152153

153154
// Determine chain position based on reason
154155
let chain_position = match reason {
155-
CanonicalReason::Assumed { .. } => match self.direct_anchors.get(txid) {
156-
Some(anchor) => ChainPosition::Confirmed {
157-
anchor,
158-
transitively: None,
159-
},
160-
None => ChainPosition::Unconfirmed {
161-
first_seen: tx_node.first_seen,
162-
last_seen: tx_node.last_seen,
163-
},
164-
},
156+
CanonicalReason::Assumed { descendant } => {
157+
match self.direct_anchors.get(txid) {
158+
// it has a direct anchor found
159+
// regardless if it's directly or transitively assumed canonical
160+
Some(anchor) => ChainPosition::Confirmed {
161+
anchor,
162+
transitively: None,
163+
},
164+
None => match descendant {
165+
// transitively assumed canonical, walk through descendants to find
166+
// the first confirmed one.
167+
Some(_descendant) => {
168+
match TxDescendants::new_exclude_root(
169+
self.tx_graph,
170+
*txid,
171+
|_, desc_txid| -> Option<(Txid, &A)> {
172+
// assert the descendant visited is canonical
173+
self.canonical_txs
174+
.contains_key(&desc_txid)
175+
.then(|| {
176+
self.direct_anchors
177+
.get(&desc_txid)
178+
.map(|anchor| (desc_txid, anchor))
179+
})
180+
.flatten()
181+
},
182+
)
183+
.next()
184+
{
185+
Some((desc_txid, anchor)) => ChainPosition::Confirmed {
186+
anchor,
187+
transitively: Some(desc_txid),
188+
},
189+
None => ChainPosition::Unconfirmed {
190+
first_seen: tx_node.first_seen,
191+
last_seen: tx_node.last_seen,
192+
},
193+
}
194+
}
195+
// directly assumed canonical, no direct anchor found.
196+
None => ChainPosition::Unconfirmed {
197+
first_seen: tx_node.first_seen,
198+
last_seen: tx_node.last_seen,
199+
},
200+
},
201+
}
202+
}
165203
CanonicalReason::Anchor { anchor, descendant } => match descendant {
166204
Some(_) => match self.direct_anchors.get(txid) {
167205
Some(anchor) => ChainPosition::Confirmed {
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#![cfg(feature = "miniscript")]
2+
3+
mod common;
4+
5+
use bdk_chain::{BlockId, CanonicalReason, ChainPosition};
6+
use bdk_testenv::{block_id, hash, local_chain};
7+
use bitcoin::Txid;
8+
use common::*;
9+
use std::collections::HashSet;
10+
11+
struct Scenario<'a> {
12+
name: &'a str,
13+
tx_templates: &'a [TxTemplate<'a, BlockId>],
14+
exp_canonical_txs: HashSet<&'a str>,
15+
}
16+
17+
#[test]
18+
fn test_assumed_canonical_scenarios() {
19+
// Create a local chain
20+
let local_chain = local_chain![
21+
(0, hash!("genesis")),
22+
(1, hash!("block1")),
23+
(2, hash!("block2")),
24+
(3, hash!("block3")),
25+
(4, hash!("block4")),
26+
(5, hash!("block5")),
27+
(6, hash!("block6")),
28+
(7, hash!("block7")),
29+
(8, hash!("block8")),
30+
(9, hash!("block9")),
31+
(10, hash!("block10"))
32+
];
33+
let chain_tip = local_chain.tip().block_id();
34+
35+
// Create arrays before scenario to avoid lifetime issues
36+
let tx_templates = [
37+
TxTemplate {
38+
tx_name: "txA",
39+
inputs: &[TxInTemplate::Bogus],
40+
outputs: &[TxOutTemplate::new(100000, Some(0))],
41+
anchors: &[],
42+
last_seen: None,
43+
assume_canonical: false,
44+
},
45+
TxTemplate {
46+
tx_name: "txB",
47+
inputs: &[TxInTemplate::PrevTx("txA", 0)],
48+
outputs: &[TxOutTemplate::new(50000, Some(0))],
49+
anchors: &[block_id!(5, "block5")],
50+
last_seen: None,
51+
assume_canonical: false,
52+
},
53+
TxTemplate {
54+
tx_name: "txC",
55+
inputs: &[TxInTemplate::PrevTx("txB", 0)],
56+
outputs: &[TxOutTemplate::new(25000, Some(0))],
57+
anchors: &[],
58+
last_seen: None,
59+
assume_canonical: true,
60+
},
61+
];
62+
63+
let scenarios = vec![Scenario {
64+
name: "txC spends txB; txB spends txA; txB is anchored; txC is assumed canonical",
65+
tx_templates: &tx_templates,
66+
exp_canonical_txs: HashSet::from(["txA", "txB", "txC"]),
67+
}];
68+
69+
for scenario in scenarios {
70+
let env = init_graph(scenario.tx_templates);
71+
72+
// get the actual txid from given tx_name.
73+
let txid_c = *env.txid_to_name.get("txC").unwrap();
74+
75+
// build the expected `CanonicalReason` with specific descendant txid's
76+
//
77+
// in this scenario: txC is assumed canonical, and it's descendant of txB and txA
78+
// therefore the whole chain should become assumed canonical.
79+
//
80+
// the descendant txid field refers to the directly **assumed canonical txC**
81+
let exp_reasons = vec![
82+
(
83+
"txA",
84+
CanonicalReason::Assumed {
85+
descendant: Some(txid_c),
86+
},
87+
),
88+
(
89+
"txB",
90+
CanonicalReason::Assumed {
91+
descendant: Some(txid_c),
92+
},
93+
),
94+
("txC", CanonicalReason::Assumed { descendant: None }),
95+
];
96+
97+
// build task & canonicalize
98+
let canonical_params = env.canonicalization_params;
99+
let canonical_task = env.tx_graph.canonical_task(chain_tip, canonical_params);
100+
let canonical_txs = local_chain.canonicalize(canonical_task);
101+
102+
// assert canonical transactions
103+
let exp_canonical_txids: HashSet<Txid> = scenario
104+
.exp_canonical_txs
105+
.iter()
106+
.map(|tx_name| {
107+
*env.txid_to_name
108+
.get(tx_name)
109+
.expect("txid should exist for tx_name")
110+
})
111+
.collect::<HashSet<Txid>>();
112+
113+
let canonical_txids = canonical_txs
114+
.txs()
115+
.map(|canonical_tx| canonical_tx.txid)
116+
.collect::<HashSet<Txid>>();
117+
118+
assert_eq!(
119+
canonical_txids, exp_canonical_txids,
120+
"[{}] canonical transactions mismatch",
121+
scenario.name
122+
);
123+
124+
// assert canonical reasons
125+
for (tx_name, exp_reason) in exp_reasons {
126+
let txid = env
127+
.txid_to_name
128+
.get(tx_name)
129+
.expect("txid should exist for tx_name");
130+
131+
let canonical_reason = canonical_txs
132+
.txs()
133+
.find(|ctx| &ctx.txid == txid)
134+
.expect("expected txid should exist in canonical txs")
135+
.pos;
136+
137+
assert_eq!(
138+
canonical_reason, exp_reason,
139+
"[{}] canonical reason mismatch for {}",
140+
scenario.name, tx_name
141+
)
142+
}
143+
144+
let txid_b = *env.txid_to_name.get("txB").unwrap();
145+
146+
// build the expected `ChainPosition` with specific txid's for transitively confirmed txs.
147+
//
148+
// in this scenario:
149+
//
150+
// txA: should be confirmed transitively by txB.
151+
// txB: should be confirmed, has a direct anchor(block5).
152+
// txC: should be unconfirmed, has been assumed canonical though has no direct anchors.
153+
let exp_positions = vec![
154+
(
155+
"txA",
156+
ChainPosition::Confirmed {
157+
anchor: block_id!(5, "block5"),
158+
transitively: Some(txid_b),
159+
},
160+
),
161+
(
162+
"txB",
163+
ChainPosition::Confirmed {
164+
anchor: block_id!(5, "block5"),
165+
transitively: None,
166+
},
167+
),
168+
(
169+
"txC",
170+
ChainPosition::Unconfirmed {
171+
first_seen: None,
172+
last_seen: None,
173+
},
174+
),
175+
];
176+
177+
// build task & resolve positions
178+
let view_task = canonical_txs.view_task(&env.tx_graph);
179+
let canonical_view = local_chain.canonicalize(view_task);
180+
181+
// assert final positions
182+
for (tx_name, exp_position) in exp_positions {
183+
let txid = *env
184+
.txid_to_name
185+
.get(tx_name)
186+
.expect("txid should exist for tx_name");
187+
188+
let canonical_position = canonical_view
189+
.txs()
190+
.find(|ctx| ctx.txid == txid)
191+
.expect("expected txid should exist in canonical view")
192+
.pos;
193+
194+
assert_eq!(
195+
canonical_position, exp_position,
196+
"[{}] canonical position mismatch for {}",
197+
scenario.name, tx_name
198+
);
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)