Skip to content

Commit 9624b00

Browse files
committed
feat(chain,wallet)!: Add ability to modify canonicalization algorithm
Introduce `CanonicalizationMods` which is passed in to `CanonicalIter::new`. `CanonicalizationMods::assume_canonical` is the only field right now. This contains a list of txids that we assume to be canonical, superceding any other canonicalization rules.
1 parent 251bd7e commit 9624b00

File tree

15 files changed

+215
-77
lines changed

15 files changed

+215
-77
lines changed

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, CanonicalizationMods, 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+
CanonicalizationMods::NONE,
313+
outpoints,
314+
|_, _| true,
315+
);
312316
Ok(balance)
313317
}
314318

crates/chain/benches/canonicalization.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use bdk_chain::CanonicalizationMods;
12
use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph};
23
use bdk_core::{BlockId, CheckPoint};
34
use bdk_core::{ConfirmationBlockTime, TxUpdate};
@@ -92,14 +93,15 @@ fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, Lo
9293
fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
9394
let txs = tx_graph
9495
.graph()
95-
.list_canonical_txs(chain, chain.tip().block_id());
96+
.list_canonical_txs(chain, chain.tip().block_id(), CanonicalizationMods::NONE);
9697
assert_eq!(txs.count(), exp_txs);
9798
}
9899

99100
fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) {
100101
let utxos = tx_graph.graph().filter_chain_txouts(
101102
chain,
102103
chain.tip().block_id(),
104+
CanonicalizationMods::NONE,
103105
tx_graph.index.outpoints().clone(),
104106
);
105107
assert_eq!(utxos.count(), exp_txos);
@@ -109,6 +111,7 @@ fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp
109111
let utxos = tx_graph.graph().filter_chain_unspents(
110112
chain,
111113
chain.tip().block_id(),
114+
CanonicalizationMods::NONE,
112115
tx_graph.index.outpoints().clone(),
113116
);
114117
assert_eq!(utxos.count(), exp_utxos);

crates/chain/src/canonical_iter.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,34 @@ use crate::{Anchor, ChainOracle, TxGraph};
44
use alloc::boxed::Box;
55
use alloc::collections::BTreeSet;
66
use alloc::sync::Arc;
7+
use alloc::vec::Vec;
78
use bdk_core::BlockId;
89
use bitcoin::{Transaction, Txid};
910

11+
/// Modifies the canonicalization algorithm.
12+
#[derive(Debug, Default, Clone)]
13+
pub struct CanonicalizationMods {
14+
/// Transactions that will supercede all other transactions.
15+
///
16+
/// In case of conflicting transactions within `assume_canonical`, transactions that appear
17+
/// later in the list (have higher index) have precedence.
18+
pub assume_canonical: Vec<Txid>,
19+
}
20+
21+
impl CanonicalizationMods {
22+
/// No mods.
23+
pub const NONE: Self = Self {
24+
assume_canonical: Vec::new(),
25+
};
26+
}
27+
1028
/// Iterates over canonical txs.
1129
pub struct CanonicalIter<'g, A, C> {
1230
tx_graph: &'g TxGraph<A>,
1331
chain: &'g C,
1432
chain_tip: BlockId,
1533

34+
unprocessed_assumed_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>)> + 'g>,
1635
unprocessed_anchored_txs:
1736
Box<dyn Iterator<Item = (Txid, Arc<Transaction>, &'g BTreeSet<A>)> + 'g>,
1837
unprocessed_seen_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>, u64)> + 'g>,
@@ -26,8 +45,19 @@ pub struct CanonicalIter<'g, A, C> {
2645

