Skip to content

Commit 86cdaf4

Browse files
committed
Merge #1808: Introduce canonicalization parameters
53afb35 test(graph): Add additional witness scenarios (Wei Chen) 46af2a5 feat(chain)!: Add ability to modify canonicalization algorithm (志宇) 58fcf32 refactor(chain): Make private fields in `CanonicalIter` concise. (志宇) 5eb5e86 feat(chain): Signed txs should displace unsigned txs in `TxGraph`. (志宇) Pull request description: Partially Fixes bitcoindevkit/bdk_wallet#40 ### Description Add the ability to modify the canonicalization algorithm. Right now, the only modifier is `assume_canonical` which takes in a `Vec` (ordered list) of txids and superimposes it on the canonicalization algorithm. Txs later in the list (higher index) have a higher priority (in case of conflicts). ### Notes to the reviewers None at the moment. ### Changelog notice * Added `CanonicalizationParams` to allow the caller to modify the canonicalization algorithm. This in a new parameter on `CanonicalIter::new`. * Changed `TxGraph::insert_tx` to allow for updating a transaction's witness field. This is useful for initially introducing an unsigned tx and adding witnesses later on. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature #### Bugfixes: * [x] This pull request breaks the existing API * [x] I've added tests to reproduce the issue which are now passing * [x] I'm linking the issue being fixed by this PR ACKs for top commit: LagginTimes: ACK 53afb35 ValuedMammal: ACK 53afb35 Tree-SHA512: 9fd070a3829d194f4d216c873922d9d47e047fa4b618628b875456f5cf4661d6b6c08a3a0dd4739b3a0f765e47c7c99acca53f195379e37f0f9cfed027bde17b
2 parents aed5a8f + 53afb35 commit 86cdaf4

File tree

14 files changed

+618
-119
lines changed

14 files changed

+618
-119
lines changed

crates/bitcoind_rpc/examples/filter_iter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ fn main() -> anyhow::Result<()> {
9292
.filter_chain_unspents(
9393
&chain,
9494
chain.tip().block_id(),
95+
Default::default(),
9596
graph.index.outpoints().clone(),
9697
)
9798
.collect();

crates/bitcoind_rpc/tests/test_emitter.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bdk_chain::{
55
bitcoin::{Address, Amount, Txid},
66
local_chain::{CheckPoint, LocalChain},
77
spk_txout::SpkTxOutIndex,
8-
Balance, BlockId, IndexedTxGraph, Merge,
8+
Balance, BlockId, CanonicalizationParams, IndexedTxGraph, Merge,
99
};
1010
use bdk_testenv::{anyhow, TestEnv};
1111
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
@@ -306,9 +306,13 @@ fn get_balance(
306306
) -> anyhow::Result<Balance> {
307307
let chain_tip = recv_chain.tip().block_id();
308308
let outpoints = recv_graph.index.outpoints().clone();
309-
let balance = recv_graph
310-
.graph()
311-
.balance(recv_chain, chain_tip, outpoints, |_, _| true);
309+
let balance = recv_graph.graph().balance(
310+
recv_chain,
311+
chain_tip,
312+
CanonicalizationParams::default(),
313+
outpoints,
314+
|_, _| true,
315+
);
312316
Ok(balance)
313317
}
314318

crates/chain/benches/canonicalization.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use bdk_chain::CanonicalizationParams;
12
use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph};
23
use bdk_core::{BlockId, CheckPoint};
34
use bdk_core::{ConfirmationBlockTime, TxUpdate};
@@ -90,16 +91,19 @@ fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, Lo
9091
}
9192

9293
fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
93-
let txs = tx_graph
94-
.graph()
95-
.list_canonical_txs(chain, chain.tip().block_id());
94+
let txs = tx_graph.graph().list_canonical_txs(
95+
chain,
96+
chain.tip().block_id(),
97+
CanonicalizationParams::default(),
98+
);
9699
assert_eq!(txs.count(), exp_txs);
97100
}
98101

