Skip to content

Commit 555f774

Browse files
oleonardolimaevanlinjin
authored andcommitted
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 4f10cde commit 555f774

2 files changed

Lines changed: 224 additions & 10 deletions

File tree

crates/chain/src/canonical_view_task.rs

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

0 commit comments

Comments
 (0)