2746
impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
2847
/// Constructs [`CanonicalIter`].
29-
pub fn new(tx_graph: &'g TxGraph<A>, chain: &'g C, chain_tip: BlockId) -> Self {
48+
pub fn new(
49+
tx_graph: &'g TxGraph<A>,
50+
chain: &'g C,
51+
chain_tip: BlockId,
52+
mods: CanonicalizationMods,
53+
) -> Self {
3054
let anchors = tx_graph.all_anchors();
55+
let unprocessed_assumed_txs = Box::new(
56+
mods.assume_canonical
57+
.into_iter()
58+
.rev()
59+
.filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))),
60+
);
3161
let unprocessed_anchored_txs = Box::new(
3262
tx_graph
3363
.txids_by_descending_anchor_height()
@@ -42,6 +72,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
4272
tx_graph,
4373
chain,
4474
chain_tip,
75+
unprocessed_assumed_txs,
4576
unprocessed_anchored_txs,
4677
unprocessed_seen_txs,
4778
unprocessed_leftover_txs: VecDeque::new(),
@@ -147,6 +178,12 @@ impl<A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'_, A, C> {
147178
return Some(Ok((txid, tx, reason)));
148179
}
149180

181+
if let Some((txid, tx)) = self.unprocessed_assumed_txs.next() {
182+
if !self.is_canonicalized(txid) {
183+
self.mark_canonical(txid, tx, CanonicalReason::assumed());
184+
}
185+
}
186+
150187
if let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() {
151188
if !self.is_canonicalized(txid) {
152189
if let Err(err) = self.scan_anchors(txid, tx, anchors) {
@@ -189,6 +226,12 @@ pub enum ObservedIn {
189226
/// The reason why a transaction is canonical.
190227
#[derive(Debug, Clone, PartialEq, Eq)]
191228
pub enum CanonicalReason<A> {
229+
/// This transaction is explicitly assumed to be canonical by the caller, superceding all other
230+
/// canonicalization rules.
231+
Assumed {
232+
/// Whether it is a descendant that is assumed to be canonical.
233+
descendant: Option<Txid>,
234+
},
192235
/// This transaction is anchored in the best chain by `A`, and therefore canonical.
193236
Anchor {
194237
/// The anchor that anchored the transaction in the chain.
@@ -207,6 +250,12 @@ pub enum CanonicalReason<A> {
207250
}
208251

209252
impl<A: Clone> CanonicalReason<A> {
253+
/// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other
254+
/// transactions.
255+
pub fn assumed() -> Self {
256+
Self::Assumed { descendant: None }
257+
}
258+
210259
/// Constructs a [`CanonicalReason`] from an `anchor`.
211260
pub fn from_anchor(anchor: A) -> Self {
212261
Self::Anchor {
@@ -229,6 +278,9 @@ impl<A: Clone> CanonicalReason<A> {
229278
/// descendant, but is transitively relevant.
230279
pub fn to_transitive(&self, descendant: Txid) -> Self {
231280
match self {
281+
CanonicalReason::Assumed { .. } => Self::Assumed {
282+
descendant: Some(descendant),
283+
},
232284
CanonicalReason::Anchor { anchor, .. } => Self::Anchor {
233285
anchor: anchor.clone(),
234286
descendant: Some(descendant),
@@ -244,6 +296,7 @@ impl<A: Clone> CanonicalReason<A> {
244296
/// descendant.
245297
pub fn descendant(&self) -> &Option<Txid> {
246298
match self {
299+
CanonicalReason::Assumed { descendant, .. } => descendant,
247300
CanonicalReason::Anchor { descendant, .. } => descendant,
248301
CanonicalReason::ObservedIn { descendant, .. } => descendant,
249302
}

crates/chain/src/tx_graph.rs

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ use crate::collections::*;
9494
use crate::BlockId;
9595
use crate::CanonicalIter;
9696
use crate::CanonicalReason;
97+
use crate::CanonicalizationMods;
9798
use crate::ObservedIn;
9899
use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge};
99100
use alloc::collections::vec_deque::VecDeque;
@@ -829,25 +830,46 @@ impl<A: Anchor> TxGraph<A> {
829830
&'a self,
830831
chain: &'a C,
831832
chain_tip: BlockId,
833+
mods: CanonicalizationMods,
832834
) -> impl Iterator<Item = Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error>> {
833-
self.canonical_iter(chain, chain_tip).flat_map(move |res| {
834-
res.map(|(txid, _, canonical_reason)| {
835-
let tx_node = self.get_tx_node(txid).expect("must contain tx");
836-
let chain_position = match canonical_reason {
837-
CanonicalReason::Anchor { anchor, descendant } => match descendant {
838-
Some(_) => {
839-
let direct_anchor = tx_node
840-
.anchors
841-
.iter()
842-
.find_map(|a| -> Option<Result<A, C::Error>> {
843-
match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
844-
Ok(Some(true)) => Some(Ok(a.clone())),
845-
Ok(Some(false)) | Ok(None) => None,
846-
Err(err) => Some(Err(err)),
847-
}
848-
})
849-
.transpose()?;
850-
match direct_anchor {
835+
fn find_direct_anchor<A: Anchor, C: ChainOracle>(
836+
tx_node: &TxNode<'_, Arc<Transaction>, A>,
837+
chain: &C,
838+
chain_tip: BlockId,
839+
) -> Result<Option<A>, C::Error> {
840+
tx_node
841+
.anchors
842+
.iter()
843+
.find_map(|a| -> Option<Result<A, C::Error>> {
844+
match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
845+
Ok(Some(true)) => Some(Ok(a.clone())),
846+
Ok(Some(false)) | Ok(None) => None,
847+
Err(err) => Some(Err(err)),
848+
}
849+
})
850+
.transpose()
851+
}
852+
self.canonical_iter(chain, chain_tip, mods)
853+
.flat_map(move |res| {
854+
res.map(|(txid, _, canonical_reason)| {
855+
let tx_node = self.get_tx_node(txid).expect("must contain tx");
856+
let chain_position = match canonical_reason {
857+
CanonicalReason::Assumed { descendant } => match descendant {
858+
Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
859+
Some(anchor) => ChainPosition::Confirmed {
860+
anchor,
861+
transitively: None,
862+
},
863+
None => ChainPosition::Unconfirmed {
864+
last_seen: tx_node.last_seen_unconfirmed,
865+
},
866+
},
867+
None => ChainPosition::Unconfirmed {
868+
last_seen: tx_node.last_seen_unconfirmed,
869+
},
870+
},
871+
CanonicalReason::Anchor { anchor, descendant } => match descendant {
872+
Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
851873
Some(anchor) => ChainPosition::Confirmed {
852874
anchor,
853875
transitively: None,
@@ -856,26 +878,25 @@ impl<A: Anchor> TxGraph<A> {
856878
anchor,
857879
transitively: descendant,
858880
},
859-
}
860-
}
861-
None => ChainPosition::Confirmed {
862-
anchor,
863-
transitively: None,
881+
},
882+
None => ChainPosition::Confirmed {
883+
anchor,
884+
transitively: None,
885+
},
864886
},
865-
},
866-
CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
867-
ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
868-
last_seen: Some(last_seen),
887+
CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
888+
ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
889+
last_seen: Some(last_seen),
890+
},
891+
ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
869892
},
870-
ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
871-
},
872-
};
873-
Ok(CanonicalTx {
874-
chain_position,
875-
tx_node,
893+
};
894+
Ok(CanonicalTx {
895+
chain_position,
896+
tx_node,
897+
})
876898
})
877899
})
878-
})
879900
}
880901

881902
/// List graph transactions that are in `chain` with `chain_tip`.
@@ -887,8 +908,9 @@ impl<A: Anchor> TxGraph<A> {
887908
&'a self,
888909
chain: &'a C,
889910
chain_tip: BlockId,
911+
mods: CanonicalizationMods,
890912
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
891-
self.try_list_canonical_txs(chain, chain_tip)
913+
self.try_list_canonical_txs(chain, chain_tip, mods)
892914
.map(|res| res.expect("infallible"))
893915
}
894916

@@ -915,11 +937,12 @@ impl<A: Anchor> TxGraph<A> {
915937
&'a self,
916938
chain: &'a C,
917939
chain_tip: BlockId,
940+
mods: CanonicalizationMods,
918941
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
919942
) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
920943
let mut canon_txs = HashMap::<Txid, CanonicalTx<Arc<Transaction>, A>>::new();
921944
let mut canon_spends = HashMap::<OutPoint, Txid>::new();
922-
for r in self.try_list_canonical_txs(chain, chain_tip) {
945+
for r in self.try_list_canonical_txs(chain, chain_tip, mods) {
923946
let canonical_tx = r?;
924947
let txid = canonical_tx.tx_node.txid;
925948

@@ -988,8 +1011,9 @@ impl<A: Anchor> TxGraph<A> {
9881011
&'a self,
9891012
chain: &'a C,
9901013
chain_tip: BlockId,
1014+
mods: CanonicalizationMods,
9911015
) -> CanonicalIter<'a, A, C> {
992-
CanonicalIter::new(self, chain, chain_tip)
1016+
CanonicalIter::new(self, chain, chain_tip, mods)
9931017
}
9941018

9951019
/// Get a filtered list of outputs from the given `outpoints` that are in `chain` with
@@ -1002,9 +1026,10 @@ impl<A: Anchor> TxGraph<A> {
10021026
&'a self,
10031027
chain: &'a C,
10041028
chain_tip: BlockId,
1029+
mods: CanonicalizationMods,
10051030
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
10061031
) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
1007-
self.try_filter_chain_txouts(chain, chain_tip, outpoints)
1032+
self.try_filter_chain_txouts(chain, chain_tip, mods, outpoints)
10081033
.expect("oracle is infallible")
10091034
}
10101035

@@ -1030,10 +1055,11 @@ impl<A: Anchor> TxGraph<A> {
10301055
&'a self,
10311056
chain: &'a C,
10321057
chain_tip: BlockId,
1058+
mods: CanonicalizationMods,
10331059
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
10341060
) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
10351061
Ok(self
1036-
.try_filter_chain_txouts(chain, chain_tip, outpoints)?
1062+
.try_filter_chain_txouts(chain, chain_tip, mods, outpoints)?
10371063
.filter(|(_, full_txo)| full_txo.spent_by.is_none()))
10381064
}
10391065

@@ -1047,9 +1073,10 @@ impl<A: Anchor> TxGraph<A> {
10471073
&'a self,
10481074
chain: &'a C,
10491075
chain_tip: BlockId,
1076+
mods: CanonicalizationMods,
10501077
txouts: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
10511078
) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
1052-
self.try_filter_chain_unspents(chain, chain_tip, txouts)
1079+
self.try_filter_chain_unspents(chain, chain_tip, mods, txouts)
10531080
.expect("oracle is infallible")
10541081
}
10551082

@@ -1069,6 +1096,7 @@ impl<A: Anchor> TxGraph<A> {
10691096
&self,
10701097
chain: &C,
10711098
chain_tip: BlockId,
1099+
mods: CanonicalizationMods,
10721100
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
10731101
mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
10741102
) -> Result<Balance, C::Error> {
@@ -1077,7 +1105,7 @@ impl<A: Anchor> TxGraph<A> {
10771105
let mut untrusted_pending = Amount::ZERO;
10781106
let mut confirmed = Amount::ZERO;
10791107

1080-
for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, outpoints)? {
1108+
for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, mods, outpoints)? {
10811109
match &txout.chain_position {
10821110
ChainPosition::Confirmed { .. } => {
10831111
if txout.is_confirmed_and_spendable(chain_tip.height) {
@@ -1113,10 +1141,11 @@ impl<A: Anchor> TxGraph<A> {
11131141
&self,
11141142
chain: &C,
11151143
chain_tip: BlockId,
1144+
mods: CanonicalizationMods,
11161145
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
11171146
trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
11181147
) -> Balance {
1119-
self.try_balance(chain, chain_tip, outpoints, trust_predicate)
1148+
self.try_balance(chain, chain_tip, mods, outpoints, trust_predicate)
11201149
.expect("oracle is infallible")
11211150
}
11221151
}

0 commit comments

Comments
 (0)