Skip to content

Commit f73f26e

Browse files
committed
feat(chain)!: 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 68db7e5 commit f73f26e

File tree

14 files changed

+408
-98
lines changed

14 files changed

+408
-98
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, 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: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,26 @@ 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 CanonicalizationParams {
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+
}
1020
/// Iterates over canonical txs.
1121
pub struct CanonicalIter<'g, A, C> {
1222
tx_graph: &'g TxGraph<A>,
1323
chain: &'g C,
1424
chain_tip: BlockId,
1525

26+
unprocessed_assumed_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>)> + 'g>,
1627
unprocessed_anchored_txs:
1728
Box<dyn Iterator<Item = (Txid, Arc<Transaction>, &'g BTreeSet<A>)> + 'g>,
1829
unprocessed_seen_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>, u64)> + 'g>,
@@ -26,8 +37,19 @@ pub struct CanonicalIter<'g, A, C> {
2637

2738
impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
2839
/// Constructs [`CanonicalIter`].
29-
pub fn new(tx_graph: &'g TxGraph<A>, chain: &'g C, chain_tip: BlockId) -> Self {
40+
pub fn new(
41+
tx_graph: &'g TxGraph<A>,
42+
chain: &'g C,
43+
chain_tip: BlockId,
44+
mods: CanonicalizationParams,
45+
) -> Self {
3046
let anchors = tx_graph.all_anchors();
47+
let unprocessed_assumed_txs = Box::new(
48+
mods.assume_canonical
49+
.into_iter()
50+
.rev()
51+
.filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))),
52+
);
3153
let unprocessed_anchored_txs = Box::new(
3254
tx_graph
3355
.txids_by_descending_anchor_height()
@@ -42,6 +64,7 @@ impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
4264
tx_graph,
4365
chain,
4466
chain_tip,
67+
unprocessed_assumed_txs,
4568
unprocessed_anchored_txs,
4669
unprocessed_seen_txs,
4770
unprocessed_leftover_txs: VecDeque::new(),
@@ -147,6 +170,12 @@ impl<A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'_, A, C> {
147170
return Some(Ok((txid, tx, reason)));
148171
}
149172