99102
fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) {
100103
let utxos = tx_graph.graph().filter_chain_txouts(
101104
chain,
102105
chain.tip().block_id(),
106+
CanonicalizationParams::default(),
103107
tx_graph.index.outpoints().clone(),
104108
);
105109
assert_eq!(utxos.count(), exp_txos);
@@ -109,6 +113,7 @@ fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp
109113
let utxos = tx_graph.graph().filter_chain_unspents(
110114
chain,
111115
chain.tip().block_id(),
116+
CanonicalizationParams::default(),
112117
tx_graph.index.outpoints().clone(),
113118
);
114119
assert_eq!(utxos.count(), exp_utxos);

crates/chain/src/canonical_iter.rs

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,27 @@ use bitcoin::{Transaction, Txid};
1111
type CanonicalMap<A> = HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>;
1212
type NotCanonicalSet = HashSet<Txid>;
1313

14+
/// Modifies the canonicalization algorithm.
15+
#[derive(Debug, Default, Clone)]
16+
pub struct CanonicalizationParams {
17+
/// Transactions that will supercede all other transactions.
18+
///
19+
/// In case of conflicting transactions within `assume_canonical`, transactions that appear
20+
/// later in the list (have higher index) have precedence.
21+
pub assume_canonical: Vec<Txid>,
22+
}
23+
1424
/// Iterates over canonical txs.
1525
pub struct CanonicalIter<'g, A, C> {
1626
tx_graph: &'g TxGraph<A>,
1727
chain: &'g C,
1828
chain_tip: BlockId,
1929

20-
unprocessed_txs_with_anchors:
30+
unprocessed_assumed_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>)> + 'g>,
31+
unprocessed_anchored_txs:
2132
Box<dyn Iterator<Item = (Txid, Arc<Transaction>, &'g BTreeSet<A>)> + 'g>,
22-
unprocessed_txs_with_last_seens: Box<dyn Iterator<Item = (Txid, Arc<Transaction>, u64)> + 'g>,
23-
unprocessed_txs_left_over: VecDeque<(Txid, Arc<Transaction>, u32)>,
33+
unprocessed_seen_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>, u64)> + 'g>,
34+
unprocessed_leftover_txs: VecDeque<(Txid, Arc<Transaction>, u32)>,
2435

2536
canonical: CanonicalMap<A>,
2637
not_canonical: NotCanonicalSet,
@@ -30,14 +41,26 @@ pub struct CanonicalIter<'g, A, C> {
3041

