Skip to content

Commit 350cf6e

Browse files
committed
test(chain): add ancestor package tests
1 parent b5b29ed commit 350cf6e

File tree

1 file changed

+283
-0
lines changed

1 file changed

+283
-0
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
#![cfg(feature = "std")]
2+
3+
use bdk_chain::{local_chain::LocalChain, AncestorPackage, CanonicalizationParams, TxGraph};
4+
use bdk_core::{BlockId, ConfirmationBlockTime};
5+
use bitcoin::{
6+
absolute, hashes::Hash, transaction, Amount, BlockHash, FeeRate, OutPoint, ScriptBuf,
7+
Transaction, TxIn, TxOut, Txid, Weight,
8+
};
9+
use std::collections::BTreeMap;
10+
11+
fn make_tx(inputs: &[OutPoint], output_values: &[Amount]) -> Transaction {
12+
Transaction {
13+
version: transaction::Version::TWO,
14+
lock_time: absolute::LockTime::ZERO,
15+
input: inputs
16+
.iter()
17+
.map(|prev| TxIn {
18+
previous_output: *prev,
19+
..Default::default()
20+
})
21+
.collect(),
22+
output: output_values
23+
.iter()
24+
.map(|&value| TxOut {
25+
value,
26+
script_pubkey: ScriptBuf::new(),
27+
})
28+
.collect(),
29+
}
30+
}
31+
32+
fn block_id(height: u32) -> BlockId {
33+
BlockId {
34+
height,
35+
hash: BlockHash::from_byte_array([height as u8; 32]),
36+
}
37+
}
38+
39+
fn anchor(height: u32) -> ConfirmationBlockTime {
40+
ConfirmationBlockTime {
41+
block_id: block_id(height),
42+
confirmation_time: 123456,
43+
}
44+
}
45+
46+
fn build_view(
47+
graph: &TxGraph<ConfirmationBlockTime>,
48+
chain: &LocalChain,
49+
) -> bdk_chain::CanonicalView<ConfirmationBlockTime> {
50+
let tip = chain.tip().block_id();
51+
graph
52+
.try_canonical_view(chain, tip, CanonicalizationParams::default())
53+
.expect("infallible chain oracle")
54+
}
55+
56+
fn build_packages(
57+
graph: &TxGraph<ConfirmationBlockTime>,
58+
chain: &LocalChain,
59+
) -> BTreeMap<OutPoint, AncestorPackage> {
60+
build_view(graph, chain).ancestor_packages()
61+
}
62+
63+
/// Set up a chain with a confirmed coinbase (1 BTC) at height 1.
64+
fn setup() -> (LocalChain, TxGraph<ConfirmationBlockTime>, Txid) {
65+
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
66+
let chain =
67+
LocalChain::from_blocks([(0, BlockHash::all_zeros()), (1, block_id(1).hash)].into())
68+
.unwrap();
69+
70+
let coinbase = make_tx(&[OutPoint::null()], &[Amount::from_sat(100_000_000)]);
71+
let coinbase_txid = coinbase.compute_txid();
72+
let _ = graph.insert_tx(coinbase);
73+
let _ = graph.insert_anchor(coinbase_txid, anchor(1));
74+
75+
(chain, graph, coinbase_txid)
76+
}
77+
78+
#[test]
79+
fn single_unconfirmed_parent() {
80+
let (chain, mut graph, coinbase_txid) = setup();
81+
82+
// fee = 100_000_000 - 99_990_000 - 9_000 = 1_000
83+
let tx_1 = make_tx(
84+
&[OutPoint::new(coinbase_txid, 0)],
85+
&[Amount::from_sat(99_990_000), Amount::from_sat(9_000)],
86+
);
87+
let txid_1 = tx_1.compute_txid();
88+
let weight_1 = tx_1.weight();
89+
let _ = graph.insert_tx(tx_1);
90+
let _ = graph.insert_seen_at(txid_1, 1000);
91+
92+
let packages = build_packages(&graph, &chain);
93+
94+
let pkg_0 = packages.get(&OutPoint::new(txid_1, 0)).unwrap();
95+
let pkg_1 = packages.get(&OutPoint::new(txid_1, 1)).unwrap();
96+
97+
assert_eq!(pkg_0, pkg_1, "sibling UTXOs must have identical packages");
98+
assert_eq!(pkg_0.fee, Amount::from_sat(1_000));
99+
assert_eq!(pkg_0.weight, weight_1);
100+
}
101+
102+
#[test]
103+
fn two_level_unconfirmed_chain() {
104+
let (chain, mut graph, coinbase_txid) = setup();
105+
106+
// tx_1: fee = 5_000
107+
let tx_1 = make_tx(
108+
&[OutPoint::new(coinbase_txid, 0)],
109+
&[Amount::from_sat(99_995_000)],
110+
);
111+
let txid_1 = tx_1.compute_txid();
112+
let weight_1 = tx_1.weight();
113+
let _ = graph.insert_tx(tx_1);
114+
let _ = graph.insert_seen_at(txid_1, 1000);
115+
116+
// tx_2: spends TX1:0, fee = 5_000
117+
let tx_2 = make_tx(&[OutPoint::new(txid_1, 0)], &[Amount::from_sat(99_990_000)]);
118+
let txid_2 = tx_2.compute_txid();
119+
let weight_2 = tx_2.weight();
120+
let _ = graph.insert_tx(tx_2);
121+
let _ = graph.insert_seen_at(txid_2, 1001);
122+
123+
let packages = build_packages(&graph, &chain);
124+
125+
assert!(!packages.contains_key(&OutPoint::new(txid_1, 0)));
126+
127+
let pkg = packages.get(&OutPoint::new(txid_2, 0)).unwrap();
128+
assert_eq!(pkg.fee, Amount::from_sat(10_000));
129+
assert_eq!(pkg.weight, weight_1 + weight_2);
130+
}
131+
132+
#[test]
133+
fn stops_at_confirmed_boundary() {
134+
let (chain, mut graph, coinbase_txid) = setup();
135+
136+
// Confirmed tx_1
137+
let tx_1 = make_tx(
138+
&[OutPoint::new(coinbase_txid, 0)],
139+
&[Amount::from_sat(99_999_000)],
140+
);
141+
let txid_1 = tx_1.compute_txid();
142+
let _ = graph.insert_tx(tx_1);
143+
let _ = graph.insert_anchor(txid_1, anchor(1));
144+
145+
// Unconfirmed tx_2: spends confirmed TX1:0, fee = 2_000
146+
let tx_2 = make_tx(&[OutPoint::new(txid_1, 0)], &[Amount::from_sat(99_997_000)]);
147+
let txid_2 = tx_2.compute_txid();
148+
let weight_2 = tx_2.weight();
149+
let _ = graph.insert_tx(tx_2);
150+
let _ = graph.insert_seen_at(txid_2, 1000);
151+
152+
// Unconfirmed tx_3: spends TX2:0, fee = 5_000
153+
let tx_3 = make_tx(&[OutPoint::new(txid_2, 0)], &[Amount::from_sat(99_992_000)]);
154+
let txid_3 = tx_3.compute_txid();
155+
let weight_3 = tx_3.weight();
156+
let _ = graph.insert_tx(tx_3);
157+
let _ = graph.insert_seen_at(txid_3, 1001);
158+
159+
let packages = build_packages(&graph, &chain);
160+
let pkg = packages.get(&OutPoint::new(txid_3, 0)).unwrap();
161+
162+
assert_eq!(pkg.fee, Amount::from_sat(7_000));
163+
assert_eq!(pkg.weight, weight_2 + weight_3);
164+
}
165+
166+
#[test]
167+
fn shared_ancestor_counted_once() {
168+
let (chain, mut graph, coinbase_txid) = setup();
169+
170+
// Unconfirmed TX0: fee = 10_000_000
171+
let tx_0 = make_tx(
172+
&[OutPoint::new(coinbase_txid, 0)],
173+
&[Amount::from_sat(50_000_000), Amount::from_sat(40_000_000)],
174+
);
175+
let txid_0 = tx_0.compute_txid();
176+
let weight_0 = tx_0.weight();
177+
let _ = graph.insert_tx(tx_0);
178+
let _ = graph.insert_seen_at(txid_0, 1000);
179+
180+
// Unconfirmed TX1: spends TX0:0, fee = 5_000
181+
let tx_1 = make_tx(&[OutPoint::new(txid_0, 0)], &[Amount::from_sat(49_995_000)]);
182+
let txid_1 = tx_1.compute_txid();
183+
let weight_1 = tx_1.weight();
184+
let _ = graph.insert_tx(tx_1);
185+
let _ = graph.insert_seen_at(txid_1, 1001);
186+
187+
// Unconfirmed TX2: spends TX0:1, fee = 5_000
188+
let tx_2 = make_tx(&[OutPoint::new(txid_0, 1)], &[Amount::from_sat(39_995_000)]);
189+
let txid_2 = tx_2.compute_txid();
190+
let weight_2 = tx_2.weight();
191+
let _ = graph.insert_tx(tx_2);
192+
let _ = graph.insert_seen_at(txid_2, 1002);
193+
194+
// Unconfirmed TX3: spends TX1:0 and TX2:0, fee = 10_000
195+
let tx_3 = make_tx(
196+
&[OutPoint::new(txid_1, 0), OutPoint::new(txid_2, 0)],
197+
&[Amount::from_sat(89_980_000)],
198+
);
199+
let txid_3 = tx_3.compute_txid();
200+
let weight_3 = tx_3.weight();
201+
let _ = graph.insert_tx(tx_3);
202+
let _ = graph.insert_seen_at(txid_3, 1003);
203+
204+
let packages = build_packages(&graph, &chain);
205+
let pkg = packages.get(&OutPoint::new(txid_3, 0)).unwrap();
206+
207+
// TX0 counted once despite being ancestor of both TX1 and TX2.
208+
assert_eq!(
209+
pkg.fee,
210+
Amount::from_sat(10_000_000 + 5_000 + 5_000 + 10_000)
211+
);
212+
assert_eq!(pkg.weight, weight_0 + weight_1 + weight_2 + weight_3);
213+
}
214+
215+
#[test]
216+
fn aggregate_deduplicates_shared_ancestors() {
217+
let (chain, mut graph, coinbase_txid) = setup();
218+
219+
// Unconfirmed tx_0: two outputs, fee = 10_000
220+
let tx_0 = make_tx(
221+
&[OutPoint::new(coinbase_txid, 0)],
222+
&[Amount::from_sat(50_000_000), Amount::from_sat(49_990_000)],
223+
);
224+
let txid_0 = tx_0.compute_txid();
225+
let weight_0 = tx_0.weight();
226+
let _ = graph.insert_tx(tx_0);
227+
let _ = graph.insert_seen_at(txid_0, 1000);
228+
229+
// Unconfirmed tx_1: spends tx_0:0, fee = 5_000
230+
let tx_1 = make_tx(&[OutPoint::new(txid_0, 0)], &[Amount::from_sat(49_995_000)]);
231+
let txid_1 = tx_1.compute_txid();
232+
let weight_1 = tx_1.weight();
233+
let _ = graph.insert_tx(tx_1);
234+
let _ = graph.insert_seen_at(txid_1, 1001);
235+
236+
// Unconfirmed tx_2: spends tx_0:1, fee = 5_000
237+
let tx_2 = make_tx(&[OutPoint::new(txid_0, 1)], &[Amount::from_sat(49_985_000)]);
238+
let txid_2 = tx_2.compute_txid();
239+
let weight_2 = tx_2.weight();
240+
let _ = graph.insert_tx(tx_2);
241+
let _ = graph.insert_seen_at(txid_2, 1002);
242+
243+
let op_1 = OutPoint::new(txid_1, 0);
244+
let op_2 = OutPoint::new(txid_2, 0);
245+
246+
let view = build_view(&graph, &chain);
247+
let packages = view.ancestor_packages();
248+
249+
// Per-outpoint: each independently includes tx_0.
250+
let sum_fee = packages[&op_1].fee + packages[&op_2].fee;
251+
assert_eq!(sum_fee, Amount::from_sat(30_000));
252+
253+
// Aggregate: tx_0 counted once.
254+
let agg = view
255+
.aggregate_ancestor_package([OutPoint::new(txid_1, 0), OutPoint::new(txid_2, 0)])
256+
.unwrap();
257+
258+
assert_eq!(agg.fee, Amount::from_sat(10_000 + 5_000 + 5_000));
259+
assert_eq!(agg.weight, weight_0 + weight_1 + weight_2);
260+
}
261+
262+
#[test]
263+
fn fee_deficit_at_various_feerates() {
264+
let pkg = AncestorPackage {
265+
weight: Weight::from_wu(1000),
266+
fee: Amount::from_sat(250),
267+
};
268+
269+
// At 1 sat/vbyte (0.25 sat/wu): required = 250. Met exactly.
270+
let rate_1 = FeeRate::from_sat_per_vb_unchecked(1);
271+
assert_eq!(pkg.fee_deficit(rate_1), Amount::ZERO);
272+
273+
// At 2 sat/vbyte (0.5 sat/wu): required = 500. Deficit = 250.
274+
let rate_2 = FeeRate::from_sat_per_vb_unchecked(2);
275+
assert_eq!(pkg.fee_deficit(rate_2), Amount::from_sat(250));
276+
277+
// At 10 sat/vbyte (2.5 sat/wu): required = 2500. Deficit = 2250.
278+
let rate_10 = FeeRate::from_sat_per_vb_unchecked(10);
279+
assert_eq!(pkg.fee_deficit(rate_10), Amount::from_sat(2250));
280+
281+
// At 0 sat/vbyte: required = 0. Already met.
282+
assert_eq!(pkg.fee_deficit(FeeRate::ZERO), Amount::ZERO);
283+
}

0 commit comments

Comments
 (0)