173+
if let Some((txid, tx)) = self.unprocessed_assumed_txs.next() {
174+
if !self.is_canonicalized(txid) {
175+
self.mark_canonical(txid, tx, CanonicalReason::assumed());
176+
}
177+
}
178+
150179
if let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() {
151180
if !self.is_canonicalized(txid) {
152181
if let Err(err) = self.scan_anchors(txid, tx, anchors) {
@@ -189,6 +218,12 @@ pub enum ObservedIn {
189218
/// The reason why a transaction is canonical.
190219
#[derive(Debug, Clone, PartialEq, Eq)]
191220
pub enum CanonicalReason<A> {
221+
/// This transaction is explicitly assumed to be canonical by the caller, superceding all other
222+
/// canonicalization rules.
223+
Assumed {
224+
/// Whether it is a descendant that is assumed to be canonical.
225+
descendant: Option<Txid>,
226+
},
192227
/// This transaction is anchored in the best chain by `A`, and therefore canonical.
193228
Anchor {
194229
/// The anchor that anchored the transaction in the chain.
@@ -207,6 +242,12 @@ pub enum CanonicalReason<A> {
207242
}
208243

209244
impl<A: Clone> CanonicalReason<A> {
245+
/// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other
246+
/// transactions.
247+
pub fn assumed() -> Self {
248+
Self::Assumed { descendant: None }
249+
}
250+
210251
/// Constructs a [`CanonicalReason`] from an `anchor`.
211252
pub fn from_anchor(anchor: A) -> Self {
212253
Self::Anchor {
@@ -229,6 +270,9 @@ impl<A: Clone> CanonicalReason<A> {
229270
/// descendant, but is transitively relevant.
230271
pub fn to_transitive(&self, descendant: Txid) -> Self {
231272
match self {
273+
CanonicalReason::Assumed { .. } => Self::Assumed {
274+
descendant: Some(descendant),
275+
},
232276
CanonicalReason::Anchor { anchor, .. } => Self::Anchor {
233277
anchor: anchor.clone(),
234278
descendant: Some(descendant),
@@ -244,6 +288,7 @@ impl<A: Clone> CanonicalReason<A> {
244288
/// descendant.
245289
pub fn descendant(&self) -> &Option<Txid> {
246290
match self {
291+
CanonicalReason::Assumed { descendant, .. } => descendant,
247292
CanonicalReason::Anchor { descendant, .. } => descendant,
248293
CanonicalReason::ObservedIn { descendant, .. } => descendant,
249294
}

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::CanonicalizationParams;
9798
use crate::ObservedIn;
9899
use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge};
99100
use alloc::collections::vec_deque::VecDeque;
@@ -797,25 +798,46 @@ impl<A: Anchor> TxGraph<A> {
797798
&'a self,
798799
chain: &'a C,
799800
chain_tip: BlockId,
801+
mods: CanonicalizationParams,
800802
) -> impl Iterator<Item = Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error>> {
801-
self.canonical_iter(chain, chain_tip).flat_map(move |res| {
802-
res.map(|(txid, _, canonical_reason)| {
803-
let tx_node = self.get_tx_node(txid).expect("must contain tx");
804-
let chain_position = match canonical_reason {
805-
CanonicalReason::Anchor { anchor, descendant } => match descendant {
806-
Some(_) => {
807-
let direct_anchor = tx_node
808-
.anchors
809-
.iter()
810-
.find_map(|a| -> Option<Result<A, C::Error>> {
811-
match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
812-
Ok(Some(true)) => Some(Ok(a.clone())),
813-
Ok(Some(false)) | Ok(None) => None,
814-
Err(err) => Some(Err(err)),
815-
}
816-
})
817-
.transpose()?;
818-
match direct_anchor {
803+
fn find_direct_anchor<A: Anchor, C: ChainOracle>(
804+
tx_node: &TxNode<'_, Arc<Transaction>, A>,
805+
chain: &C,
806+
chain_tip: BlockId,
807+
) -> Result<Option<A>, C::Error> {
808+
tx_node
809+
.anchors
810+
.iter()
811+
.find_map(|a| -> Option<Result<A, C::Error>> {
812+
match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
813+
Ok(Some(true)) => Some(Ok(a.clone())),
814+
Ok(Some(false)) | Ok(None) => None,
815+
Err(err) => Some(Err(err)),
816+
}
817+
})
818+
.transpose()
819+
}
820+
self.canonical_iter(chain, chain_tip, mods)
821+
.flat_map(move |res| {
822+
res.map(|(txid, _, canonical_reason)| {
823+
let tx_node = self.get_tx_node(txid).expect("must contain tx");
824+
let chain_position = match canonical_reason {
825+
CanonicalReason::Assumed { descendant } => match descendant {
826+
Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
827+
Some(anchor) => ChainPosition::Confirmed {
828+
anchor,
829+
transitively: None,
830+
},
831+
None => ChainPosition::Unconfirmed {
832+
last_seen: tx_node.last_seen_unconfirmed,
833+
},
834+
},
835+
None => ChainPosition::Unconfirmed {
836+
last_seen: tx_node.last_seen_unconfirmed,
837+
},
838+
},
839+
CanonicalReason::Anchor { anchor, descendant } => match descendant {
840+
Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
819841
Some(anchor) => ChainPosition::Confirmed {
820842
anchor,
821843
transitively: None,
@@ -824,26 +846,25 @@ impl<A: Anchor> TxGraph<A> {
824846
anchor,
825847
transitively: descendant,
826848
},
827-
}
828-
}
829-
None => ChainPosition::Confirmed {
830-
anchor,
831-
transitively: None,
849+
},
850+
None => ChainPosition::Confirmed {
851+
anchor,
852+
transitively: None,
853+
},
832854
},
833-
},
834-
CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
835-
ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
836-
last_seen: Some(last_seen),
855+
CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
856+
ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
857+
last_seen: Some(last_seen),
858+
},
859+
ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
837860
},
838-
ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
839-
},
840-
};
841-
Ok(CanonicalTx {
842-
chain_position,
843-
tx_node,
861+
};
862+
Ok(CanonicalTx {
863+
chain_position,
864+
tx_node,
865+
})
844866
})
845867
})
846-
})
847868
}
848869

849870
/// List graph transactions that are in `chain` with `chain_tip`.
@@ -855,8 +876,9 @@ impl<A: Anchor> TxGraph<A> {
855876
&'a self,
856877
chain: &'a C,
857878
chain_tip: BlockId,
879+
mods: CanonicalizationParams,
858880
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
859-
self.try_list_canonical_txs(chain, chain_tip)
881+
self.try_list_canonical_txs(chain, chain_tip, mods)
860882
.map(|res| res.expect("infallible"))
861883
}
862884

@@ -883,11 +905,12 @@ impl<A: Anchor> TxGraph<A> {
883905
&'a self,
884906
chain: &'a C,
885907
chain_tip: BlockId,
908+
mods: CanonicalizationParams,
886909
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
887910
) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
888911
let mut canon_txs = HashMap::<Txid, CanonicalTx<Arc<Transaction>, A>>::new();
889912
let mut canon_spends = HashMap::<OutPoint, Txid>::new();
890-
for r in self.try_list_canonical_txs(chain, chain_tip) {
913+
for r in self.try_list_canonical_txs(chain, chain_tip, mods) {
891914
let canonical_tx = r?;
892915
let txid = canonical_tx.tx_node.txid;
893916

@@ -956,8 +979,9 @@ impl<A: Anchor> TxGraph<A> {
956979
&'a self,
957980
chain: &'a C,
958981
chain_tip: BlockId,
982+
mods: CanonicalizationParams,
959983
) -> CanonicalIter<'a, A, C> {
960-
CanonicalIter::new(self, chain, chain_tip)
984+
CanonicalIter::new(self, chain, chain_tip, mods)
961985
}
962986

963987
/// Get a filtered list of outputs from the given `outpoints` that are in `chain` with
@@ -970,9 +994,10 @@ impl<A: Anchor> TxGraph<A> {
970994
&'a self,
971995
chain: &'a C,
972996
chain_tip: BlockId,
997+
mods: CanonicalizationParams,
973998
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
974999
) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
975-
self.try_filter_chain_txouts(chain, chain_tip, outpoints)
1000+
self.try_filter_chain_txouts(chain, chain_tip, mods, outpoints)
9761001
.expect("oracle is infallible")
9771002
}
9781003

@@ -998,10 +1023,11 @@ impl<A: Anchor> TxGraph<A> {
9981023
&'a self,
9991024
chain: &'a C,
10001025
chain_tip: BlockId,
1026+
mods: CanonicalizationParams,
10011027
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
10021028
) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
10031029
Ok(self
1004-
.try_filter_chain_txouts(chain, chain_tip, outpoints)?
1030+
.try_filter_chain_txouts(chain, chain_tip, mods, outpoints)?
10051031
.filter(|(_, full_txo)| full_txo.spent_by.is_none()))
10061032
}
10071033

@@ -1015,9 +1041,10 @@ impl<A: Anchor> TxGraph<A> {
10151041
&'a self,
10161042
chain: &'a C,
10171043
chain_tip: BlockId,
1044+
mods: CanonicalizationParams,
10181045
txouts: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
10191046
) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
1020-
self.try_filter_chain_unspents(chain, chain_tip, txouts)
1047+
self.try_filter_chain_unspents(chain, chain_tip, mods, txouts)
10211048
.expect("oracle is infallible")
10221049
}
10231050

@@ -1037,6 +1064,7 @@ impl<A: Anchor> TxGraph<A> {
10371064
&self,
10381065
chain: &C,
10391066
chain_tip: BlockId,
1067+
mods: CanonicalizationParams,
10401068
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
10411069
mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
10421070
) -> Result<Balance, C::Error> {
@@ -1045,7 +1073,7 @@ impl<A: Anchor> TxGraph<A> {
10451073
let mut untrusted_pending = Amount::ZERO;
10461074
let mut confirmed = Amount::ZERO;
10471075

1048-
for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, outpoints)? {
1076+
for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, mods, outpoints)? {
10491077
match &txout.chain_position {
10501078
ChainPosition::Confirmed { .. } => {
10511079
if txout.is_confirmed_and_spendable(chain_tip.height) {
@@ -1081,10 +1109,11 @@ impl<A: Anchor> TxGraph<A> {
10811109
&self,
10821110
chain: &C,
10831111
chain_tip: BlockId,
1112+
mods: CanonicalizationParams,
10841113
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
10851114
trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
10861115
) -> Balance {
1087-
self.try_balance(chain, chain_tip, outpoints, trust_predicate)
1116+
self.try_balance(chain, chain_tip, mods, outpoints, trust_predicate)
10881117
.expect("oracle is infallible")
10891118
}
10901119
}

0 commit comments

Comments
 (0)