3142
impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
3243
/// Constructs [`CanonicalIter`].
33-
pub fn new(tx_graph: &'g TxGraph<A>, chain: &'g C, chain_tip: BlockId) -> Self {
44+
pub fn new(
45+
tx_graph: &'g TxGraph<A>,
46+
chain: &'g C,
47+
chain_tip: BlockId,
48+
params: CanonicalizationParams,
49+
) -> Self {
3450
let anchors = tx_graph.all_anchors();
35-
let pending_anchored = Box::new(
51+
let unprocessed_assumed_txs = Box::new(
52+
params
53+
.assume_canonical
54+
.into_iter()
55+
.rev()
56+
.filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))),
57+
);
58+
let unprocessed_anchored_txs = Box::new(
3659
tx_graph
3760
.txids_by_descending_anchor_height()
3861
.filter_map(|(_, txid)| Some((txid, tx_graph.get_tx(txid)?, anchors.get(&txid)?))),
3962
);
40-
let pending_last_seen = Box::new(
63+
let unprocessed_seen_txs = Box::new(
4164
tx_graph
4265
.txids_by_descending_last_seen()
4366
.filter_map(|(last_seen, txid)| Some((txid, tx_graph.get_tx(txid)?, last_seen))),
@@ -46,9 +69,10 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
4669
tx_graph,
4770
chain,
4871
chain_tip,
49-
unprocessed_txs_with_anchors: pending_anchored,
50-
unprocessed_txs_with_last_seens: pending_last_seen,
51-
unprocessed_txs_left_over: VecDeque::new(),
72+
unprocessed_assumed_txs,
73+
unprocessed_anchored_txs,
74+
unprocessed_seen_txs,
75+
unprocessed_leftover_txs: VecDeque::new(),
5276
canonical: HashMap::new(),
5377
not_canonical: HashSet::new(),
5478
queue: VecDeque::new(),
@@ -77,7 +101,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
77101
}
78102
}
79103
// cannot determine
80-
self.unprocessed_txs_left_over.push_back((
104+
self.unprocessed_leftover_txs.push_back((
81105
txid,
82106
tx,
83107
anchors
@@ -190,7 +214,13 @@ impl<A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'_, A, C> {
190214
return Some(Ok((txid, tx, reason)));
191215
}
192216

193-
if let Some((txid, tx, anchors)) = self.unprocessed_txs_with_anchors.next() {
217+
if let Some((txid, tx)) = self.unprocessed_assumed_txs.next() {
218+
if !self.is_canonicalized(txid) {
219+
self.mark_canonical(txid, tx, CanonicalReason::assumed());
220+
}
221+
}
222+
223+
if let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() {
194224
if !self.is_canonicalized(txid) {
195225
if let Err(err) = self.scan_anchors(txid, tx, anchors) {
196226
return Some(Err(err));
@@ -199,15 +229,15 @@ impl<A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'_, A, C> {
199229
continue;
200230
}
201231

202-
if let Some((txid, tx, last_seen)) = self.unprocessed_txs_with_last_seens.next() {
232+
if let Some((txid, tx, last_seen)) = self.unprocessed_seen_txs.next() {
203233
if !self.is_canonicalized(txid) {
204234
let observed_in = ObservedIn::Mempool(last_seen);
205235
self.mark_canonical(txid, tx, CanonicalReason::from_observed_in(observed_in));
206236
}
207237
continue;
208238
}
209239

210-
if let Some((txid, tx, height)) = self.unprocessed_txs_left_over.pop_front() {
240+
if let Some((txid, tx, height)) = self.unprocessed_leftover_txs.pop_front() {
211241
if !self.is_canonicalized(txid) {
212242
let observed_in = ObservedIn::Block(height);
213243
self.mark_canonical(txid, tx, CanonicalReason::from_observed_in(observed_in));
@@ -232,6 +262,12 @@ pub enum ObservedIn {
232262
/// The reason why a transaction is canonical.
233263
#[derive(Debug, Clone, PartialEq, Eq)]
234264
pub enum CanonicalReason<A> {
265+
/// This transaction is explicitly assumed to be canonical by the caller, superceding all other
266+
/// canonicalization rules.
267+
Assumed {
268+
/// Whether it is a descendant that is assumed to be canonical.
269+
descendant: Option<Txid>,
270+
},
235271
/// This transaction is anchored in the best chain by `A`, and therefore canonical.
236272
Anchor {
237273
/// The anchor that anchored the transaction in the chain.
@@ -250,6 +286,12 @@ pub enum CanonicalReason<A> {
250286
}
251287

252288
impl<A: Clone> CanonicalReason<A> {
289+
/// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other
290+
/// transactions.
291+
pub fn assumed() -> Self {
292+
Self::Assumed { descendant: None }
293+
}
294+
253295
/// Constructs a [`CanonicalReason`] from an `anchor`.
254296
pub fn from_anchor(anchor: A) -> Self {
255297
Self::Anchor {
@@ -272,6 +314,9 @@ impl<A: Clone> CanonicalReason<A> {
272314
/// descendant, but is transitively relevant.
273315
pub fn to_transitive(&self, descendant: Txid) -> Self {
274316
match self {
317+
CanonicalReason::Assumed { .. } => Self::Assumed {
318+
descendant: Some(descendant),
319+
},
275320
CanonicalReason::Anchor { anchor, .. } => Self::Anchor {
276321
anchor: anchor.clone(),
277322
descendant: Some(descendant),
@@ -287,6 +332,7 @@ impl<A: Clone> CanonicalReason<A> {
287332
/// descendant.
288333
pub fn descendant(&self) -> &Option<Txid> {
289334
match self {
335+
CanonicalReason::Assumed { descendant, .. } => descendant,
290336
CanonicalReason::Anchor { descendant, .. } => descendant,
291337
CanonicalReason::ObservedIn { descendant, .. } => descendant,
292338
}

0 commit comments

Comments
 (0)