.filter_chain_unspents(
&chain,
chain.tip().block_id(),
+ Default::default(),
graph.index.outpoints().clone(),
)
.collect();
bitcoin::{Address, Amount, Txid},
local_chain::{CheckPoint, LocalChain},
spk_txout::SpkTxOutIndex,
- Balance, BlockId, IndexedTxGraph, Merge,
+ Balance, BlockId, CanonicalizationParams, IndexedTxGraph, Merge,
};
use bdk_testenv::{anyhow, TestEnv};
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
) -> anyhow::Result<Balance> {
let chain_tip = recv_chain.tip().block_id();
let outpoints = recv_graph.index.outpoints().clone();
- let balance = recv_graph
- .graph()
- .balance(recv_chain, chain_tip, outpoints, |_, _| true);
+ let balance = recv_graph.graph().balance(
+ recv_chain,
+ chain_tip,
+ CanonicalizationParams::default(),
+ outpoints,
+ |_, _| true,
+ );
Ok(balance)
}
+use bdk_chain::CanonicalizationParams;
use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph};
use bdk_core::{BlockId, CheckPoint};
use bdk_core::{ConfirmationBlockTime, TxUpdate};
}
fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
- let txs = tx_graph
- .graph()
- .list_canonical_txs(chain, chain.tip().block_id());
+ let txs = tx_graph.graph().list_canonical_txs(
+ chain,
+ chain.tip().block_id(),
+ CanonicalizationParams::default(),
+ );
assert_eq!(txs.count(), exp_txs);
}
let utxos = tx_graph.graph().filter_chain_txouts(
chain,
chain.tip().block_id(),
+ CanonicalizationParams::default(),
tx_graph.index.outpoints().clone(),
);
assert_eq!(utxos.count(), exp_txos);
let utxos = tx_graph.graph().filter_chain_unspents(
chain,
chain.tip().block_id(),
+ CanonicalizationParams::default(),
tx_graph.index.outpoints().clone(),
);
assert_eq!(utxos.count(), exp_utxos);
type CanonicalMap<A> = HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>;
type NotCanonicalSet = HashSet<Txid>;
+/// Modifies the canonicalization algorithm.
+#[derive(Debug, Default, Clone)]
+pub struct CanonicalizationParams {
+ /// Transactions that will supercede all other transactions.
+ ///
+ /// In case of conflicting transactions within `assume_canonical`, transactions that appear
+ /// later in the list (have higher index) have precedence.
+ pub assume_canonical: Vec<Txid>,
+}
+
/// Iterates over canonical txs.
pub struct CanonicalIter<'g, A, C> {
tx_graph: &'g TxGraph<A>,
chain: &'g C,
chain_tip: BlockId,
+ unprocessed_assumed_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>)> + 'g>,
unprocessed_anchored_txs:
Box<dyn Iterator<Item = (Txid, Arc<Transaction>, &'g BTreeSet<A>)> + 'g>,
unprocessed_seen_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>, u64)> + 'g>,
impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> {
/// Constructs [`CanonicalIter`].
- pub fn new(tx_graph: &'g TxGraph<A>, chain: &'g C, chain_tip: BlockId) -> Self {
+ pub fn new(
+ tx_graph: &'g TxGraph<A>,
+ chain: &'g C,
+ chain_tip: BlockId,
+ params: CanonicalizationParams,
+ ) -> Self {
let anchors = tx_graph.all_anchors();
+ let unprocessed_assumed_txs = Box::new(
+ params
+ .assume_canonical
+ .into_iter()
+ .rev()
+ .filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))),
+ );
let unprocessed_anchored_txs = Box::new(
tx_graph
.txids_by_descending_anchor_height()
tx_graph,
chain,
chain_tip,
+ unprocessed_assumed_txs,
unprocessed_anchored_txs,
unprocessed_seen_txs,
unprocessed_leftover_txs: VecDeque::new(),
return Some(Ok((txid, tx, reason)));
}
+ if let Some((txid, tx)) = self.unprocessed_assumed_txs.next() {
+ if !self.is_canonicalized(txid) {
+ self.mark_canonical(txid, tx, CanonicalReason::assumed());
+ }
+ }
+
if let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() {
if !self.is_canonicalized(txid) {
if let Err(err) = self.scan_anchors(txid, tx, anchors) {
/// The reason why a transaction is canonical.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CanonicalReason<A> {
+ /// This transaction is explicitly assumed to be canonical by the caller, superceding all other
+ /// canonicalization rules.
+ Assumed {
+ /// Whether it is a descendant that is assumed to be canonical.
+ descendant: Option<Txid>,
+ },
/// This transaction is anchored in the best chain by `A`, and therefore canonical.
Anchor {
/// The anchor that anchored the transaction in the chain.
}
impl<A: Clone> CanonicalReason<A> {
+ /// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other
+ /// transactions.
+ pub fn assumed() -> Self {
+ Self::Assumed { descendant: None }
+ }
+
/// Constructs a [`CanonicalReason`] from an `anchor`.
pub fn from_anchor(anchor: A) -> Self {
Self::Anchor {
/// descendant, but is transitively relevant.
pub fn to_transitive(&self, descendant: Txid) -> Self {
match self {
+ CanonicalReason::Assumed { .. } => Self::Assumed {
+ descendant: Some(descendant),
+ },
CanonicalReason::Anchor { anchor, .. } => Self::Anchor {
anchor: anchor.clone(),
descendant: Some(descendant),
/// descendant.
pub fn descendant(&self) -> &Option<Txid> {
match self {
+ CanonicalReason::Assumed { descendant, .. } => descendant,
CanonicalReason::Anchor { descendant, .. } => descendant,
CanonicalReason::ObservedIn { descendant, .. } => descendant,
}
use crate::BlockId;
use crate::CanonicalIter;
use crate::CanonicalReason;
+use crate::CanonicalizationParams;
use crate::ObservedIn;
use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge};
use alloc::collections::vec_deque::VecDeque;
&'a self,
chain: &'a C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
) -> impl Iterator<Item = Result<CanonicalTx<'a, Arc<Transaction>, A>, C::Error>> {
- self.canonical_iter(chain, chain_tip).flat_map(move |res| {
- res.map(|(txid, _, canonical_reason)| {
- let tx_node = self.get_tx_node(txid).expect("must contain tx");
- let chain_position = match canonical_reason {
- CanonicalReason::Anchor { anchor, descendant } => match descendant {
- Some(_) => {
- let direct_anchor = tx_node
- .anchors
- .iter()
- .find_map(|a| -> Option<Result<A, C::Error>> {
- match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
- Ok(Some(true)) => Some(Ok(a.clone())),
- Ok(Some(false)) | Ok(None) => None,
- Err(err) => Some(Err(err)),
- }
- })
- .transpose()?;
- match direct_anchor {
+ fn find_direct_anchor<A: Anchor, C: ChainOracle>(
+ tx_node: &TxNode<'_, Arc<Transaction>, A>,
+ chain: &C,
+ chain_tip: BlockId,
+ ) -> Result<Option<A>, C::Error> {
+ tx_node
+ .anchors
+ .iter()
+ .find_map(|a| -> Option<Result<A, C::Error>> {
+ match chain.is_block_in_chain(a.anchor_block(), chain_tip) {
+ Ok(Some(true)) => Some(Ok(a.clone())),
+ Ok(Some(false)) | Ok(None) => None,
+ Err(err) => Some(Err(err)),
+ }
+ })
+ .transpose()
+ }
+ self.canonical_iter(chain, chain_tip, params)
+ .flat_map(move |res| {
+ res.map(|(txid, _, canonical_reason)| {
+ let tx_node = self.get_tx_node(txid).expect("must contain tx");
+ let chain_position = match canonical_reason {
+ CanonicalReason::Assumed { descendant } => match descendant {
+ Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
+ Some(anchor) => ChainPosition::Confirmed {
+ anchor,
+ transitively: None,
+ },
+ None => ChainPosition::Unconfirmed {
+ last_seen: tx_node.last_seen_unconfirmed,
+ },
+ },
+ None => ChainPosition::Unconfirmed {
+ last_seen: tx_node.last_seen_unconfirmed,
+ },
+ },
+ CanonicalReason::Anchor { anchor, descendant } => match descendant {
+ Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? {
Some(anchor) => ChainPosition::Confirmed {
anchor,
transitively: None,
anchor,
transitively: descendant,
},
- }
- }
- None => ChainPosition::Confirmed {
- anchor,
- transitively: None,
+ },
+ None => ChainPosition::Confirmed {
+ anchor,
+ transitively: None,
+ },
},
- },
- CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
- ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
- last_seen: Some(last_seen),
+ CanonicalReason::ObservedIn { observed_in, .. } => match observed_in {
+ ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed {
+ last_seen: Some(last_seen),
+ },
+ ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
},
- ObservedIn::Block(_) => ChainPosition::Unconfirmed { last_seen: None },
- },
- };
- Ok(CanonicalTx {
- chain_position,
- tx_node,
+ };
+ Ok(CanonicalTx {
+ chain_position,
+ tx_node,
+ })
})
})
- })
}
/// List graph transactions that are in `chain` with `chain_tip`.
&'a self,
chain: &'a C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
- self.try_list_canonical_txs(chain, chain_tip)
+ self.try_list_canonical_txs(chain, chain_tip, params)
.map(|res| res.expect("infallible"))
}
&'a self,
chain: &'a C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
let mut canon_txs = HashMap::<Txid, CanonicalTx<Arc<Transaction>, A>>::new();
let mut canon_spends = HashMap::<OutPoint, Txid>::new();
- for r in self.try_list_canonical_txs(chain, chain_tip) {
+ for r in self.try_list_canonical_txs(chain, chain_tip, params) {
let canonical_tx = r?;
let txid = canonical_tx.tx_node.txid;
&'a self,
chain: &'a C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
) -> CanonicalIter<'a, A, C> {
- CanonicalIter::new(self, chain, chain_tip)
+ CanonicalIter::new(self, chain, chain_tip, params)
}
/// Get a filtered list of outputs from the given `outpoints` that are in `chain` with
&'a self,
chain: &'a C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
- self.try_filter_chain_txouts(chain, chain_tip, outpoints)
+ self.try_filter_chain_txouts(chain, chain_tip, params, outpoints)
.expect("oracle is infallible")
}
&'a self,
chain: &'a C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
outpoints: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
Ok(self
- .try_filter_chain_txouts(chain, chain_tip, outpoints)?
+ .try_filter_chain_txouts(chain, chain_tip, params, outpoints)?
.filter(|(_, full_txo)| full_txo.spent_by.is_none()))
}
&'a self,
chain: &'a C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
txouts: impl IntoIterator<Item = (OI, OutPoint)> + 'a,
) -> impl Iterator<Item = (OI, FullTxOut<A>)> + 'a {
- self.try_filter_chain_unspents(chain, chain_tip, txouts)
+ self.try_filter_chain_unspents(chain, chain_tip, params, txouts)
.expect("oracle is infallible")
}
&self,
chain: &C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
) -> Result<Balance, C::Error> {
let mut untrusted_pending = Amount::ZERO;
let mut confirmed = Amount::ZERO;
- for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, outpoints)? {
+ for (spk_i, txout) in self.try_filter_chain_unspents(chain, chain_tip, params, outpoints)? {
match &txout.chain_position {
ChainPosition::Confirmed { .. } => {
if txout.is_confirmed_and_spendable(chain_tip.height) {
&self,
chain: &C,
chain_tip: BlockId,
+ params: CanonicalizationParams,
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
) -> Balance {
- self.try_balance(chain, chain_tip, outpoints, trust_predicate)
+ self.try_balance(chain, chain_tip, params, outpoints, trust_predicate)
.expect("oracle is infallible")
}
I: fmt::Debug + Clone + Ord + 'a,
{
let indexer = indexer.as_ref();
- self.try_list_canonical_txs(chain, chain_tip).flat_map(
- move |res| -> Vec<Result<(ScriptBuf, Txid), C::Error>> {
+ self.try_list_canonical_txs(chain, chain_tip, CanonicalizationParams::default())
+ .flat_map(move |res| -> Vec<Result<(ScriptBuf, Txid), C::Error>> {
let range = &spk_index_range;
let c_tx = match res {
Ok(c_tx) => c_tx,
.filter(|(i, _)| range.contains(i))
.map(|(_, spk)| Ok((spk, c_tx.tx_node.txid)))
.collect()
- },
- )
+ })
}
/// List txids that are expected to exist under the given spks.
use rand::distributions::{Alphanumeric, DistString};
use std::collections::HashMap;
-use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor};
+use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor, CanonicalizationParams};
use bitcoin::{
locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf,
Sequence, Transaction, TxIn, TxOut, Txid, Witness,
pub outputs: &'a [TxOutTemplate],
pub anchors: &'a [A],
pub last_seen: Option<u64>,
+ pub assume_canonical: bool,
}
#[allow(dead_code)]
}
}
+#[allow(dead_code)]
+pub struct TxTemplateEnv<'a, A> {
+ pub tx_graph: TxGraph<A>,
+ pub indexer: SpkTxOutIndex<u32>,
+ pub txid_to_name: HashMap<&'a str, Txid>,
+ pub canonicalization_params: CanonicalizationParams,
+}
+
#[allow(dead_code)]
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
-) -> (TxGraph<A>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
+) -> TxTemplateEnv<'a, A> {
let (descriptor, _) =
Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[2]).unwrap();
- let mut graph = TxGraph::<A>::default();
- let mut spk_index = SpkTxOutIndex::default();
+ let mut tx_graph = TxGraph::<A>::default();
+ let mut indexer = SpkTxOutIndex::default();
(0..10).for_each(|index| {
- spk_index.insert_spk(
+ indexer.insert_spk(
index,
descriptor
.at_derivation_index(index)
.script_pubkey(),
);
});
- let mut tx_ids = HashMap::<&'a str, Txid>::new();
+ let mut txid_to_name = HashMap::<&'a str, Txid>::new();
+ let mut canonicalization_params = CanonicalizationParams::default();
for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() {
let tx = Transaction {
version: transaction::Version::non_standard(0),
witness: Witness::new(),
},
TxInTemplate::PrevTx(prev_name, prev_vout) => {
- let prev_txid = tx_ids.get(prev_name).expect(
+ let prev_txid = txid_to_name.get(prev_name).expect(
"txin template must spend from tx of template that comes before",
);
TxIn {
},
Some(index) => TxOut {
value: Amount::from_sat(output.value),
- script_pubkey: spk_index.spk_at_index(index).unwrap(),
+ script_pubkey: indexer.spk_at_index(index).unwrap(),
},
})
.collect(),
};
- tx_ids.insert(tx_tmp.tx_name, tx.compute_txid());
- spk_index.scan(&tx);
- let _ = graph.insert_tx(tx.clone());
+ let txid = tx.compute_txid();
+ if tx_tmp.assume_canonical {
+ canonicalization_params.assume_canonical.push(txid);
+ }
+ txid_to_name.insert(tx_tmp.tx_name, txid);
+ indexer.scan(&tx);
+ let _ = tx_graph.insert_tx(tx.clone());
for anchor in tx_tmp.anchors.iter() {
- let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone());
+ let _ = tx_graph.insert_anchor(txid, anchor.clone());
}
if let Some(last_seen) = tx_tmp.last_seen {
- let _ = graph.insert_seen_at(tx.compute_txid(), last_seen);
+ let _ = tx_graph.insert_seen_at(txid, last_seen);
}
}
- (graph, spk_index, tx_ids)
+ TxTemplateEnv {
+ tx_graph,
+ indexer,
+ txid_to_name,
+ canonicalization_params,
+ }
}
indexed_tx_graph::{self, IndexedTxGraph},
indexer::keychain_txout::KeychainTxOutIndex,
local_chain::LocalChain,
- tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt,
+ tx_graph, Balance, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, DescriptorExt,
};
use bdk_testenv::{
block_id, hash,
.filter_chain_txouts(
&local_chain,
chain_tip,
+ CanonicalizationParams::default(),
graph.index.outpoints().iter().cloned(),
)
.collect::<Vec<_>>();
.filter_chain_unspents(
&local_chain,
chain_tip,
+ CanonicalizationParams::default(),
graph.index.outpoints().iter().cloned(),
)
.collect::<Vec<_>>();
let balance = graph.graph().balance(
&local_chain,
chain_tip,
+ CanonicalizationParams::default(),
graph.index.outpoints().iter().cloned(),
|_, spk: ScriptBuf| trusted_spks.contains(&spk),
);
// check chain position
let chain_pos = graph
.graph()
- .list_canonical_txs(chain, chain.tip().block_id())
+ .list_canonical_txs(
+ chain,
+ chain.tip().block_id(),
+ CanonicalizationParams::default(),
+ )
.find_map(|canon_tx| {
if canon_tx.tx_node.txid == txid {
Some(canon_tx.chain_position)
#[macro_use]
mod common;
-use bdk_chain::{collections::*, BlockId, ConfirmationBlockTime};
+use bdk_chain::{collections::*, BlockId, CanonicalizationParams, ConfirmationBlockTime};
use bdk_chain::{
local_chain::LocalChain,
tx_graph::{self, CalculateFeeError},
.filter_chain_txouts(
chain,
tip.block_id(),
+ CanonicalizationParams::default(),
tx_graph.all_txouts().map(|(op, _)| ((), op)),
)
.filter_map(|(_, full_txo)| Some((full_txo.outpoint, full_txo.spent_by?)))
tx_graph: &TxGraph<ConfirmationBlockTime>|
-> HashMap<Txid, ChainPosition<ConfirmationBlockTime>> {
tx_graph
- .list_canonical_txs(chain, tip.block_id())
+ .list_canonical_txs(chain, tip.block_id(), CanonicalizationParams::default())
.map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position))
.collect()
};
.collect();
let chain = LocalChain::from_blocks(blocks).unwrap();
let canonical_txs: Vec<_> = graph
- .list_canonical_txs(&chain, chain.tip().block_id())
+ .list_canonical_txs(
+ &chain,
+ chain.tip().block_id(),
+ CanonicalizationParams::default(),
+ )
.collect();
assert!(canonical_txs.is_empty());
// tx0 with seen_at should be returned by canonical txs
let _ = graph.insert_seen_at(txids[0], 2);
- let mut canonical_txs = graph.list_canonical_txs(&chain, chain.tip().block_id());
+ let mut canonical_txs = graph.list_canonical_txs(
+ &chain,
+ chain.tip().block_id(),
+ CanonicalizationParams::default(),
+ );
assert_eq!(
canonical_txs.next().map(|tx| tx.tx_node.txid).unwrap(),
txids[0]
// tx1 with anchor is also canonical
let _ = graph.insert_anchor(txids[1], block_id!(2, "B"));
let canonical_txids: Vec<_> = graph
- .list_canonical_txs(&chain, chain.tip().block_id())
+ .list_canonical_txs(
+ &chain,
+ chain.tip().block_id(),
+ CanonicalizationParams::default(),
+ )
.map(|tx| tx.tx_node.txid)
.collect();
assert!(canonical_txids.contains(&txids[1]));
outputs: &[TxOutTemplate::new(10000, Some(1))],
anchors: &[block_id!(1, "A")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "tx2",
..Default::default()
},
];
- let (graph, _, _) = init_graph(&template);
+ let graph = init_graph(&template).tx_graph;
let new_graph = graph.clone().map_anchors(|a| NonDeterministicAnchor {
anchor_block: a,
// A non-deterministic value
outputs: &[TxOutTemplate::new(10000, Some(1))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "unconfirmed_conflict",
outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "tx_conflict_1",
outputs: &[TxOutTemplate::new(10000, Some(0))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "tx_conflict_1",
outputs: &[TxOutTemplate::new(10000, Some(0))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "tx_conflict_1",
outputs: &[TxOutTemplate::new(30000, Some(2))],
anchors: &[block_id!(4, "Orphaned Block")],
last_seen: Some(300),
+ ..Default::default()
},
],
exp_chain_txs: HashSet::from(["tx1", "tx_orphaned_conflict"]),
outputs: &[TxOutTemplate::new(10000, Some(0))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "tx_conflict_1",
outputs: &[TxOutTemplate::new(30000, Some(2))],
anchors: &[block_id!(4, "Orphaned Block")],
last_seen: Some(100),
+ ..Default::default()
},
],
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1"]),
outputs: &[TxOutTemplate::new(10000, Some(0))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "tx_conflict_1",
outputs: &[TxOutTemplate::new(10000, Some(0))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "B",
outputs: &[TxOutTemplate::new(10000, Some(0))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "B",
outputs: &[TxOutTemplate::new(10000, Some(0))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "B",
outputs: &[TxOutTemplate::new(10000, Some(0))],
anchors: &[block_id!(1, "B")],
last_seen: None,
+ ..Default::default()
},
TxTemplate {
tx_name: "B",
exp_unspents: HashSet::from([("B", 0)]),
exp_balance: Balance { trusted_pending: Amount::from_sat(8_000), ..Default::default() },
},
+ Scenario {
+ name: "assume-canonical-tx displaces unconfirmed chain",
+ tx_templates: &[
+ TxTemplate {
+ tx_name: "root",
+ inputs: &[TxInTemplate::Bogus],
+ outputs: &[
+ TxOutTemplate::new(21_000, Some(0)),
+ TxOutTemplate::new(21_000, Some(1)),
+ ],
+ anchors: &[block_id!(1, "B")],
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "unconfirmed",
+ inputs: &[TxInTemplate::PrevTx("root", 0)],
+ outputs: &[TxOutTemplate::new(20_000, Some(1))],
+ last_seen: Some(2),
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "unconfirmed_descendant",
+ inputs: &[
+ TxInTemplate::PrevTx("unconfirmed", 0),
+ TxInTemplate::PrevTx("root", 1),
+ ],
+ outputs: &[TxOutTemplate::new(28_000, Some(2))],
+ last_seen: Some(2),
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "assume_canonical",
+ inputs: &[TxInTemplate::PrevTx("root", 0)],
+ outputs: &[TxOutTemplate::new(19_000, Some(3))],
+ assume_canonical: true,
+ ..Default::default()
+ },
+ ],
+ exp_chain_txs: HashSet::from(["root", "assume_canonical"]),
+ exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]),
+ exp_unspents: HashSet::from([("root", 1), ("assume_canonical", 0)]),
+ exp_balance: Balance {
+ immature: Amount::ZERO,
+ trusted_pending: Amount::from_sat(19_000),
+ untrusted_pending: Amount::ZERO,
+ confirmed: Amount::from_sat(21_000),
+ },
+ },
+ Scenario {
+ name: "assume-canonical-tx displaces confirmed chain",
+ tx_templates: &[
+ TxTemplate {
+ tx_name: "root",
+ inputs: &[TxInTemplate::Bogus],
+ outputs: &[
+ TxOutTemplate::new(21_000, Some(0)),
+ TxOutTemplate::new(21_000, Some(1)),
+ ],
+ anchors: &[block_id!(1, "B")],
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "confirmed",
+ inputs: &[TxInTemplate::PrevTx("root", 0)],
+ outputs: &[TxOutTemplate::new(20_000, Some(1))],
+ anchors: &[block_id!(2, "C")],
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "confirmed_descendant",
+ inputs: &[
+ TxInTemplate::PrevTx("confirmed", 0),
+ TxInTemplate::PrevTx("root", 1),
+ ],
+ outputs: &[TxOutTemplate::new(28_000, Some(2))],
+ anchors: &[block_id!(3, "D")],
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "assume_canonical",
+ inputs: &[TxInTemplate::PrevTx("root", 0)],
+ outputs: &[TxOutTemplate::new(19_000, Some(3))],
+ assume_canonical: true,
+ ..Default::default()
+ },
+ ],
+ exp_chain_txs: HashSet::from(["root", "assume_canonical"]),
+ exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]),
+ exp_unspents: HashSet::from([("root", 1), ("assume_canonical", 0)]),
+ exp_balance: Balance {
+ immature: Amount::ZERO,
+ trusted_pending: Amount::from_sat(19_000),
+ untrusted_pending: Amount::ZERO,
+ confirmed: Amount::from_sat(21_000),
+ },
+ },
+ Scenario {
+ name: "assume-canonical txs respects order",
+ tx_templates: &[
+ TxTemplate {
+ tx_name: "root",
+ inputs: &[TxInTemplate::Bogus],
+ outputs: &[
+ TxOutTemplate::new(21_000, Some(0)),
+ ],
+ anchors: &[block_id!(1, "B")],
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "assume_a",
+ inputs: &[TxInTemplate::PrevTx("root", 0)],
+ outputs: &[TxOutTemplate::new(20_000, Some(1))],
+ assume_canonical: true,
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "assume_b",
+ inputs: &[TxInTemplate::PrevTx("root", 0)],
+ outputs: &[TxOutTemplate::new(19_000, Some(1))],
+ assume_canonical: true,
+ ..Default::default()
+ },
+ TxTemplate {
+ tx_name: "assume_c",
+ inputs: &[TxInTemplate::PrevTx("root", 0)],
+ outputs: &[TxOutTemplate::new(18_000, Some(1))],
+ assume_canonical: true,
+ ..Default::default()
+ },
+ ],
+ exp_chain_txs: HashSet::from(["root", "assume_c"]),
+ exp_chain_txouts: HashSet::from([("root", 0), ("assume_c", 0)]),
+ exp_unspents: HashSet::from([("assume_c", 0)]),
+ exp_balance: Balance {
+ immature: Amount::ZERO,
+ trusted_pending: Amount::from_sat(18_000),
+ untrusted_pending: Amount::ZERO,
+ confirmed: Amount::ZERO,
+ },
+ },
];
for scenario in scenarios {
- let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter());
+ let env = init_graph(scenario.tx_templates.iter());
- let txs = tx_graph
- .list_canonical_txs(&local_chain, chain_tip)
+ let txs = env
+ .tx_graph
+ .list_canonical_txs(&local_chain, chain_tip, env.canonicalization_params.clone())
.map(|tx| tx.tx_node.txid)
.collect::<BTreeSet<_>>();
let exp_txs = scenario
.exp_chain_txs
.iter()
- .map(|txid| *exp_tx_ids.get(txid).expect("txid must exist"))
+ .map(|txid| *env.txid_to_name.get(txid).expect("txid must exist"))
.collect::<BTreeSet<_>>();
assert_eq!(
txs, exp_txs,
scenario.name
);
- let txouts = tx_graph
+ let txouts = env
+ .tx_graph
.filter_chain_txouts(
&local_chain,
chain_tip,
- spk_index.outpoints().iter().cloned(),
+ env.canonicalization_params.clone(),
+ env.indexer.outpoints().iter().cloned(),
)
.map(|(_, full_txout)| full_txout.outpoint)
.collect::<BTreeSet<_>>();
.exp_chain_txouts
.iter()
.map(|(txid, vout)| OutPoint {
- txid: *exp_tx_ids.get(txid).expect("txid must exist"),
+ txid: *env.txid_to_name.get(txid).expect("txid must exist"),
vout: *vout,
})
.collect::<BTreeSet<_>>();
scenario.name
);
- let utxos = tx_graph
+ let utxos = env
+ .tx_graph
.filter_chain_unspents(
&local_chain,
chain_tip,
- spk_index.outpoints().iter().cloned(),
+ env.canonicalization_params.clone(),
+ env.indexer.outpoints().iter().cloned(),
)
.map(|(_, full_txout)| full_txout.outpoint)
.collect::<BTreeSet<_>>();
.exp_unspents
.iter()
.map(|(txid, vout)| OutPoint {
- txid: *exp_tx_ids.get(txid).expect("txid must exist"),
+ txid: *env.txid_to_name.get(txid).expect("txid must exist"),
vout: *vout,
})
.collect::<BTreeSet<_>>();
scenario.name
);
- let balance = tx_graph.balance(
+ let balance = env.tx_graph.balance(
&local_chain,
chain_tip,
- spk_index.outpoints().iter().cloned(),
- |_, spk: ScriptBuf| spk_index.index_of_spk(spk).is_some(),
+ env.canonicalization_params.clone(),
+ env.indexer.outpoints().iter().cloned(),
+ |_, spk: ScriptBuf| env.indexer.index_of_spk(spk).is_some(),
);
assert_eq!(
balance, scenario.exp_balance,
local_chain::LocalChain,
spk_client::{FullScanRequest, SyncRequest, SyncResponse},
spk_txout::SpkTxOutIndex,
- Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph,
+ Balance, CanonicalizationParams, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge,
+ TxGraph,
};
use bdk_core::bitcoin::{
key::{Secp256k1, UntweakedPublicKey},
) -> anyhow::Result<Balance> {
let chain_tip = recv_chain.tip().block_id();
let outpoints = recv_graph.index.outpoints().clone();
- let balance = recv_graph
- .graph()
- .balance(recv_chain, chain_tip, outpoints, |_, _| true);
+ let balance = recv_graph.graph().balance(
+ recv_chain,
+ chain_tip,
+ CanonicalizationParams::default(),
+ outpoints,
+ |_, _| true,
+ );
Ok(balance)
}
};
use bdk_chain::{
bitcoin::{Block, Transaction},
- local_chain, Merge,
+ local_chain, CanonicalizationParams, Merge,
};
use example_cli::{
anyhow,
graph.graph().balance(
&*chain,
synced_to.block_id(),
+ CanonicalizationParams::default(),
graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal,
)
graph.graph().balance(
&*chain,
synced_to.block_id(),
+ CanonicalizationParams::default(),
graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal,
)
psbt::PsbtExt,
Descriptor, DescriptorPublicKey, ForEachKey,
};
+use bdk_chain::CanonicalizationParams;
use bdk_chain::ConfirmationBlockTime;
use bdk_chain::{
indexed_tx_graph,
let outpoints = graph.index.outpoints();
graph
.graph()
- .try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned())?
+ .try_filter_chain_unspents(
+ chain,
+ chain_tip,
+ CanonicalizationParams::default(),
+ outpoints.iter().cloned(),
+ )?
.filter_map(|((k, i), full_txo)| -> Option<Result<PlanUtxo, _>> {
let desc = graph
.index
let balance = graph.graph().try_balance(
chain,
chain.get_chain_tip()?,
+ CanonicalizationParams::default(),
graph.index.outpoints().iter().cloned(),
|(k, _), _| k == &Keychain::Internal,
)?;
} => {
let txouts = graph
.graph()
- .try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned())?
+ .try_filter_chain_txouts(
+ chain,
+ chain_tip,
+ CanonicalizationParams::default(),
+ outpoints.iter().cloned(),
+ )?
.filter(|(_, full_txo)| match (spent, unspent) {
(true, false) => full_txo.spent_by.is_some(),
(false, true) => full_txo.spent_by.is_none(),
collections::BTreeSet,
indexed_tx_graph,
spk_client::{FullScanRequest, SyncRequest},
- ConfirmationBlockTime, Merge,
+ CanonicalizationParams, ConfirmationBlockTime, Merge,
};
use bdk_electrum::{
electrum_client::{self, Client, ElectrumApi},
.filter_chain_unspents(
&*chain,
chain_tip.block_id(),
+ CanonicalizationParams::default(),
init_outpoints.iter().cloned(),
)
.map(|(_, utxo)| utxo.outpoint),
request = request.txids(
graph
.graph()
- .list_canonical_txs(&*chain, chain_tip.block_id())
+ .list_canonical_txs(
+ &*chain,
+ chain_tip.block_id(),
+ CanonicalizationParams::default(),
+ )
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
.map(|canonical_tx| canonical_tx.tx_node.txid),
);
bitcoin::Network,
keychain_txout::FullScanRequestBuilderExt,
spk_client::{FullScanRequest, SyncRequest},
- Merge,
+ CanonicalizationParams, Merge,
};
use bdk_esplora::{esplora_client, EsploraExt};
use example_cli::{
.filter_chain_unspents(
&*chain,
local_tip.block_id(),
+ CanonicalizationParams::default(),
init_outpoints.iter().cloned(),
)
.map(|(_, utxo)| utxo.outpoint),
request = request.txids(
graph
.graph()
- .list_canonical_txs(&*chain, local_tip.block_id())
+ .list_canonical_txs(
+ &*chain,
+ local_tip.block_id(),
+ CanonicalizationParams::default(),
+ )
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
.map(|canonical_tx| canonical_tx.tx_node.txid),
);