From 5c42b61eadd58e29f011b4de7aa629dc9d2f5fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Tue, 8 Oct 2024 09:15:55 +0000 Subject: [PATCH 1/4] feat(chain,wallet)!: rm `ConfirmationTime` We rm `ConfirmationTime` because it is essentially the same thing as `ChainPosition` without the block hash. We also impl `serde::Deserialize` and `serde::Serialize` for `ChainPosition`. --- crates/chain/src/chain_data.rs | 53 ++------ crates/wallet/src/types.rs | 6 +- crates/wallet/src/wallet/coin_selection.rs | 105 ++++++++------- crates/wallet/src/wallet/mod.rs | 39 +++--- crates/wallet/src/wallet/tx_builder.rs | 14 +- crates/wallet/tests/common.rs | 43 +++---- crates/wallet/tests/wallet.rs | 142 +++++++++------------ 7 files changed, 179 insertions(+), 223 deletions(-) diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index ce6076c51..e0202e1af 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -1,4 +1,3 @@ -use crate::ConfirmationBlockTime; use bitcoin::{OutPoint, TxOut, Txid}; use crate::{Anchor, COINBASE_MATURITY}; @@ -7,6 +6,14 @@ use crate::{Anchor, COINBASE_MATURITY}; /// /// The generic `A` should be a [`Anchor`] implementation. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(bound( + deserialize = "A: Ord + serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize", + )) +)] pub enum ChainPosition { /// The chain data is seen as confirmed, and in anchored by `A`. Confirmed(A), @@ -41,48 +48,6 @@ impl ChainPosition { } } -/// Block height and timestamp at which a transaction is confirmed. -#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum ConfirmationTime { - /// The transaction is confirmed - Confirmed { - /// Confirmation height. - height: u32, - /// Confirmation time in unix seconds. - time: u64, - }, - /// The transaction is unconfirmed - Unconfirmed { - /// The last-seen timestamp in unix seconds. - last_seen: u64, - }, -} - -impl ConfirmationTime { - /// Construct an unconfirmed variant using the given `last_seen` time in unix seconds. - pub fn unconfirmed(last_seen: u64) -> Self { - Self::Unconfirmed { last_seen } - } - - /// Returns whether [`ConfirmationTime`] is the confirmed variant. - pub fn is_confirmed(&self) -> bool { - matches!(self, Self::Confirmed { .. }) - } -} - -impl From> for ConfirmationTime { - fn from(observed_as: ChainPosition) -> Self { - match observed_as { - ChainPosition::Confirmed(a) => Self::Confirmed { - height: a.block_id.height, - time: a.confirmation_time, - }, - ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen }, - } - } -} - /// A `TxOut` with as much data as we can retrieve about it #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct FullTxOut { @@ -159,6 +124,8 @@ impl FullTxOut { #[cfg(test)] mod test { + use bdk_core::ConfirmationBlockTime; + use crate::BlockId; use super::*; diff --git a/crates/wallet/src/types.rs b/crates/wallet/src/types.rs index 6ed17b575..c4626fbf3 100644 --- a/crates/wallet/src/types.rs +++ b/crates/wallet/src/types.rs @@ -10,9 +10,9 @@ // licenses. use alloc::boxed::Box; +use chain::{ChainPosition, ConfirmationBlockTime}; use core::convert::AsRef; -use bdk_chain::ConfirmationTime; use bitcoin::transaction::{OutPoint, Sequence, TxOut}; use bitcoin::{psbt, Weight}; @@ -61,8 +61,8 @@ pub struct LocalOutput { pub is_spent: bool, /// The derivation index for the script pubkey in the wallet pub derivation_index: u32, - /// The confirmation time for transaction containing this utxo - pub confirmation_time: ConfirmationTime, + /// The position of the output in the blockchain. + pub chain_position: ChainPosition, } /// A [`Utxo`] with its `satisfaction_weight`. diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 381ff65b2..0f0e4a88e 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -278,7 +278,7 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { // For utxo that doesn't exist in DB, they will have lowest priority to be selected let utxos = { optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo { - Utxo::Local(local) => Some(local.confirmation_time), + Utxo::Local(local) => Some(local.chain_position), Utxo::Foreign { .. } => None, }); @@ -733,11 +733,12 @@ where #[cfg(test)] mod test { use assert_matches::assert_matches; + use bitcoin::hashes::Hash; + use chain::{BlockId, ChainPosition, ConfirmationBlockTime}; use core::str::FromStr; use rand::rngs::StdRng; - use bdk_chain::ConfirmationTime; - use bitcoin::{Amount, ScriptBuf, TxIn, TxOut}; + use bitcoin::{Amount, BlockHash, ScriptBuf, TxIn, TxOut}; use super::*; use crate::types::*; @@ -752,7 +753,34 @@ mod test { const FEE_AMOUNT: u64 = 50; - fn utxo(value: u64, index: u32, confirmation_time: ConfirmationTime) -> WeightedUtxo { + fn unconfirmed_utxo(value: u64, index: u32, last_seen: u64) -> WeightedUtxo { + utxo(value, index, ChainPosition::Unconfirmed(last_seen)) + } + + fn confirmed_utxo( + value: u64, + index: u32, + confirmation_height: u32, + confirmation_time: u64, + ) -> WeightedUtxo { + utxo( + value, + index, + ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: chain::BlockId { + height: confirmation_height, + hash: bitcoin::BlockHash::all_zeros(), + }, + confirmation_time, + }), + ) + } + + fn utxo( + value: u64, + index: u32, + chain_position: ChainPosition, + ) -> WeightedUtxo { assert!(index < 10); let outpoint = OutPoint::from_str(&format!( "000000000000000000000000000000000000000000000000000000000000000{}:0", @@ -770,49 +798,24 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: 42, - confirmation_time, + chain_position, }), } } fn get_test_utxos() -> Vec { vec![ - utxo(100_000, 0, ConfirmationTime::Unconfirmed { last_seen: 0 }), - utxo( - FEE_AMOUNT - 40, - 1, - ConfirmationTime::Unconfirmed { last_seen: 0 }, - ), - utxo(200_000, 2, ConfirmationTime::Unconfirmed { last_seen: 0 }), + unconfirmed_utxo(100_000, 0, 0), + unconfirmed_utxo(FEE_AMOUNT - 40, 1, 0), + unconfirmed_utxo(200_000, 2, 0), ] } fn get_oldest_first_test_utxos() -> Vec { // ensure utxos are from different tx - let utxo1 = utxo( - 120_000, - 1, - ConfirmationTime::Confirmed { - height: 1, - time: 1231006505, - }, - ); - let utxo2 = utxo( - 80_000, - 2, - ConfirmationTime::Confirmed { - height: 2, - time: 1231006505, - }, - ); - let utxo3 = utxo( - 300_000, - 3, - ConfirmationTime::Confirmed { - height: 3, - time: 1231006505, - }, - ); + let utxo1 = confirmed_utxo(120_000, 1, 1, 1231006505); + let utxo2 = confirmed_utxo(80_000, 2, 2, 1231006505); + let utxo3 = confirmed_utxo(300_000, 3, 3, 1231006505); vec![utxo1, utxo2, utxo3] } @@ -834,13 +837,16 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: rng.next_u32(), - confirmation_time: if rng.gen_bool(0.5) { - ConfirmationTime::Confirmed { - height: rng.next_u32(), - time: rng.next_u64(), - } + chain_position: if rng.gen_bool(0.5) { + ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: chain::BlockId { + height: rng.next_u32(), + hash: BlockHash::all_zeros(), + }, + confirmation_time: rng.next_u64(), + }) } else { - ConfirmationTime::Unconfirmed { last_seen: 0 } + ChainPosition::Unconfirmed(0) }, }), }); @@ -865,7 +871,7 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: 42, - confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, + chain_position: ChainPosition::Unconfirmed(0), }), }) .collect() @@ -1222,7 +1228,7 @@ mod test { optional.push(utxo( 500_000, 3, - ConfirmationTime::Unconfirmed { last_seen: 0 }, + ChainPosition::::Unconfirmed(0), )); // Defensive assertions, for sanity and in case someone changes the test utxos vector. @@ -1584,10 +1590,13 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: 0, - confirmation_time: ConfirmationTime::Confirmed { - height: 12345, - time: 12345, - }, + chain_position: ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: BlockId { + height: 12345, + hash: BlockHash::all_zeros(), + }, + confirmation_time: 12345, + }), }), } } diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 19604f441..562e684c1 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -34,8 +34,8 @@ use bdk_chain::{ SyncResult, }, tx_graph::{CanonicalTx, TxGraph, TxNode, TxUpdate}, - BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, DescriptorExt, FullTxOut, - Indexed, IndexedTxGraph, Merge, + BlockId, ChainPosition, ConfirmationBlockTime, DescriptorExt, FullTxOut, Indexed, + IndexedTxGraph, Merge, }; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::{ @@ -1660,11 +1660,10 @@ impl Wallet { .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?; let txout = &prev_tx.output[txin.previous_output.vout as usize]; - let confirmation_time: ConfirmationTime = graph + let chain_position = graph .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid) .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))? - .cloned() - .into(); + .cloned(); let weighted_utxo = match txout_index.index_of_spk(txout.script_pubkey.clone()) { Some(&(keychain, derivation_index)) => { @@ -1679,7 +1678,7 @@ impl Wallet { keychain, is_spent: true, derivation_index, - confirmation_time, + chain_position, }), satisfaction_weight, } @@ -2051,33 +2050,33 @@ impl Wallet { Some(tx) => tx, None => return false, }; - let confirmation_time: ConfirmationTime = match self - .indexed_graph - .graph() - .get_chain_position(&self.chain, chain_tip, txid) - { - Some(chain_position) => chain_position.cloned().into(), + let chain_position = match self.indexed_graph.graph().get_chain_position( + &self.chain, + chain_tip, + txid, + ) { + Some(chain_position) => chain_position.cloned(), None => return false, }; // Whether the UTXO is mature and, if needed, confirmed let mut spendable = true; - if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() { + if must_only_use_confirmed_tx && !chain_position.is_confirmed() { return false; } if tx.is_coinbase() { debug_assert!( - confirmation_time.is_confirmed(), + chain_position.is_confirmed(), "coinbase must always be confirmed" ); if let Some(current_height) = current_height { - match confirmation_time { - ConfirmationTime::Confirmed { height, .. } => { + match chain_position { + ChainPosition::Confirmed(a) => { // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 - spendable &= - (current_height.saturating_sub(height)) >= COINBASE_MATURITY; + spendable &= (current_height.saturating_sub(a.block_id.height)) + >= COINBASE_MATURITY; } - ConfirmationTime::Unconfirmed { .. } => spendable = false, + ChainPosition::Unconfirmed { .. } => spendable = false, } } } @@ -2546,7 +2545,7 @@ fn new_local_utxo( outpoint: full_txo.outpoint, txout: full_txo.txout, is_spent: full_txo.spent_by.is_some(), - confirmation_time: full_txo.chain_position.into(), + chain_position: full_txo.chain_position, keychain, derivation_index, } diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs index c3dde7330..343734a8d 100644 --- a/crates/wallet/src/wallet/tx_builder.rs +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -858,7 +858,6 @@ mod test { }; } - use bdk_chain::ConfirmationTime; use bitcoin::consensus::deserialize; use bitcoin::hex::FromHex; use bitcoin::TxOut; @@ -1018,7 +1017,7 @@ mod test { txout: TxOut::NULL, keychain: KeychainKind::External, is_spent: false, - confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, + chain_position: chain::ChainPosition::Unconfirmed(0), derivation_index: 0, }, LocalOutput { @@ -1029,10 +1028,13 @@ mod test { txout: TxOut::NULL, keychain: KeychainKind::Internal, is_spent: false, - confirmation_time: ConfirmationTime::Confirmed { - height: 32, - time: 42, - }, + chain_position: chain::ChainPosition::Confirmed(chain::ConfirmationBlockTime { + block_id: chain::BlockId { + height: 32, + hash: bitcoin::BlockHash::all_zeros(), + }, + confirmation_time: 42, + }), derivation_index: 1, }, ] diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 375d680d1..a2870c807 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,5 +1,5 @@ #![allow(unused)] -use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; +use bdk_chain::{tx_graph, BlockId, ChainPosition, ConfirmationBlockTime, TxGraph}; use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet}; use bitcoin::{ hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, @@ -89,20 +89,26 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, insert_anchor_from_conf( &mut wallet, tx0.compute_txid(), - ConfirmationTime::Confirmed { - height: 1_000, - time: 100, - }, + ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: BlockId { + height: 1_000, + hash: BlockHash::all_zeros(), + }, + confirmation_time: 100, + }), ); wallet.insert_tx(tx1.clone()); insert_anchor_from_conf( &mut wallet, tx1.compute_txid(), - ConfirmationTime::Confirmed { - height: 2_000, - time: 200, - }, + ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: BlockId { + height: 2_000, + hash: BlockHash::all_zeros(), + }, + confirmation_time: 200, + }), ); (wallet, tx1.compute_txid()) @@ -205,19 +211,12 @@ pub fn feerate_unchecked(sat_vb: f64) -> FeeRate { /// Simulates confirming a tx with `txid` at the specified `position` by inserting an anchor /// at the lowest height in local chain that is greater or equal to `position`'s height, /// assuming the confirmation time matches `ConfirmationTime::Confirmed`. -pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: ConfirmationTime) { - if let ConfirmationTime::Confirmed { height, time } = position { - // anchor tx to checkpoint with lowest height that is >= position's height - let anchor = wallet - .local_chain() - .range(height..) - .last() - .map(|anchor_cp| ConfirmationBlockTime { - block_id: anchor_cp.block_id(), - confirmation_time: time, - }) - .expect("confirmation height cannot be greater than tip"); - +pub fn insert_anchor_from_conf( + wallet: &mut Wallet, + txid: Txid, + position: ChainPosition, +) { + if let ChainPosition::Confirmed(anchor) = position { wallet .apply_update(Update { tx_update: tx_graph::TxUpdate { diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 5914ce289..44b194a83 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use anyhow::Context; use assert_matches::assert_matches; use bdk_chain::{tx_graph, COINBASE_MATURITY}; -use bdk_chain::{BlockId, ConfirmationTime}; +use bdk_chain::{BlockId, ChainPosition, ConfirmationBlockTime}; use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; use bdk_wallet::error::CreateTxError; @@ -32,7 +32,11 @@ use rand::SeedableRng; mod common; use common::*; -fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint { +fn receive_output( + wallet: &mut Wallet, + value: u64, + height: ChainPosition, +) -> OutPoint { let addr = wallet.next_unused_address(KeychainKind::External).address; receive_output_to_address(wallet, addr, value, height) } @@ -41,7 +45,7 @@ fn receive_output_to_address( wallet: &mut Wallet, addr: Address, value: u64, - height: ConfirmationTime, + height: ChainPosition, ) -> OutPoint { let tx = Transaction { version: transaction::Version::ONE, @@ -57,10 +61,10 @@ fn receive_output_to_address( wallet.insert_tx(tx); match height { - ConfirmationTime::Confirmed { .. } => { + ChainPosition::Confirmed { .. } => { insert_anchor_from_conf(wallet, txid, height); } - ConfirmationTime::Unconfirmed { last_seen } => { + ChainPosition::Unconfirmed(last_seen) => { insert_seen_at(wallet, txid, last_seen); } } @@ -72,9 +76,12 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { let latest_cp = wallet.latest_checkpoint(); let height = latest_cp.height(); let anchor = if height == 0 { - ConfirmationTime::Unconfirmed { last_seen: 0 } + ChainPosition::Unconfirmed(0) } else { - ConfirmationTime::Confirmed { height, time: 0 } + ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: latest_cp.block_id(), + confirmation_time: 0, + }) }; receive_output(wallet, value, anchor) } @@ -1209,14 +1216,11 @@ fn test_create_tx_add_utxo() { }; let txid = small_output_tx.compute_txid(); wallet.insert_tx(small_output_tx); - insert_anchor_from_conf( - &mut wallet, - txid, - ConfirmationTime::Confirmed { - height: 2000, - time: 200, - }, - ); + let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: wallet.latest_checkpoint().get(2000).unwrap().block_id(), + confirmation_time: 200, + }); + insert_anchor_from_conf(&mut wallet, txid, chain_position); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() @@ -1259,14 +1263,11 @@ fn test_create_tx_manually_selected_insufficient() { }; let txid = small_output_tx.compute_txid(); wallet.insert_tx(small_output_tx.clone()); - insert_anchor_from_conf( - &mut wallet, - txid, - ConfirmationTime::Confirmed { - height: 2000, - time: 200, - }, - ); + let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: wallet.latest_checkpoint().get(2000).unwrap().block_id(), + confirmation_time: 200, + }); + insert_anchor_from_conf(&mut wallet, txid, chain_position); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() @@ -1491,7 +1492,7 @@ fn test_create_tx_increment_change_index() { .create_wallet_no_persist() .unwrap(); // fund wallet - receive_output(&mut wallet, amount, ConfirmationTime::unconfirmed(0)); + receive_output(&mut wallet, amount, ChainPosition::Unconfirmed(0)); // create tx let mut builder = wallet.build_tx(); builder.add_recipient(recipient.clone(), Amount::from_sat(test.to_send)); @@ -1822,14 +1823,12 @@ fn test_bump_fee_confirmed_tx() { let txid = tx.compute_txid(); wallet.insert_tx(tx); - insert_anchor_from_conf( - &mut wallet, - txid, - ConfirmationTime::Confirmed { - height: 42, - time: 42_000, - }, - ); + + let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: wallet.latest_checkpoint().get(42).unwrap().block_id(), + confirmation_time: 42_000, + }); + insert_anchor_from_conf(&mut wallet, txid, chain_position); wallet.build_fee_bump(txid).unwrap().finish().unwrap(); } @@ -2101,16 +2100,12 @@ fn test_bump_fee_drain_wallet() { }], }; let txid = tx.compute_txid(); - let tip = wallet.latest_checkpoint().height(); wallet.insert_tx(tx.clone()); - insert_anchor_from_conf( - &mut wallet, - txid, - ConfirmationTime::Confirmed { - height: tip, - time: 42_000, - }, - ); + let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: wallet.latest_checkpoint().block_id(), + confirmation_time: 42_000, + }); + insert_anchor_from_conf(&mut wallet, txid, chain_position); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") .unwrap() @@ -2166,13 +2161,12 @@ fn test_bump_fee_remove_output_manually_selected_only() { value: Amount::from_sat(25_000), }], }; - let position: ConfirmationTime = wallet + let position: ChainPosition = wallet .transactions() .last() .unwrap() .chain_position - .cloned() - .into(); + .cloned(); wallet.insert_tx(init_tx.clone()); insert_anchor_from_conf(&mut wallet, init_tx.compute_txid(), position); @@ -2220,13 +2214,12 @@ fn test_bump_fee_add_input() { }], }; let txid = init_tx.compute_txid(); - let pos: ConfirmationTime = wallet + let pos: ChainPosition = wallet .transactions() .last() .unwrap() .chain_position - .cloned() - .into(); + .cloned(); wallet.insert_tx(init_tx); insert_anchor_from_conf(&mut wallet, txid, pos); @@ -2621,11 +2614,7 @@ fn test_bump_fee_unconfirmed_inputs_only() { let psbt = builder.finish().unwrap(); // Now we receive one transaction with 0 confirmations. We won't be able to use that for // fee bumping, as it's still unconfirmed! - receive_output( - &mut wallet, - 25_000, - ConfirmationTime::Unconfirmed { last_seen: 0 }, - ); + receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); let mut tx = psbt.extract_tx().expect("failed to extract tx"); let txid = tx.compute_txid(); for txin in &mut tx.input { @@ -2651,7 +2640,7 @@ fn test_bump_fee_unconfirmed_input() { .assume_checked(); // We receive a tx with 0 confirmations, which will be used as an input // in the drain tx. - receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0)); + receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); let mut builder = wallet.build_tx(); builder.drain_wallet().drain_to(addr.script_pubkey()); let psbt = builder.finish().unwrap(); @@ -3855,12 +3844,11 @@ fn test_spend_coinbase() { .unwrap(); let confirmation_height = 5; - wallet - .insert_checkpoint(BlockId { - height: confirmation_height, - hash: BlockHash::all_zeros(), - }) - .unwrap(); + let confirmation_block_id = BlockId { + height: confirmation_height, + hash: BlockHash::all_zeros(), + }; + wallet.insert_checkpoint(confirmation_block_id).unwrap(); let coinbase_tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, @@ -3877,14 +3865,11 @@ fn test_spend_coinbase() { }; let txid = coinbase_tx.compute_txid(); wallet.insert_tx(coinbase_tx); - insert_anchor_from_conf( - &mut wallet, - txid, - ConfirmationTime::Confirmed { - height: confirmation_height, - time: 30_000, - }, - ); + let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: confirmation_block_id, + confirmation_time: 30_000, + }); + insert_anchor_from_conf(&mut wallet, txid, chain_position); let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1; let maturity_time = confirmation_height + COINBASE_MATURITY; @@ -4126,15 +4111,14 @@ fn test_keychains_with_overlapping_spks() { .last() .unwrap() .address; - let _outpoint = receive_output_to_address( - &mut wallet, - addr, - 8000, - ConfirmationTime::Confirmed { - height: 2000, - time: 0, + let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + block_id: BlockId { + height: 8000, + hash: BlockHash::all_zeros(), }, - ); + confirmation_time: 0, + }); + let _outpoint = receive_output_to_address(&mut wallet, addr, 8000, chain_position); assert_eq!(wallet.balance().confirmed, Amount::from_sat(58000)); } @@ -4266,11 +4250,7 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() { .unwrap(); assert_eq!(wallet.keychains().count(), 1); let amt = Amount::from_sat(5_000); - receive_output( - &mut wallet, - 2 * amt.to_sat(), - ConfirmationTime::Unconfirmed { last_seen: 2 }, - ); + receive_output(&mut wallet, 2 * amt.to_sat(), ChainPosition::Unconfirmed(2)); // create spend tx that produces a change output let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm") .unwrap() @@ -4297,7 +4277,7 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() { #[test] fn test_transactions_sort_by() { let (mut wallet, _txid) = get_funded_wallet_wpkh(); - receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0)); + receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); // sort by chain position, unconfirmed then confirmed by descending block height let sorted_txs: Vec = From 6a94e25d7f9c7fe114373e2c042b2c3f0a13ae48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 16 Oct 2024 14:28:23 +0000 Subject: [PATCH 2/4] feat(chain)!: `TxGraph` contain anchors in one field Previously, the field `TxGraph::anchors` existed because we assumed there was use in having a partially-chronological order of transactions there. Turns out it wasn't used at all. This commit removes anchors from the `txs` field and reworks `anchors` field to be a map of `txid -> set`. This is a breaking change since the signature of `all_anchors()` is changed. --- crates/chain/src/tx_graph.rs | 71 +++++++++++++++-------------- crates/chain/tests/test_tx_graph.rs | 6 ++- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 9a32ccdfc..6ca0f1e27 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -106,7 +106,7 @@ use core::{ ops::{Deref, RangeInclusive}, }; -impl From> for TxUpdate { +impl From> for TxUpdate { fn from(graph: TxGraph) -> Self { Self { txs: graph.full_txs().map(|tx_node| tx_node.tx).collect(), @@ -114,7 +114,11 @@ impl From> for TxUpdate { .floating_txouts() .map(|(op, txo)| (op, txo.clone())) .collect(), - anchors: graph.anchors, + anchors: graph + .anchors + .into_iter() + .flat_map(|(txid, anchors)| anchors.into_iter().map(move |a| (a, txid))) + .collect(), seen_ats: graph.last_seen.into_iter().collect(), } } @@ -135,15 +139,15 @@ impl From> for TxGraph { /// [module-level documentation]: crate::tx_graph #[derive(Clone, Debug, PartialEq)] pub struct TxGraph { - // all transactions that the graph is aware of in format: `(tx_node, tx_anchors)` - txs: HashMap)>, + txs: HashMap, spends: BTreeMap>, - anchors: BTreeSet<(A, Txid)>, + anchors: HashMap>, last_seen: HashMap, // This atrocity exists so that `TxGraph::outspends()` can return a reference. // FIXME: This can be removed once `HashSet::new` is a const fn. empty_outspends: HashSet, + empty_anchors: BTreeSet, } impl Default for TxGraph { @@ -154,6 +158,7 @@ impl Default for TxGraph { anchors: Default::default(), last_seen: Default::default(), empty_outspends: Default::default(), + empty_anchors: Default::default(), } } } @@ -238,7 +243,7 @@ impl TxGraph { /// /// This includes txouts of both full transactions as well as floating transactions. pub fn all_txouts(&self) -> impl Iterator { - self.txs.iter().flat_map(|(txid, (tx, _))| match tx { + self.txs.iter().flat_map(|(txid, tx)| match tx { TxNodeInternal::Whole(tx) => tx .as_ref() .output @@ -260,7 +265,7 @@ impl TxGraph { pub fn floating_txouts(&self) -> impl Iterator { self.txs .iter() - .filter_map(|(txid, (tx_node, _))| match tx_node { + .filter_map(|(txid, tx_node)| match tx_node { TxNodeInternal::Whole(_) => None, TxNodeInternal::Partial(txouts) => Some( txouts @@ -273,17 +278,15 @@ impl TxGraph { /// Iterate over all full transactions in the graph. pub fn full_txs(&self) -> impl Iterator, A>> { - self.txs - .iter() - .filter_map(|(&txid, (tx, anchors))| match tx { - TxNodeInternal::Whole(tx) => Some(TxNode { - txid, - tx: tx.clone(), - anchors, - last_seen_unconfirmed: self.last_seen.get(&txid).copied(), - }), - TxNodeInternal::Partial(_) => None, - }) + self.txs.iter().filter_map(|(&txid, tx)| match tx { + TxNodeInternal::Whole(tx) => Some(TxNode { + txid, + tx: tx.clone(), + anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors), + last_seen_unconfirmed: self.last_seen.get(&txid).copied(), + }), + TxNodeInternal::Partial(_) => None, + }) } /// Iterate over graph transactions with no anchors or last-seen. @@ -311,10 +314,10 @@ impl TxGraph { /// Get a transaction node by txid. This only returns `Some` for full transactions. pub fn get_tx_node(&self, txid: Txid) -> Option, A>> { match &self.txs.get(&txid)? { - (TxNodeInternal::Whole(tx), anchors) => Some(TxNode { + TxNodeInternal::Whole(tx) => Some(TxNode { txid, tx: tx.clone(), - anchors, + anchors: self.anchors.get(&txid).unwrap_or(&self.empty_anchors), last_seen_unconfirmed: self.last_seen.get(&txid).copied(), }), _ => None, @@ -323,7 +326,7 @@ impl TxGraph { /// Obtains a single tx output (if any) at the specified outpoint. pub fn get_txout(&self, outpoint: OutPoint) -> Option<&TxOut> { - match &self.txs.get(&outpoint.txid)?.0 { + match &self.txs.get(&outpoint.txid)? { TxNodeInternal::Whole(tx) => tx.as_ref().output.get(outpoint.vout as usize), TxNodeInternal::Partial(txouts) => txouts.get(&outpoint.vout), } @@ -333,7 +336,7 @@ impl TxGraph { /// /// Returns a [`BTreeMap`] of vout to output of the provided `txid`. pub fn tx_outputs(&self, txid: Txid) -> Option> { - Some(match &self.txs.get(&txid)?.0 { + Some(match &self.txs.get(&txid)? { TxNodeInternal::Whole(tx) => tx .as_ref() .output @@ -496,7 +499,7 @@ impl TxGraph { } /// Get all transaction anchors known by [`TxGraph`]. - pub fn all_anchors(&self) -> &BTreeSet<(A, Txid)> { + pub fn all_anchors(&self) -> &HashMap> { &self.anchors } @@ -540,7 +543,7 @@ impl TxGraph { /// [`apply_changeset`]: Self::apply_changeset pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { let mut changeset = ChangeSet::::default(); - let (tx_node, _) = self.txs.entry(outpoint.txid).or_default(); + let tx_node = self.txs.entry(outpoint.txid).or_default(); match tx_node { TxNodeInternal::Whole(_) => { // ignore this txout we have the full one already. @@ -573,7 +576,7 @@ impl TxGraph { let txid = tx.compute_txid(); let mut changeset = ChangeSet::::default(); - let (tx_node, _) = self.txs.entry(txid).or_default(); + let tx_node = self.txs.entry(txid).or_default(); match tx_node { TxNodeInternal::Whole(existing_tx) => { debug_assert_eq!( @@ -625,13 +628,7 @@ impl TxGraph { /// `anchor`. pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet { let mut changeset = ChangeSet::::default(); - if self.anchors.insert((anchor.clone(), txid)) { - let (_tx_node, anchors) = self.txs.entry(txid).or_default(); - let _inserted = anchors.insert(anchor.clone()); - debug_assert!( - _inserted, - "anchors in `.anchors` and `.txs` should be consistent" - ); + if self.anchors.entry(txid).or_default().insert(anchor.clone()) { changeset.anchors.insert((anchor, txid)); } changeset @@ -711,7 +708,11 @@ impl TxGraph { .floating_txouts() .map(|(op, txout)| (op, txout.clone())) .collect(), - anchors: self.anchors.clone(), + anchors: self + .anchors + .iter() + .flat_map(|(txid, anchors)| anchors.iter().map(|a| (a.clone(), *txid))) + .collect(), last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(), } } @@ -763,12 +764,12 @@ impl TxGraph { chain_tip: BlockId, txid: Txid, ) -> Result>, C::Error> { - let (tx_node, anchors) = match self.txs.get(&txid) { + let tx_node = match self.txs.get(&txid) { Some(v) => v, None => return Ok(None), }; - for anchor in anchors { + for anchor in self.anchors.get(&txid).unwrap_or(&self.empty_anchors) { match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? { Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))), _ => continue, diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 08be91c7a..7f94713e9 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1122,11 +1122,13 @@ fn call_map_anchors_with_non_deterministic_anchor() { } assert!(new_txs.next().is_none()); - let new_graph_anchors: Vec<_> = new_graph + let mut new_graph_anchors: Vec<_> = new_graph .all_anchors() .iter() - .map(|i| i.0.anchor_block) + .flat_map(|(_, anchors)| anchors) + .map(|a| a.anchor_block) .collect(); + new_graph_anchors.sort(); assert_eq!( new_graph_anchors, vec![ From 84e57be801bb47c90388f7ba7c30b9d82ed98b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 17 Oct 2024 12:49:56 +0000 Subject: [PATCH 3/4] feat(chain)!: change the `TxGraph::spends` field to be a `HashMap` Previously, this was a `BTreeMap` which was less performant. Order by `OutPoint` is not important. Test `test_descendants_no_repeat` is fixed. Previously, a tx of the test spent a non-existing prevout, and `tx_a` was not part of the `TxGraph`. --- crates/chain/src/tx_graph.rs | 51 +++++++++++++++-------------- crates/chain/tests/test_tx_graph.rs | 6 ++-- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 6ca0f1e27..067b6eba0 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -101,10 +101,7 @@ use alloc::vec::Vec; pub use bdk_core::TxUpdate; use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; use core::fmt::{self, Formatter}; -use core::{ - convert::Infallible, - ops::{Deref, RangeInclusive}, -}; +use core::{convert::Infallible, ops::Deref}; impl From> for TxUpdate { fn from(graph: TxGraph) -> Self { @@ -140,10 +137,12 @@ impl From> for TxGraph { #[derive(Clone, Debug, PartialEq)] pub struct TxGraph { txs: HashMap, - spends: BTreeMap>, anchors: HashMap>, last_seen: HashMap, + // Indexes + spends: HashMap>, + // This atrocity exists so that `TxGraph::outspends()` can return a reference. // FIXME: This can be removed once `HashSet::new` is a const fn. empty_outspends: HashSet, @@ -409,15 +408,20 @@ impl TxGraph { /// /// - `vout` is the provided `txid`'s outpoint that is being spent /// - `txid-set` is the set of txids spending the `vout`. - pub fn tx_spends( - &self, - txid: Txid, - ) -> impl DoubleEndedIterator)> + '_ { - let start = OutPoint::new(txid, 0); - let end = OutPoint::new(txid, u32::MAX); - self.spends - .range(start..=end) - .map(|(outpoint, spends)| (outpoint.vout, spends)) + pub fn tx_spends(&self, txid: Txid) -> impl Iterator)> + '_ { + let txout_count = self + .txs + .get(&txid) + .map(|tx| match tx { + TxNodeInternal::Whole(tx) => tx.output.len() as u32, + TxNodeInternal::Partial(_) => 0, + }) + .unwrap_or(0); + (0..txout_count).map(move |vout| { + let op = OutPoint::new(txid, vout); + let spends = self.spends.get(&op).unwrap_or(&self.empty_outspends); + (vout, spends) + }) } } @@ -1517,13 +1521,14 @@ impl<'g, A, F> TxDescendants<'g, A, F> { impl<'g, A, F> TxDescendants<'g, A, F> { fn populate_queue(&mut self, depth: usize, txid: Txid) { - let spend_paths = self - .graph - .spends - .range(tx_outpoint_range(txid)) - .flat_map(|(_, spends)| spends) - .map(|&txid| (depth, txid)); - self.queue.extend(spend_paths); + if let Some(tx) = self.graph.get_tx(txid) { + let spend_paths = (0..tx.output.len() as u32) + .map(|vout| OutPoint::new(txid, vout)) + .filter_map(|op| self.graph.spends.get(&op)) + .flatten() + .map(|&txid| (depth, txid)); + self.queue.extend(spend_paths); + } } } @@ -1550,7 +1555,3 @@ where Some(item) } } - -fn tx_outpoint_range(txid: Txid) -> RangeInclusive { - OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX) -} diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 7f94713e9..c80735211 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -725,9 +725,9 @@ fn test_descendants_no_repeat() { .collect::>(); let txs_c = (0..2) - .map(|vout| Transaction { + .map(|prev_tx_index| Transaction { input: vec![TxIn { - previous_output: OutPoint::new(txs_b[vout as usize].compute_txid(), vout), + previous_output: OutPoint::new(txs_b[prev_tx_index as usize].compute_txid(), 0), ..TxIn::default() }], output: vec![TxOut::NULL], @@ -773,6 +773,8 @@ fn test_descendants_no_repeat() { let mut graph = TxGraph::<()>::default(); let mut expected_txids = Vec::new(); + let _ = graph.insert_tx(tx_a.clone()); + // these are NOT descendants of `tx_a` for tx in txs_not_connected { let _ = graph.insert_tx(tx.clone()); From 37e4da6d69633a42f24e8c78b014d6dce0d202e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 24 Oct 2024 05:54:25 +0000 Subject: [PATCH 4/4] WIP --- crates/bitcoind_rpc/tests/test_emitter.rs | 5 +- crates/chain/src/canonical_view.rs | 511 ++++++++++++++++ crates/chain/src/lib.rs | 4 + crates/chain/src/tx_graph.rs | 549 +++--------------- crates/chain/src/unconfirmed_oracle.rs | 87 +++ crates/chain/tests/test_indexed_tx_graph.rs | 94 ++- crates/chain/tests/test_tx_graph.rs | 156 +++-- crates/chain/tests/test_tx_graph_conflicts.rs | 44 +- crates/electrum/tests/test_electrum.rs | 5 +- crates/testenv/src/utils.rs | 13 +- crates/wallet/src/types.rs | 4 +- crates/wallet/src/wallet/coin_selection.rs | 20 +- crates/wallet/src/wallet/export.rs | 20 +- crates/wallet/src/wallet/mod.rs | 197 +++---- crates/wallet/src/wallet/tx_builder.rs | 4 +- crates/wallet/tests/common.rs | 10 +- crates/wallet/tests/wallet.rs | 124 ++-- .../example_bitcoind_rpc_polling/src/main.rs | 30 +- example-crates/example_cli/src/lib.rs | 74 ++- example-crates/example_electrum/src/main.rs | 18 +- example-crates/example_esplora/src/main.rs | 18 +- 21 files changed, 1111 insertions(+), 876 deletions(-) create mode 100644 crates/chain/src/canonical_view.rs create mode 100644 crates/chain/src/unconfirmed_oracle.rs diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 8c41efc03..0f46bb52c 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -5,7 +5,7 @@ use bdk_chain::{ bitcoin::{Address, Amount, Txid}, local_chain::{CheckPoint, LocalChain}, spk_txout::SpkTxOutIndex, - Balance, BlockId, IndexedTxGraph, Merge, + Balance, BlockId, IndexedTxGraph, LastSeenPrioritizer, Merge, }; use bdk_testenv::{anyhow, TestEnv}; use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash}; @@ -308,7 +308,8 @@ fn get_balance( let outpoints = recv_graph.index.outpoints().clone(); let balance = recv_graph .graph() - .balance(recv_chain, chain_tip, outpoints, |_, _| true); + .canonical_view(recv_chain, chain_tip, &LastSeenPrioritizer) + .balance(outpoints, |_, _| true); Ok(balance) } diff --git a/crates/chain/src/canonical_view.rs b/crates/chain/src/canonical_view.rs new file mode 100644 index 000000000..338b882c4 --- /dev/null +++ b/crates/chain/src/canonical_view.rs @@ -0,0 +1,511 @@ +use core::{convert::Infallible, fmt::Debug}; + +use crate::{ + alloc::{collections::VecDeque, vec::Vec}, + collections::{HashMap, HashSet}, + tx_graph::{TxDescendants, TxNode}, + Anchor, Balance, ChainOracle, TxGraph, UnconfirmedOracle, COINBASE_MATURITY, +}; +use alloc::sync::Arc; +use bdk_core::BlockId; +use bitcoin::{Amount, OutPoint, Script, Transaction, TxOut, Txid}; + +/// A transaction that is part of the [`CanonicalView`]. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CanonicalTx { + /// The position of the transaction in the [`CanonicalView`]. + pub pos: CanonicalPos, + /// The txid. + pub txid: Txid, + /// The transaction. + pub tx: Arc, +} + +/// A transaction output that is part of the [`CanonicalView`]. +#[derive(Debug, Clone, PartialEq, PartialOrd)] +pub struct CanonicalTxOut { + /// The position of the txout's residing transaction in the [`CanonicalView`]. + pub pos: CanonicalPos, + /// The index of the script pubkey of this txout. + pub spk_index: I, + /// The outpoint. + pub outpoint: OutPoint, + /// The txout. + pub txout: TxOut, + /// The canonical transaction spending this txout (if any). + pub spent_by: Option, + /// Whether this output resides in a coinbase transaction. + pub is_on_coinbase: bool, +} + +impl CanonicalTxOut { + /// Whether txout belongs in a confirmed transaction. + pub fn is_confirmed(&self) -> bool { + match self.pos { + CanonicalPos::Confirmed(_) => true, + CanonicalPos::Unconfirmed(_) => false, + } + } +} + +impl CanonicalTxOut { + /// Whether the `txout` is considered mature. + /// + /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this + /// method may return false-negatives. In other words, interpreted confirmation count may be + /// less than the actual value. + /// + /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound + pub fn is_mature(&self, tip_height: u32) -> bool { + if self.is_on_coinbase { + let tx_height = match &self.pos { + CanonicalPos::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), + CanonicalPos::Unconfirmed(_) => { + debug_assert!(false, "coinbase tx can never be unconfirmed"); + return false; + } + }; + let age = tip_height.saturating_sub(tx_height); + if age + 1 < COINBASE_MATURITY { + return false; + } + } + true + } +} + +/// A consistent view of transactions. +/// +/// Function: +/// * Return ordered history of transactions. +/// * Quickly query whether a txid is in the canonical history and it's position. +#[derive(Debug, Clone)] +pub struct CanonicalView { + pub(crate) tip: BlockId, + pub(crate) txs: HashMap, Arc)>, + pub(crate) ordered_txids: Vec, + pub(crate) spends: HashMap, +} + +/// The position of the transaction in a [`CanonicalView`]. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(bound( + deserialize = "A: serde::Deserialize<'de>, U: serde::Deserialize<'de>", + serialize = "A: serde::Serialize, U: serde::Serialize" + )) +)] +pub enum CanonicalPos { + /// Confirmed with anchor `A`. + Confirmed(A), + /// Unconfirmed, positioned with `U`. + Unconfirmed(U), +} + +impl CanonicalPos { + /// Whether the [`CanonicalPos`] is a confirmed position. + pub fn is_confirmed(&self) -> bool { + matches!(self, Self::Confirmed(_)) + } + + /// Get the upper bound of the chain data's confirmation height. + /// + /// Refer to [`Anchor::confirmation_height_upper_bound`]. + pub fn confirmation_height_upper_bound(&self) -> Option + where + A: Anchor, + { + match self { + CanonicalPos::Confirmed(anchor) => Some(anchor.confirmation_height_upper_bound()), + CanonicalPos::Unconfirmed(_) => None, + } + } +} + +impl CanonicalView { + /// Yoo + pub fn new>( + chain_oracle: &CO, + chain_tip: BlockId, + unconf_oracle: &UO, + tx_graph: &TxGraph, + ) -> Result> { + // Each outpoint represents a set of conflicting edges. + let mut next_ops = VecDeque::::new(); + let mut visited_ops = HashSet::::new(); + let mut included = HashMap::, Arc)>::new(); + let mut included_in_order = Vec::::new(); + let mut excluded = HashSet::::new(); + + fn insert_canon_tx( + tx_node: &TxNode, A>, + canonical_pos: CanonicalPos, + included: &mut HashMap, Arc)>, + included_in_order: &mut Vec, + next_ops: &mut VecDeque, + ) -> bool { + let is_new = included + .insert(tx_node.txid, (canonical_pos, tx_node.tx.clone())) + .is_none(); + if is_new { + included_in_order.push(tx_node.txid); + next_ops.extend( + (0..tx_node.output.len() as u32).map(|vout| OutPoint::new(tx_node.txid, vout)), + ); + } + is_new + } + + for tx_node in tx_graph.coinbase_txs() { + for anchor in tx_node.anchors { + let is_canon = chain_oracle + .is_block_in_chain(anchor.anchor_block(), chain_tip) + .map_err(CanonicalError::ChainOracle)?; + if is_canon == Some(true) + && insert_canon_tx( + &tx_node, + CanonicalPos::Confirmed(anchor.clone()), + &mut included, + &mut included_in_order, + &mut next_ops, + ) + { + break; + } + } + } + next_ops.extend(tx_graph.root_outpoints()); + + //println!("[START: CREATE CANONICAL VIEW]"); + while let Some(op) = next_ops.pop_front() { + if !visited_ops.insert(op) || excluded.contains(&op.txid) { + continue; + } + //println!("ITERATION: {}", op); + + let conflicts = tx_graph + .outspends(op) + .iter() + .copied() + .filter(|txid| !excluded.contains(txid)) + .collect::>(); + + let mut conflicts_iter = conflicts.iter(); + let mut canon_txid: Option = 'find_canon_txid: loop { + let txid = match conflicts_iter.next() { + Some(&txid) => txid, + None => break None, + }; + if let Some((CanonicalPos::Confirmed(_), _)) = included.get(&txid) { + break Some(txid); + } + if let Some(tx_node) = tx_graph.get_tx_node(txid) { + for anchor in tx_node.anchors { + let is_canon = chain_oracle + .is_block_in_chain(anchor.anchor_block(), chain_tip) + .map_err(CanonicalError::ChainOracle)?; + if is_canon == Some(true) { + assert!( + insert_canon_tx( + &tx_node, + CanonicalPos::Confirmed(anchor.clone()), + &mut included, + &mut included_in_order, + &mut next_ops, + ), + "we just checked that tx is not already canonical with anchor" + ); + break 'find_canon_txid Some(txid); + } + } + } + }; + if canon_txid.is_none() { + if let Some((unconf_pos, txid)) = unconf_oracle + .pick_canonical(tx_graph, conflicts.clone()) + .map_err(CanonicalError::UnconfirmedOracle)? + { + if let Some(tx_node) = tx_graph.get_tx_node(txid) { + insert_canon_tx( + &tx_node, + CanonicalPos::Unconfirmed(unconf_pos), + &mut included, + &mut included_in_order, + &mut next_ops, + ); + //println!("\t FOUND UNCONFIRMED CANON: {}", txid); + canon_txid = Some(txid); + }; + } + } + + let _n = TxDescendants::from_multiple_include_root( + tx_graph, + conflicts + .into_iter() + .filter(|&txid| Some(txid) != canon_txid), + |_: usize, txid| { + if excluded.insert(txid) { + //println!("\t EXCLUDED: {}", txid); + Some(()) + } else { + None + } + }, + ) + .count(); + } + + // remove excluded elements and sort. + included_in_order.retain(|txid| !excluded.contains(txid)); + included.retain(|txid, _| !excluded.contains(txid)); + let spends = included + .iter() + .flat_map(|(txid, (_, tx))| tx.input.iter().map(|txin| (txin.previous_output, *txid))) + .collect(); + Ok(Self { + tip: chain_tip, + txs: included, + ordered_txids: included_in_order, + spends, + }) + } + + /// Whether this view has no transactions. + pub fn is_empty(&self) -> bool { + self.txs.is_empty() + } + + /// Returns the number of transactions in the view. + pub fn len(&self) -> usize { + self.txs.len() + } + + /// Get the canonical transaction of txid. + pub fn tx(&self, txid: Txid) -> Option> { + get_tx(&self.txs, txid) + } + + /// Get the canonical output of the given outpoint. + pub fn txout( + &self, + outpoint: OutPoint, + index_from_outpoint: impl Fn(OutPoint) -> Option, + ) -> Option> { + let spk_i = index_from_outpoint(outpoint)?; + self.filter_txouts(core::iter::once((spk_i, outpoint))) + .next() + } + + /// Get the canonical unspent output of the given outpoint. + pub fn unspent( + &self, + outpoint: OutPoint, + index_from_outpoint: impl Fn(OutPoint) -> Option, + ) -> Option> { + let spk_i = index_from_outpoint(outpoint)?; + self.filter_unspents(core::iter::once((spk_i, outpoint))) + .next() + } + + /// Get spend for given output (if any). + pub fn spend(&self, outpoint: OutPoint) -> Option { + self.spends.get(&outpoint).copied() + } + + /// Get spend for given output (if any), alongside the spending tx's canoncial position. + pub fn spend_with_pos(&self, outpoint: OutPoint) -> Option<(CanonicalPos, Txid)> { + let spend_txid = self.spends.get(&outpoint).copied()?; + let spend_pos = self.tx(spend_txid).expect("must exist").pos; + Some((spend_pos, spend_txid)) + } + + /// Sort the canonical txs by key. + pub fn sort_txs_by_key(&mut self, mut f: F) + where + F: FnMut(&Txid, &CanonicalPos) -> K, + K: Ord, + { + let txs = &self.txs; + self.ordered_txids.sort_by_key(move |txid| { + let (pos, _) = txs.get(txid).expect("must have corresponding tx"); + f(txid, pos) + }); + } + + /// Iterate over all transactions of the [`CanonicalView`]. + pub fn txs( + &self, + ) -> impl ExactSizeIterator> + DoubleEndedIterator + '_ { + self.ordered_txids + .iter() + .map(|&txid| get_tx(&self.txs, txid).expect("corresponding tx must exist")) + } + + /// Iterate over all transactions of [`CanonicalView`] by owning it. + pub fn into_txs( + self, + ) -> impl ExactSizeIterator> + DoubleEndedIterator { + self.ordered_txids + .into_iter() + .map(move |txid| get_tx(&self.txs, txid).expect("corresponding tx must exist")) + } + + /// Obtain a subset of `outpoints` that are canonical. + pub fn filter_txouts<'a, I: 'a>( + &'a self, + outpoints: impl IntoIterator + 'a, + ) -> impl Iterator> + 'a { + outpoints.into_iter().filter_map(|(spk_index, outpoint)| { + get_txout(&self.txs, &self.spends, spk_index, outpoint) + }) + } + + /// Obtain a subset of `outpoints` that are canonical by taking ownership. + pub fn into_filter_txouts( + self, + outpoints: impl IntoIterator, + ) -> impl Iterator> { + outpoints + .into_iter() + .filter_map(move |(spk_index, outpoint)| { + get_txout(&self.txs, &self.spends, spk_index, outpoint) + }) + } + + /// Obtain a subset of `outpoints` that are canonical and unspent. + pub fn filter_unspents<'a, I: 'a>( + &'a self, + outpoints: impl IntoIterator + 'a, + ) -> impl Iterator> + 'a { + self.filter_txouts(outpoints) + .filter(|txo| txo.spent_by.is_none()) + } + + /// Obtain a subset of `outpoints` that are canonical and unspent by taking ownership. + pub fn into_filter_unspents( + self, + outpoints: impl IntoIterator, + ) -> impl Iterator> { + self.into_filter_txouts(outpoints) + .filter(|txo| txo.spent_by.is_none()) + } + + /// Get the total balance of `outpoints`. + pub fn balance( + &self, + outpoints: impl IntoIterator, + mut trust_predicate: impl FnMut(&I, &Script) -> bool, + ) -> Balance { + let mut immature = Amount::ZERO; + let mut trusted_pending = Amount::ZERO; + let mut untrusted_pending = Amount::ZERO; + let mut confirmed = Amount::ZERO; + + for txout in self.filter_unspents(outpoints) { + match &txout.pos { + CanonicalPos::Confirmed(_) => { + if txout.is_mature(self.tip.height) { + confirmed += txout.txout.value; + } else { + immature += txout.txout.value; + } + } + CanonicalPos::Unconfirmed(_) => { + if trust_predicate(&txout.spk_index, txout.txout.script_pubkey.as_script()) { + trusted_pending += txout.txout.value; + } else { + untrusted_pending += txout.txout.value; + } + } + } + } + + Balance { + immature, + trusted_pending, + untrusted_pending, + confirmed, + } + } +} + +fn get_tx( + txs: &HashMap, Arc)>, + txid: Txid, +) -> Option> { + txs.get(&txid) + .cloned() + .map(|(pos, tx)| CanonicalTx { pos, txid, tx }) +} + +fn get_txout( + txs: &HashMap, Arc)>, + spends: &HashMap, + spk_index: I, + outpoint: OutPoint, +) -> Option> { + let (pos, tx) = txs.get(&outpoint.txid).cloned()?; + let txout = tx.output.get(outpoint.vout as usize).cloned()?; + let spent_by = spends.get(&outpoint).copied(); + let is_on_coinbase = tx.is_coinbase(); + Some(CanonicalTxOut { + pos, + spk_index, + outpoint, + txout, + spent_by, + is_on_coinbase, + }) +} + +/// Occurs when constructing a [`CanonicalView`] fails. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CanonicalError { + /// The [`ChainOracle`] impl errored. + ChainOracle(CE), + /// The [`UnconfirmedOracle`] impl errored. + UnconfirmedOracle(UE), +} + +impl CanonicalError { + /// Transform into an [`UnconfirmedOracle`] error. + pub fn into_unconfirmed_oracle_error(self) -> UE { + match self { + CanonicalError::ChainOracle(_) => unreachable!("error is infallible"), + CanonicalError::UnconfirmedOracle(err) => err, + } + } +} + +impl CanonicalError { + /// Transform into a [`ChainOracle`] error. + pub fn into_chain_oracle_error(self) -> CE { + match self { + CanonicalError::ChainOracle(err) => err, + CanonicalError::UnconfirmedOracle(_) => unreachable!("error is infallible"), + } + } +} + +impl core::fmt::Display for CanonicalError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CanonicalError::ChainOracle(err) => write!( + f, + "chain oracle errored while constructing canonical view: {}", + err + ), + CanonicalError::UnconfirmedOracle(err) => write!( + f, + "unconfirmed oracle errored while constructing canonical view: {}", + err + ), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CanonicalError {} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 9667bb549..168dda4d5 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -43,6 +43,10 @@ pub mod tx_graph; pub use tx_graph::TxGraph; mod chain_oracle; pub use chain_oracle::*; +mod unconfirmed_oracle; +pub use unconfirmed_oracle::*; +mod canonical_view; +pub use canonical_view::*; #[doc(hidden)] pub mod example_utils; diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 067b6eba0..7457a6e6c 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -94,14 +94,18 @@ use crate::collections::*; use crate::BlockId; -use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge}; +use crate::CanonicalError; +use crate::CanonicalView; +use crate::UnconfirmedOracle; +use crate::{Anchor, ChainOracle, Merge}; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; use alloc::vec::Vec; pub use bdk_core::TxUpdate; -use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; +use bitcoin::{Amount, OutPoint, SignedAmount, Transaction, TxOut, Txid}; +use core::convert::Infallible; use core::fmt::{self, Formatter}; -use core::{convert::Infallible, ops::Deref}; +use core::ops::Deref; impl From> for TxUpdate { fn from(graph: TxGraph) -> Self { @@ -142,6 +146,7 @@ pub struct TxGraph { // Indexes spends: HashMap>, + roots: TxRoots, // This atrocity exists so that `TxGraph::outspends()` can return a reference. // FIXME: This can be removed once `HashSet::new` is a const fn. @@ -153,15 +158,25 @@ impl Default for TxGraph { fn default() -> Self { Self { txs: Default::default(), - spends: Default::default(), anchors: Default::default(), last_seen: Default::default(), + spends: Default::default(), + roots: Default::default(), empty_outspends: Default::default(), empty_anchors: Default::default(), } } } +/// Roots of the [`TxGraph`]. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TxRoots { + /// These outputs being spent reside in transactions that are not part of the [`TxGraph`]. + pub spends: BTreeSet, + /// Coinbase transactions. + pub coinbase: BTreeSet, +} + /// A transaction node in the [`TxGraph`]. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct TxNode<'a, T, A> { @@ -199,15 +214,6 @@ impl Default for TxNodeInternal { } } -/// A transaction that is included in the chain, or is still in mempool. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct CanonicalTx<'a, T, A> { - /// How the transaction is observed as (confirmed or unconfirmed). - pub chain_position: ChainPosition<&'a A>, - /// The transaction node (as part of the graph). - pub tx_node: TxNode<'a, T, A>, -} - /// Errors returned by `TxGraph::calculate_fee`. #[derive(Debug, PartialEq, Eq)] pub enum CalculateFeeError { @@ -238,6 +244,19 @@ impl fmt::Display for CalculateFeeError { impl std::error::Error for CalculateFeeError {} impl TxGraph { + /// Coinbase transactions. + pub fn coinbase_txs(&self) -> impl ExactSizeIterator, A>> { + self.roots.coinbase.iter().map(|&txid| { + self.get_tx_node(txid) + .expect("coinbase tx must have corresponding full tx") + }) + } + + /// Root outpoints. + pub fn root_outpoints(&self) -> &BTreeSet { + &self.roots.spends + } + /// Iterate over all tx outputs known by [`TxGraph`]. /// /// This includes txouts of both full transactions as well as floating transactions. @@ -588,23 +607,43 @@ impl TxGraph { tx.as_ref(), "tx of same txid should never change" ); + return changeset; } partial_tx => { - for txin in &tx.input { - // this means the tx is coinbase so there is no previous output - if txin.previous_output.is_null() { - continue; - } - self.spends - .entry(txin.previous_output) - .or_default() - .insert(txid); - } *partial_tx = TxNodeInternal::Whole(tx.clone()); - changeset.txs.insert(tx); } } + for tx_op in (0..tx.output.len() as u32).map(|vout| OutPoint::new(txid, vout)) { + self.roots.spends.remove(&tx_op); + } + for txin in &tx.input { + // this means the tx is coinbase so there is no previous output + if txin.previous_output.is_null() { + assert!( + self.roots.coinbase.insert(txid), + "must not insert the same tx twice" + ); + debug_assert_eq!(tx.input.len(), 1, "coinbase txs must have one input"); + break; + } + match self.txs.get(&txin.previous_output.txid) { + Some(TxNodeInternal::Whole(_)) => {} + _ => { + self.roots.spends.insert(txin.previous_output); + } + } + self.spends + .entry(txin.previous_output) + .or_default() + .insert(txid); + } + debug_assert!( + !tx.input.is_empty(), + "a transaction must have atleast one input" + ); + + changeset.txs.insert(tx); changeset } @@ -739,457 +778,57 @@ impl TxGraph { } impl TxGraph { - /// Get the position of the transaction in `chain` with tip `chain_tip`. - /// - /// Chain data is fetched from `chain`, a [`ChainOracle`] implementation. - /// - /// This method returns `Ok(None)` if the transaction is not found in the chain, and no longer - /// belongs in the mempool. The following factors are used to approximate whether an - /// unconfirmed transaction exists in the mempool (not evicted): - /// - /// 1. Unconfirmed transactions that conflict with confirmed transactions are evicted. - /// 2. Unconfirmed transactions that spend from transactions that are evicted, are also - /// evicted. - /// 3. Given two conflicting unconfirmed transactions, the transaction with the lower - /// `last_seen_unconfirmed` parameter is evicted. A transaction's `last_seen_unconfirmed` - /// parameter is the max of all it's descendants' `last_seen_unconfirmed` parameters. If the - /// final `last_seen_unconfirmed`s are the same, the transaction with the lower `txid` (by - /// lexicographical order) is evicted. - /// - /// # Error - /// - /// An error will occur if the [`ChainOracle`] implementation (`chain`) fails. If the - /// [`ChainOracle`] is infallible, [`get_chain_position`] can be used instead. - /// - /// [`get_chain_position`]: Self::get_chain_position - pub fn try_get_chain_position( + /// Contruct the [`CanonicalView`] of transactions. + pub fn try_canonical_view( &self, - chain: &C, + chain_oracle: &C, chain_tip: BlockId, - txid: Txid, - ) -> Result>, C::Error> { - let tx_node = match self.txs.get(&txid) { - Some(v) => v, - None => return Ok(None), - }; - - for anchor in self.anchors.get(&txid).unwrap_or(&self.empty_anchors) { - match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? { - Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))), - _ => continue, - } - } - - // If no anchors are in best chain and we don't have a last_seen, we can return - // early because by definition the tx doesn't have a chain position. - let last_seen = match self.last_seen.get(&txid) { - Some(t) => *t, - None => return Ok(None), - }; - - // The tx is not anchored to a block in the best chain, which means that it - // might be in mempool, or it might have been dropped already. - // Let's check conflicts to find out! - let tx = match tx_node { - TxNodeInternal::Whole(tx) => { - // A coinbase tx that is not anchored in the best chain cannot be unconfirmed and - // should always be filtered out. - if tx.is_coinbase() { - return Ok(None); - } - tx.clone() - } - TxNodeInternal::Partial(_) => { - // Partial transactions (outputs only) cannot have conflicts. - return Ok(None); - } - }; - - // We want to retrieve all the transactions that conflict with us, plus all the - // transactions that conflict with our unconfirmed ancestors, since they conflict with us - // as well. - // We only traverse unconfirmed ancestors since conflicts of confirmed transactions - // cannot be in the best chain. - - // First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the - // resulting array will also include `tx` - let unconfirmed_ancestor_txs = - TxAncestors::new_include_root(self, tx.clone(), |_, ancestor_tx: Arc| { - let tx_node = self.get_tx_node(ancestor_tx.as_ref().compute_txid())?; - // We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in - // the best chain) - for block in tx_node.anchors { - match chain.is_block_in_chain(block.anchor_block(), chain_tip) { - Ok(Some(true)) => return None, - Err(e) => return Some(Err(e)), - _ => continue, - } - } - Some(Ok(tx_node)) - }) - .collect::, C::Error>>()?; - - // We determine our tx's last seen, which is the max between our last seen, - // and our unconf descendants' last seen. - let unconfirmed_descendants_txs = TxDescendants::new_include_root( - self, - tx.as_ref().compute_txid(), - |_, descendant_txid: Txid| { - let tx_node = self.get_tx_node(descendant_txid)?; - // We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in - // the best chain) - for block in tx_node.anchors { - match chain.is_block_in_chain(block.anchor_block(), chain_tip) { - Ok(Some(true)) => return None, - Err(e) => return Some(Err(e)), - _ => continue, - } - } - Some(Ok(tx_node)) - }, - ) - .collect::, C::Error>>()?; - - let tx_last_seen = unconfirmed_descendants_txs - .iter() - .max_by_key(|tx| tx.last_seen_unconfirmed) - .map(|tx| tx.last_seen_unconfirmed) - .expect("descendants always includes at least one transaction (the root tx"); - - // Now we traverse our ancestors and consider all their conflicts - for tx_node in unconfirmed_ancestor_txs { - // We retrieve all the transactions conflicting with this specific ancestor - let conflicting_txs = - self.walk_conflicts(tx_node.tx.as_ref(), |_, txid| self.get_tx_node(txid)); - - // If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then - // this tx cannot exist in the best chain - for conflicting_tx in conflicting_txs { - for block in conflicting_tx.anchors { - if chain.is_block_in_chain(block.anchor_block(), chain_tip)? == Some(true) { - return Ok(None); - } - } - if conflicting_tx.last_seen_unconfirmed > tx_last_seen { - return Ok(None); - } - if conflicting_tx.last_seen_unconfirmed == Some(last_seen) - && conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid() - { - // Conflicting tx has priority if txid of conflicting tx > txid of original tx - return Ok(None); - } - } - } - - Ok(Some(ChainPosition::Unconfirmed(last_seen))) + unconf_oracle: &U, + ) -> Result, CanonicalError> { + CanonicalView::new(chain_oracle, chain_tip, unconf_oracle, self) } - /// Get the position of the transaction in `chain` with tip `chain_tip`. - /// - /// This is the infallible version of [`try_get_chain_position`]. - /// - /// [`try_get_chain_position`]: Self::try_get_chain_position - pub fn get_chain_position>( + /// Infallible version of [`Self::try_canonical_view`]. + pub fn canonical_view< + C: ChainOracle, + U: UnconfirmedOracle, + >( &self, - chain: &C, + chain_oracle: &C, chain_tip: BlockId, - txid: Txid, - ) -> Option> { - self.try_get_chain_position(chain, chain_tip, txid) - .expect("error is infallible") + unconf_oracle: &U, + ) -> CanonicalView { + self.try_canonical_view(chain_oracle, chain_tip, unconf_oracle) + .expect("infallible") } - /// Get the txid of the spending transaction and where the spending transaction is observed in - /// the `chain` of `chain_tip`. - /// - /// If no in-chain transaction spends `outpoint`, `None` will be returned. - /// - /// # Error - /// - /// An error will occur only if the [`ChainOracle`] implementation (`chain`) fails. - /// - /// If the [`ChainOracle`] is infallible, [`get_chain_spend`] can be used instead. - /// - /// [`get_chain_spend`]: Self::get_chain_spend - pub fn try_get_chain_spend( + /// Try get confirmation anchor for the given tx. + pub fn try_confirmation_anchor( &self, chain: &C, chain_tip: BlockId, - outpoint: OutPoint, - ) -> Result, Txid)>, C::Error> { - if self - .try_get_chain_position(chain, chain_tip, outpoint.txid)? - .is_none() - { - return Ok(None); - } - if let Some(spends) = self.spends.get(&outpoint) { - for &txid in spends { - if let Some(observed_at) = self.try_get_chain_position(chain, chain_tip, txid)? { - return Ok(Some((observed_at, txid))); + txid: Txid, + ) -> Result, C::Error> { + if let Some(anchors) = self.anchors.get(&txid) { + for anchor in anchors { + let is_in_chain = chain.is_block_in_chain(anchor.anchor_block(), chain_tip)?; + if is_in_chain == Some(true) { + return Ok(Some(anchor.clone())); } } } Ok(None) } - /// Get the txid of the spending transaction and where the spending transaction is observed in - /// the `chain` of `chain_tip`. - /// - /// This is the infallible version of [`try_get_chain_spend`] - /// - /// [`try_get_chain_spend`]: Self::try_get_chain_spend - pub fn get_chain_spend>( - &self, - chain: &C, - static_block: BlockId, - outpoint: OutPoint, - ) -> Option<(ChainPosition<&A>, Txid)> { - self.try_get_chain_spend(chain, static_block, outpoint) - .expect("error is infallible") - } - - /// List graph transactions that are in `chain` with `chain_tip`. - /// - /// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is - /// observed in-chain, and the [`TxNode`]. - /// - /// # Error - /// - /// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the - /// returned item. - /// - /// If the [`ChainOracle`] is infallible, [`list_canonical_txs`] can be used instead. - /// - /// [`list_canonical_txs`]: Self::list_canonical_txs - pub fn try_list_canonical_txs<'a, C: ChainOracle + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - ) -> impl Iterator, A>, C::Error>> { - self.full_txs().filter_map(move |tx| { - self.try_get_chain_position(chain, chain_tip, tx.txid) - .map(|v| { - v.map(|observed_in| CanonicalTx { - chain_position: observed_in, - tx_node: tx, - }) - }) - .transpose() - }) - } - - /// List graph transactions that are in `chain` with `chain_tip`. - /// - /// This is the infallible version of [`try_list_canonical_txs`]. - /// - /// [`try_list_canonical_txs`]: Self::try_list_canonical_txs - pub fn list_canonical_txs<'a, C: ChainOracle + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - ) -> impl Iterator, A>> { - self.try_list_canonical_txs(chain, chain_tip) - .map(|r| r.expect("oracle is infallible")) - } - - /// Get a filtered list of outputs from the given `outpoints` that are in `chain` with - /// `chain_tip`. - /// - /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier - /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or - /// [`Iterator::enumerate`] over a list of [`OutPoint`]s. - /// - /// Floating outputs (i.e., outputs for which we don't have the full transaction in the graph) - /// are ignored. - /// - /// # Error - /// - /// An [`Iterator::Item`] can be an [`Err`] if the [`ChainOracle`] implementation (`chain`) - /// fails. - /// - /// If the [`ChainOracle`] implementation is infallible, [`filter_chain_txouts`] can be used - /// instead. - /// - /// [`filter_chain_txouts`]: Self::filter_chain_txouts - pub fn try_filter_chain_txouts<'a, C: ChainOracle + 'a, OI: Clone + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - outpoints: impl IntoIterator + 'a, - ) -> impl Iterator), C::Error>> + 'a { - outpoints - .into_iter() - .map( - move |(spk_i, op)| -> Result)>, C::Error> { - let tx_node = match self.get_tx_node(op.txid) { - Some(n) => n, - None => return Ok(None), - }; - - let txout = match tx_node.tx.as_ref().output.get(op.vout as usize) { - Some(txout) => txout.clone(), - None => return Ok(None), - }; - - let chain_position = - match self.try_get_chain_position(chain, chain_tip, op.txid)? { - Some(pos) => pos.cloned(), - None => return Ok(None), - }; - - let spent_by = self - .try_get_chain_spend(chain, chain_tip, op)? - .map(|(a, txid)| (a.cloned(), txid)); - - Ok(Some(( - spk_i, - FullTxOut { - outpoint: op, - txout, - chain_position, - spent_by, - is_on_coinbase: tx_node.tx.is_coinbase(), - }, - ))) - }, - ) - .filter_map(Result::transpose) - } - - /// Get a filtered list of outputs from the given `outpoints` that are in `chain` with - /// `chain_tip`. - /// - /// This is the infallible version of [`try_filter_chain_txouts`]. - /// - /// [`try_filter_chain_txouts`]: Self::try_filter_chain_txouts - pub fn filter_chain_txouts<'a, C: ChainOracle + 'a, OI: Clone + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - outpoints: impl IntoIterator + 'a, - ) -> impl Iterator)> + 'a { - self.try_filter_chain_txouts(chain, chain_tip, outpoints) - .map(|r| r.expect("oracle is infallible")) - } - - /// Get a filtered list of unspent outputs (UTXOs) from the given `outpoints` that are in - /// `chain` with `chain_tip`. - /// - /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier - /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or - /// [`Iterator::enumerate`] over a list of [`OutPoint`]s. - /// - /// Floating outputs are ignored. - /// - /// # Error - /// - /// An [`Iterator::Item`] can be an [`Err`] if the [`ChainOracle`] implementation (`chain`) - /// fails. - /// - /// If the [`ChainOracle`] implementation is infallible, [`filter_chain_unspents`] can be used - /// instead. - /// - /// [`filter_chain_unspents`]: Self::filter_chain_unspents - pub fn try_filter_chain_unspents<'a, C: ChainOracle + 'a, OI: Clone + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - outpoints: impl IntoIterator + 'a, - ) -> impl Iterator), C::Error>> + 'a { - self.try_filter_chain_txouts(chain, chain_tip, outpoints) - .filter(|r| match r { - // keep unspents, drop spents - Ok((_, full_txo)) => full_txo.spent_by.is_none(), - // keep errors - Err(_) => true, - }) - } - - /// Get a filtered list of unspent outputs (UTXOs) from the given `outpoints` that are in - /// `chain` with `chain_tip`. - /// - /// This is the infallible version of [`try_filter_chain_unspents`]. - /// - /// [`try_filter_chain_unspents`]: Self::try_filter_chain_unspents - pub fn filter_chain_unspents<'a, C: ChainOracle + 'a, OI: Clone + 'a>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - txouts: impl IntoIterator + 'a, - ) -> impl Iterator)> + 'a { - self.try_filter_chain_unspents(chain, chain_tip, txouts) - .map(|r| r.expect("oracle is infallible")) - } - - /// Get the total balance of `outpoints` that are in `chain` of `chain_tip`. - /// - /// The output of `trust_predicate` should return `true` for scripts that we trust. - /// - /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier - /// (`OI`) for convenience. If `OI` is not necessary, the caller can use `()`, or - /// [`Iterator::enumerate`] over a list of [`OutPoint`]s. - /// - /// If the provided [`ChainOracle`] implementation (`chain`) is infallible, [`balance`] can be - /// used instead. - /// - /// [`balance`]: Self::balance - pub fn try_balance( + /// Infallible version of [`Self::try_confirmation_anchor`]. + pub fn confirmation_anchor>( &self, chain: &C, chain_tip: BlockId, - outpoints: impl IntoIterator, - mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool, - ) -> Result { - let mut immature = Amount::ZERO; - let mut trusted_pending = Amount::ZERO; - let mut untrusted_pending = Amount::ZERO; - let mut confirmed = Amount::ZERO; - - for res in self.try_filter_chain_unspents(chain, chain_tip, outpoints) { - let (spk_i, txout) = res?; - - match &txout.chain_position { - ChainPosition::Confirmed(_) => { - if txout.is_confirmed_and_spendable(chain_tip.height) { - confirmed += txout.txout.value; - } else if !txout.is_mature(chain_tip.height) { - immature += txout.txout.value; - } - } - ChainPosition::Unconfirmed(_) => { - if trust_predicate(&spk_i, txout.txout.script_pubkey) { - trusted_pending += txout.txout.value; - } else { - untrusted_pending += txout.txout.value; - } - } - } - } - - Ok(Balance { - immature, - trusted_pending, - untrusted_pending, - confirmed, - }) - } - - /// Get the total balance of `outpoints` that are in `chain` of `chain_tip`. - /// - /// This is the infallible version of [`try_balance`]. - /// - /// [`try_balance`]: Self::try_balance - pub fn balance, OI: Clone>( - &self, - chain: &C, - chain_tip: BlockId, - outpoints: impl IntoIterator, - trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool, - ) -> Balance { - self.try_balance(chain, chain_tip, outpoints, trust_predicate) - .expect("oracle is infallible") + txid: Txid, + ) -> Option { + self.try_confirmation_anchor(chain, chain_tip, txid) + .expect("infallible") } } diff --git a/crates/chain/src/unconfirmed_oracle.rs b/crates/chain/src/unconfirmed_oracle.rs new file mode 100644 index 000000000..ed4e6f9d6 --- /dev/null +++ b/crates/chain/src/unconfirmed_oracle.rs @@ -0,0 +1,87 @@ +use core::convert::Infallible; + +use bitcoin::Txid; + +use crate::{collections::HashMap, Anchor, TxGraph}; + +/// Determines the canonical transaction from a set of conflicting unconfirmed transactions. +/// +/// This is used for constructing the [`CanonicalView`]. +pub trait UnconfirmedOracle { + /// Error type. + type Error; + + /// Unconfirmed position in the [`CanonicalView`]. + type UnconfirmedPos: Ord + Clone; + + /// Given a set of conflicting unconfirmed transactions, pick the transaction which is to be + /// part of the canoncial history. + /// + /// If this returns `None`, it signals that none of the conflicts are to be considered + /// canonical. + fn pick_canonical( + &self, + tx_graph: &TxGraph, + conflicting_txids: T, + ) -> Result, Self::Error> + where + A: Anchor, + T: IntoIterator; +} + +/// A simple [`UnconfirmedOracle`] implementation that uses `last_seen` in mempool values to +/// prioritize unconfirmed transactions. +#[derive(Debug, Clone, Copy, Default)] +pub struct LastSeenPrioritizer; + +impl UnconfirmedOracle for LastSeenPrioritizer { + type Error = Infallible; + + /// Last seen in mempool. + type UnconfirmedPos = u64; + + fn pick_canonical( + &self, + tx_graph: &TxGraph, + conflicting_txids: T, + ) -> Result, Self::Error> + where + A: Anchor, + T: IntoIterator, + { + let mut best = Option::<(u64, Txid)>::None; + for txid in conflicting_txids { + let last_seen = tx_graph + .get_tx_node(txid) + .expect("must exist") + .last_seen_unconfirmed; + if let Some(last_seen) = last_seen { + let this_key = (last_seen, txid); + if Some(this_key) > best { + best = Some(this_key); + } + } + } + Ok(best) + } +} + +/// A [`UnconfirmedOracle`] implementation which allows setting a custom priority value per +/// transaction. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct CustomPrioritizer { + prioritized_txids: HashMap, +} + +/// Transaction to prioritize is missing from [`TxGraph`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MissingTx(pub Txid); + +impl core::fmt::Display for MissingTx { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "missing transaction '{}'", self.0) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for MissingTx {} diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index a8d17ca91..e04dc0c57 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -9,7 +9,7 @@ use bdk_chain::{ indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, - tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, + tx_graph, Balance, CanonicalPos, ConfirmationBlockTime, DescriptorExt, LastSeenPrioritizer, }; use bdk_testenv::{ block_id, hash, @@ -191,8 +191,9 @@ fn test_list_owned_txouts() { value: Amount::from_sat(70000), script_pubkey: trusted_spks[0].to_owned(), }], - ..new_tx(0) + ..new_tx(1) }; + assert!(tx1.is_coinbase()); // tx2 is an incoming transaction received at untrusted keychain at block 1. let tx2 = Transaction { @@ -200,7 +201,7 @@ fn test_list_owned_txouts() { value: Amount::from_sat(30000), script_pubkey: untrusted_spks[0].to_owned(), }], - ..new_tx(0) + ..new_tx(2) }; // tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2. @@ -213,7 +214,7 @@ fn test_list_owned_txouts() { value: Amount::from_sat(10000), script_pubkey: trusted_spks[1].to_owned(), }], - ..new_tx(0) + ..new_tx(3) }; // tx4 is an external transaction receiving at untrusted keychain, unconfirmed. @@ -222,7 +223,7 @@ fn test_list_owned_txouts() { value: Amount::from_sat(20000), script_pubkey: untrusted_spks[1].to_owned(), }], - ..new_tx(0) + ..new_tx(4) }; // tx5 is an external transaction receiving at trusted keychain, unconfirmed. @@ -231,11 +232,11 @@ fn test_list_owned_txouts() { value: Amount::from_sat(15000), script_pubkey: trusted_spks[2].to_owned(), }], - ..new_tx(0) + ..new_tx(5) }; // tx6 is an unrelated transaction confirmed at 3. - let tx6 = new_tx(0); + let tx6 = new_tx(6); // Insert transactions into graph with respective anchors // Insert unconfirmed txs with a last_seen timestamp @@ -265,36 +266,25 @@ fn test_list_owned_txouts() { .get(height) .map(|cp| cp.block_id()) .unwrap_or_else(|| panic!("block must exist at {}", height)); - let txouts = graph + let view = graph .graph() - .filter_chain_txouts( - &local_chain, - chain_tip, - graph.index.outpoints().iter().cloned(), - ) - .collect::>(); + .canonical_view(&local_chain, chain_tip, &LastSeenPrioritizer); - let utxos = graph - .graph() - .filter_chain_unspents( - &local_chain, - chain_tip, - graph.index.outpoints().iter().cloned(), - ) + let txouts = view + .filter_txouts(graph.index.outpoints().iter().cloned()) .collect::>(); - - let balance = graph.graph().balance( - &local_chain, - chain_tip, - graph.index.outpoints().iter().cloned(), - |_, spk: ScriptBuf| trusted_spks.contains(&spk), - ); + let utxos = view + .filter_unspents(graph.index.outpoints().iter().cloned()) + .collect::>(); + let balance = view.balance(graph.index.outpoints().iter().cloned(), |_, spk| { + trusted_spks.contains(&spk.to_owned()) + }); let confirmed_txouts_txid = txouts .iter() - .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) { - Some(full_txout.outpoint.txid) + .filter_map(|canon_txout| { + if canon_txout.is_confirmed() { + Some(canon_txout.outpoint.txid) } else { None } @@ -303,20 +293,20 @@ fn test_list_owned_txouts() { let unconfirmed_txouts_txid = txouts .iter() - .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) { - Some(full_txout.outpoint.txid) - } else { + .filter_map(|canon_txout| { + if canon_txout.is_confirmed() { None + } else { + Some(canon_txout.outpoint.txid) } }) .collect::>(); let confirmed_utxos_txid = utxos .iter() - .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) { - Some(full_txout.outpoint.txid) + .filter_map(|canon_txout| { + if canon_txout.is_confirmed() { + Some(canon_txout.outpoint.txid) } else { None } @@ -325,11 +315,11 @@ fn test_list_owned_txouts() { let unconfirmed_utxos_txid = utxos .iter() - .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) { - Some(full_txout.outpoint.txid) - } else { + .filter_map(|canon_txout| { + if canon_txout.is_confirmed() { None + } else { + Some(canon_txout.outpoint.txid) } }) .collect::>(); @@ -539,7 +529,7 @@ fn test_get_chain_position() { tx: Transaction, anchor: Option, last_seen: Option, - exp_pos: Option>, + exp_pos: Option>, } // addr: bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm @@ -583,14 +573,12 @@ fn test_get_chain_position() { } // check chain position - let res = graph + let pos = graph .graph() - .get_chain_position(chain, chain.tip().block_id(), txid); - assert_eq!( - res.map(ChainPosition::cloned), - exp_pos, - "failed test case: {name}" - ); + .canonical_view(chain, chain.tip().block_id(), &LastSeenPrioritizer) + .tx(txid) + .map(|c_tx| c_tx.pos); + assert_eq!(pos, exp_pos, "failed test case: {name}"); } [ @@ -618,7 +606,7 @@ fn test_get_chain_position() { }, anchor: None, last_seen: Some(2), - exp_pos: Some(ChainPosition::Unconfirmed(2)), + exp_pos: Some(CanonicalPos::Unconfirmed(2)), }, TestCase { name: "tx anchor in best chain - confirmed", @@ -631,7 +619,7 @@ fn test_get_chain_position() { }, anchor: Some(blocks[1]), last_seen: None, - exp_pos: Some(ChainPosition::Confirmed(blocks[1])), + exp_pos: Some(CanonicalPos::Confirmed(blocks[1])), }, TestCase { name: "tx unknown anchor with last_seen - unconfirmed", @@ -644,7 +632,7 @@ fn test_get_chain_position() { }, anchor: Some(block_id!(2, "B'")), last_seen: Some(2), - exp_pos: Some(ChainPosition::Unconfirmed(2)), + exp_pos: Some(CanonicalPos::Unconfirmed(2)), }, TestCase { name: "tx unknown anchor - no chain pos", diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index c80735211..dd2a8bbdc 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,12 +2,14 @@ #[macro_use] mod common; -use bdk_chain::{collections::*, BlockId, ConfirmationBlockTime}; +use bdk_chain::{ + collections::*, BlockId, CanonicalPos, ConfirmationBlockTime, LastSeenPrioritizer, +}; use bdk_chain::{ local_chain::LocalChain, tx_graph::{self, CalculateFeeError}, tx_graph::{ChangeSet, TxGraph}, - Anchor, ChainOracle, ChainPosition, Merge, + Anchor, ChainOracle, Merge, }; use bdk_testenv::{block_id, hash, utils::new_tx}; use bitcoin::{ @@ -197,8 +199,8 @@ fn insert_tx_graph_keeps_track_of_spend() { let tx1 = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, - input: vec![], output: vec![TxOut::NULL], + ..new_tx(1) }; let op = OutPoint { @@ -213,7 +215,7 @@ fn insert_tx_graph_keeps_track_of_spend() { previous_output: op, ..Default::default() }], - output: vec![], + ..new_tx(2) }; let mut graph1 = TxGraph::<()>::default(); @@ -262,11 +264,11 @@ fn insert_tx_displaces_txouts() { let tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, - input: vec![], output: vec![TxOut { value: Amount::from_sat(42_000), script_pubkey: ScriptBuf::default(), }], + ..new_tx(0) }; let txid = tx.compute_txid(); let outpoint = OutPoint::new(txid, 0); @@ -288,11 +290,11 @@ fn insert_txout_does_not_displace_tx() { let tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, - input: vec![], output: vec![TxOut { value: Amount::from_sat(42_000), script_pubkey: ScriptBuf::new(), }], + ..new_tx(0) }; let _changeset = tx_graph.insert_tx(tx.clone()); @@ -344,20 +346,20 @@ fn test_calculate_fee() { let intx1 = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, - input: vec![], output: vec![TxOut { value: Amount::from_sat(100), script_pubkey: ScriptBuf::new(), }], + ..new_tx(1) }; let intx2 = Transaction { version: transaction::Version::TWO, lock_time: absolute::LockTime::ZERO, - input: vec![], output: vec![TxOut { value: Amount::from_sat(200), script_pubkey: ScriptBuf::new(), }], + ..new_tx(2) }; let intxout1 = ( @@ -811,7 +813,6 @@ fn test_chain_spends() { // The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx. // The parent tx is confirmed at block 95. let tx_0 = Transaction { - input: vec![], output: vec![ TxOut { value: Amount::from_sat(10_000), @@ -841,7 +842,7 @@ fn test_chain_spends() { script_pubkey: ScriptBuf::new(), }, ], - ..new_tx(0) + ..new_tx(1) }; // The second transactions spends vout:1, and is unconfirmed. @@ -860,7 +861,7 @@ fn test_chain_spends() { script_pubkey: ScriptBuf::new(), }, ], - ..new_tx(0) + ..new_tx(2) }; let mut graph = TxGraph::::default(); @@ -880,29 +881,19 @@ fn test_chain_spends() { } // Assert that confirmed spends are returned correctly. + let view = graph.canonical_view(&local_chain, tip.block_id(), &LastSeenPrioritizer); assert_eq!( - graph.get_chain_spend( - &local_chain, - tip.block_id(), - OutPoint::new(tx_0.compute_txid(), 0) - ), - Some(( - ChainPosition::Confirmed(&ConfirmationBlockTime { - block_id: BlockId { - hash: tip.get(98).unwrap().hash(), - height: 98, - }, - confirmation_time: 100 - }), - tx_1.compute_txid(), - )), + view.spend(OutPoint::new(tx_0.compute_txid(), 0)), + Some(tx_1.compute_txid()), ); // Check if chain position is returned correctly. assert_eq!( - graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()), - // Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))), - Some(ChainPosition::Confirmed(&ConfirmationBlockTime { + graph + .canonical_view(&local_chain, tip.block_id(), &LastSeenPrioritizer) + .tx(tx_0.compute_txid()) + .map(|c_tx| c_tx.pos), + Some(CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: BlockId { hash: tip.get(95).unwrap().hash(), height: 95, @@ -917,13 +908,9 @@ fn test_chain_spends() { // Check chain spend returned correctly. assert_eq!( graph - .get_chain_spend( - &local_chain, - tip.block_id(), - OutPoint::new(tx_0.compute_txid(), 1) - ) - .unwrap(), - (ChainPosition::Unconfirmed(1234567), tx_2.compute_txid()) + .canonical_view(&local_chain, tip.block_id(), &LastSeenPrioritizer) + .spend_with_pos(OutPoint::new(tx_0.compute_txid(), 1)), + Some((CanonicalPos::Unconfirmed(1234567), tx_2.compute_txid())) ); // A conflicting transaction that conflicts with tx_1. @@ -938,7 +925,8 @@ fn test_chain_spends() { // Because this tx conflicts with an already confirmed transaction, chain position should return none. assert!(graph - .get_chain_position(&local_chain, tip.block_id(), tx_1_conflict.compute_txid()) + .canonical_view(&local_chain, tip.block_id(), &LastSeenPrioritizer) + .tx(tx_1_conflict.compute_txid()) .is_none()); // Another conflicting tx that conflicts with tx_2. @@ -957,29 +945,27 @@ fn test_chain_spends() { // This should return a valid observation with correct last seen. assert_eq!( graph - .get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.compute_txid()) - .expect("position expected"), - ChainPosition::Unconfirmed(1234568) + .canonical_view(&local_chain, tip.block_id(), &LastSeenPrioritizer) + .tx(tx_2_conflict.compute_txid()) + .map(|c_tx| c_tx.pos), + Some(CanonicalPos::Unconfirmed(1234568)) ); // Chain_spend now catches the new transaction as the spend. assert_eq!( graph - .get_chain_spend( - &local_chain, - tip.block_id(), - OutPoint::new(tx_0.compute_txid(), 1) - ) - .expect("expect observation"), - ( - ChainPosition::Unconfirmed(1234568), + .canonical_view(&local_chain, tip.block_id(), &LastSeenPrioritizer) + .spend_with_pos(OutPoint::new(tx_0.compute_txid(), 1)), + Some(( + CanonicalPos::Unconfirmed(1234568), tx_2_conflict.compute_txid() - ) + )) ); // Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict` assert!(graph - .get_chain_position(&local_chain, tip.block_id(), tx_2.compute_txid()) + .canonical_view(&local_chain, tip.block_id(), &LastSeenPrioritizer) + .tx(tx_2.compute_txid()) .is_none()); } @@ -1018,7 +1004,27 @@ fn test_changeset_last_seen_merge() { #[test] fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anchor_in_best_chain() { - let txs = vec![new_tx(0), new_tx(1)]; + fn make_tx(n: u8) -> bitcoin::Transaction { + bitcoin::Transaction { + input: vec![ + TxIn { + previous_output: OutPoint::new(bitcoin::hashes::Hash::hash(&[n]), 0), + ..Default::default() + }, + TxIn { + previous_output: OutPoint::new(bitcoin::hashes::Hash::hash(&[n]), 2), + ..Default::default() + }, + ], + output: vec![TxOut { + value: bitcoin::Amount::from_sat(n as u64 * 1000), + script_pubkey: ScriptBuf::new(), + }], + ..new_tx(n as _) + } + } + + let txs = vec![make_tx(0), make_tx(1)]; let txids: Vec = txs.iter().map(Transaction::compute_txid).collect(); // graph @@ -1033,28 +1039,25 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch .into_iter() .collect(); let chain = LocalChain::from_blocks(blocks).unwrap(); - let canonical_txs: Vec<_> = graph - .list_canonical_txs(&chain, chain.tip().block_id()) - .collect(); - assert!(canonical_txs.is_empty()); + assert!(graph + .canonical_view(&chain, chain.tip().block_id(), &LastSeenPrioritizer) + .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()); - assert_eq!( - canonical_txs.next().map(|tx| tx.tx_node.txid).unwrap(), - txids[0] - ); - drop(canonical_txs); + { + let view = graph.canonical_view(&chain, chain.tip().block_id(), &LastSeenPrioritizer); + assert_eq!(view.len(), 1); + assert_eq!(view.txs().next().map(|c_tx| c_tx.txid), Some(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()) - .map(|tx| tx.tx_node.txid) - .collect(); - assert!(canonical_txids.contains(&txids[1])); - assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none()); + { + let view = graph.canonical_view(&chain, chain.tip().block_id(), &LastSeenPrioritizer); + assert!(view.tx(txids[1]).is_some()); + assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none()); + } } #[test] @@ -1149,15 +1152,6 @@ fn tx_graph_update_conversion() { type TestCase = (&'static str, TxUpdate); - fn make_tx(v: i32) -> Transaction { - Transaction { - version: transaction::Version(v), - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![], - } - } - fn make_txout(a: u64) -> TxOut { TxOut { value: Amount::from_sat(a), @@ -1170,21 +1164,21 @@ fn tx_graph_update_conversion() { ( "single_tx", TxUpdate { - txs: vec![make_tx(0).into()], + txs: vec![new_tx(0).into()], ..Default::default() }, ), ( "two_txs", TxUpdate { - txs: vec![make_tx(0).into(), make_tx(1).into()], + txs: vec![new_tx(0).into(), new_tx(1).into()], ..Default::default() }, ), ( "with_floating_txouts", TxUpdate { - txs: vec![make_tx(0).into(), make_tx(1).into()], + txs: vec![new_tx(0).into(), new_tx(1).into()], txouts: [ (OutPoint::new(hash!("a"), 0), make_txout(0)), (OutPoint::new(hash!("a"), 1), make_txout(1)), @@ -1197,7 +1191,7 @@ fn tx_graph_update_conversion() { ( "with_anchors", TxUpdate { - txs: vec![make_tx(0).into(), make_tx(1).into()], + txs: vec![new_tx(0).into(), new_tx(1).into()], txouts: [ (OutPoint::new(hash!("a"), 0), make_txout(0)), (OutPoint::new(hash!("a"), 1), make_txout(1)), @@ -1215,7 +1209,7 @@ fn tx_graph_update_conversion() { ( "with_seen_ats", TxUpdate { - txs: vec![make_tx(0).into(), make_tx(1).into()], + txs: vec![new_tx(0).into(), new_tx(1).into()], txouts: [ (OutPoint::new(hash!("a"), 0), make_txout(0)), (OutPoint::new(hash!("a"), 1), make_txout(1)), diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 1f54c4b82..ccd2c5ce2 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -3,9 +3,9 @@ #[macro_use] mod common; -use bdk_chain::{Balance, BlockId}; +use bdk_chain::{Balance, BlockId, LastSeenPrioritizer}; use bdk_testenv::{block_id, hash, local_chain}; -use bitcoin::{Amount, OutPoint, ScriptBuf}; +use bitcoin::{Amount, OutPoint}; use common::*; use std::collections::{BTreeSet, HashSet}; @@ -92,6 +92,7 @@ fn test_tx_conflict_handling() { tx_templates: &[ TxTemplate { tx_name: "tx1", + inputs: &[TxInTemplate::Coinbase], outputs: &[TxOutTemplate::new(40000, Some(0))], anchors: &[block_id!(1, "B")], last_seen: None, @@ -333,7 +334,9 @@ fn test_tx_conflict_handling() { tx_name: "B", inputs: &[TxInTemplate::PrevTx("A", 0)], outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(23), + // TODO: C shoud be seen alongside B (since B is a parent). + // last_seen: Some(23), + last_seen: Some(25), ..Default::default() }, TxTemplate { @@ -595,11 +598,9 @@ fn test_tx_conflict_handling() { for scenario in scenarios { let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter()); + let view = tx_graph.canonical_view(&local_chain, chain_tip, &LastSeenPrioritizer); - let txs = tx_graph - .list_canonical_txs(&local_chain, chain_tip) - .map(|tx| tx.tx_node.txid) - .collect::>(); + let txs = view.txs().map(|tx| tx.txid).collect::>(); let exp_txs = scenario .exp_chain_txs .iter() @@ -611,13 +612,9 @@ fn test_tx_conflict_handling() { scenario.name ); - let txouts = tx_graph - .filter_chain_txouts( - &local_chain, - chain_tip, - spk_index.outpoints().iter().cloned(), - ) - .map(|(_, full_txout)| full_txout.outpoint) + let txouts = view + .filter_txouts(spk_index.outpoints().iter().cloned()) + .map(|c_txo| c_txo.outpoint) .collect::>(); let exp_txouts = scenario .exp_chain_txouts @@ -633,13 +630,9 @@ fn test_tx_conflict_handling() { scenario.name ); - let utxos = tx_graph - .filter_chain_unspents( - &local_chain, - chain_tip, - spk_index.outpoints().iter().cloned(), - ) - .map(|(_, full_txout)| full_txout.outpoint) + let utxos = view + .filter_unspents(spk_index.outpoints().iter().cloned()) + .map(|c_txo| c_txo.outpoint) .collect::>(); let exp_utxos = scenario .exp_unspents @@ -655,12 +648,9 @@ fn test_tx_conflict_handling() { scenario.name ); - let balance = tx_graph.balance( - &local_chain, - chain_tip, - spk_index.outpoints().iter().cloned(), - |_, spk: ScriptBuf| spk_index.index_of_spk(spk).is_some(), - ); + let balance = view.balance(spk_index.outpoints().iter().cloned(), |_, spk| { + spk_index.index_of_spk(spk.to_owned()).is_some() + }); assert_eq!( balance, scenario.exp_balance, "\n[{}] 'balance' failed", diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 5f032ba6c..6ef15baaf 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -3,7 +3,7 @@ use bdk_chain::{ local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest, SyncResult}, spk_txout::SpkTxOutIndex, - Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph, + Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, LastSeenPrioritizer, Merge, TxGraph, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; @@ -22,7 +22,8 @@ fn get_balance( let outpoints = recv_graph.index.outpoints().clone(); let balance = recv_graph .graph() - .balance(recv_chain, chain_tip, outpoints, |_, _| true); + .canonical_view(recv_chain, chain_tip, &LastSeenPrioritizer) + .balance(outpoints, |_, _| true); Ok(balance) } diff --git a/crates/testenv/src/utils.rs b/crates/testenv/src/utils.rs index 93ca1f217..17cc62406 100644 --- a/crates/testenv/src/utils.rs +++ b/crates/testenv/src/utils.rs @@ -1,4 +1,4 @@ -use bdk_chain::bitcoin; +use bdk_chain::bitcoin::{self, TxIn}; #[allow(unused_macros)] #[macro_export] @@ -67,12 +67,21 @@ macro_rules! changeset { }}; } +/// Creates a "fake" transaction where `lt` is used to set the locktime and previous_output for +/// uniqueness. #[allow(unused)] pub fn new_tx(lt: u32) -> bitcoin::Transaction { bitcoin::Transaction { version: bitcoin::transaction::Version::non_standard(0x00), lock_time: bitcoin::absolute::LockTime::from_consensus(lt), - input: vec![], + // Enforce single input otherwise we can't determine root spends. + input: vec![TxIn { + previous_output: bitcoin::OutPoint::new( + bitcoin::hashes::Hash::hash(lt.to_le_bytes().as_slice()), + 0, + ), + ..Default::default() + }], output: vec![], } } diff --git a/crates/wallet/src/types.rs b/crates/wallet/src/types.rs index c4626fbf3..fdc738625 100644 --- a/crates/wallet/src/types.rs +++ b/crates/wallet/src/types.rs @@ -10,7 +10,7 @@ // licenses. use alloc::boxed::Box; -use chain::{ChainPosition, ConfirmationBlockTime}; +use chain::{CanonicalPos, ConfirmationBlockTime}; use core::convert::AsRef; use bitcoin::transaction::{OutPoint, Sequence, TxOut}; @@ -62,7 +62,7 @@ pub struct LocalOutput { /// The derivation index for the script pubkey in the wallet pub derivation_index: u32, /// The position of the output in the blockchain. - pub chain_position: ChainPosition, + pub chain_position: CanonicalPos, } /// A [`Utxo`] with its `satisfaction_weight`. diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 0f0e4a88e..b5ac08c37 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -278,7 +278,7 @@ impl CoinSelectionAlgorithm for OldestFirstCoinSelection { // For utxo that doesn't exist in DB, they will have lowest priority to be selected let utxos = { optional_utxos.sort_unstable_by_key(|wu| match &wu.utxo { - Utxo::Local(local) => Some(local.chain_position), + Utxo::Local(local) => Some(local.chain_position.clone()), Utxo::Foreign { .. } => None, }); @@ -734,7 +734,7 @@ where mod test { use assert_matches::assert_matches; use bitcoin::hashes::Hash; - use chain::{BlockId, ChainPosition, ConfirmationBlockTime}; + use chain::{BlockId, CanonicalPos, ChainPosition, ConfirmationBlockTime}; use core::str::FromStr; use rand::rngs::StdRng; @@ -754,7 +754,7 @@ mod test { const FEE_AMOUNT: u64 = 50; fn unconfirmed_utxo(value: u64, index: u32, last_seen: u64) -> WeightedUtxo { - utxo(value, index, ChainPosition::Unconfirmed(last_seen)) + utxo(value, index, CanonicalPos::Unconfirmed(last_seen)) } fn confirmed_utxo( @@ -766,7 +766,7 @@ mod test { utxo( value, index, - ChainPosition::Confirmed(ConfirmationBlockTime { + CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: chain::BlockId { height: confirmation_height, hash: bitcoin::BlockHash::all_zeros(), @@ -779,7 +779,7 @@ mod test { fn utxo( value: u64, index: u32, - chain_position: ChainPosition, + chain_position: CanonicalPos, ) -> WeightedUtxo { assert!(index < 10); let outpoint = OutPoint::from_str(&format!( @@ -838,7 +838,7 @@ mod test { is_spent: false, derivation_index: rng.next_u32(), chain_position: if rng.gen_bool(0.5) { - ChainPosition::Confirmed(ConfirmationBlockTime { + CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: chain::BlockId { height: rng.next_u32(), hash: BlockHash::all_zeros(), @@ -846,7 +846,7 @@ mod test { confirmation_time: rng.next_u64(), }) } else { - ChainPosition::Unconfirmed(0) + CanonicalPos::Unconfirmed(0) }, }), }); @@ -871,7 +871,7 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: 42, - chain_position: ChainPosition::Unconfirmed(0), + chain_position: CanonicalPos::Unconfirmed(0), }), }) .collect() @@ -1228,7 +1228,7 @@ mod test { optional.push(utxo( 500_000, 3, - ChainPosition::::Unconfirmed(0), + CanonicalPos::::Unconfirmed(0), )); // Defensive assertions, for sanity and in case someone changes the test utxos vector. @@ -1590,7 +1590,7 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: 0, - chain_position: ChainPosition::Confirmed(ConfirmationBlockTime { + chain_position: CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: BlockId { height: 12345, hash: BlockHash::all_zeros(), diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index ad5a6b2a8..2ddee1e19 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -128,12 +128,13 @@ impl FullyNodedExport { Self::is_compatible_with_core(&descriptor)?; let blockheight = if include_blockheight { - wallet.transactions().next().map_or(0, |canonical_tx| { - match canonical_tx.chain_position { - bdk_chain::ChainPosition::Confirmed(a) => a.block_id.height, - bdk_chain::ChainPosition::Unconfirmed(_) => 0, - } - }) + wallet + .transactions() + .next() + .map_or(0, |canonical_tx| match canonical_tx.pos { + bdk_chain::CanonicalPos::Confirmed(a) => a.block_id.height, + bdk_chain::CanonicalPos::Unconfirmed(_) => 0, + }) } else { 0 }; @@ -218,7 +219,7 @@ mod test { use crate::std::string::ToString; use bdk_chain::{BlockId, ConfirmationBlockTime}; use bitcoin::hashes::Hash; - use bitcoin::{transaction, BlockHash, Network, Transaction}; + use bitcoin::{transaction, BlockHash, Network, OutPoint, Transaction, TxIn}; use chain::tx_graph; use super::*; @@ -231,7 +232,10 @@ mod test { .create_wallet_no_persist() .expect("must create wallet"); let transaction = Transaction { - input: vec![], + input: vec![TxIn { + previous_output: OutPoint::new(bitcoin::hashes::Hash::hash(b"yoo"), 0), + ..Default::default() + }], output: vec![], version: transaction::Version::non_standard(0), lock_time: bitcoin::absolute::LockTime::ZERO, diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 562e684c1..6729fec75 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -33,9 +33,8 @@ use bdk_chain::{ FullScanRequest, FullScanRequestBuilder, FullScanResult, SyncRequest, SyncRequestBuilder, SyncResult, }, - tx_graph::{CanonicalTx, TxGraph, TxNode, TxUpdate}, - BlockId, ChainPosition, ConfirmationBlockTime, DescriptorExt, FullTxOut, Indexed, - IndexedTxGraph, Merge, + tx_graph::{TxGraph, TxNode, TxUpdate}, + BlockId, ConfirmationBlockTime, DescriptorExt, Indexed, IndexedTxGraph, Merge, }; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::{ @@ -45,6 +44,7 @@ use bitcoin::{ use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt}; use bitcoin::{constants::genesis_block, Amount}; use bitcoin::{secp256k1::Secp256k1, Weight}; +use chain::{CanonicalPos, CanonicalTx, CanonicalTxOut, CanonicalView, LastSeenPrioritizer}; use core::cmp::Ordering; use core::fmt; use core::mem; @@ -116,6 +116,7 @@ pub struct Wallet { change_signers: Arc, chain: LocalChain, indexed_graph: IndexedTxGraph>, + mempool_oracle: LastSeenPrioritizer, stage: ChangeSet, network: Network, secp: SecpCtx, @@ -291,7 +292,7 @@ impl fmt::Display for ApplyBlockError { impl std::error::Error for ApplyBlockError {} /// A `CanonicalTx` managed by a `Wallet`. -pub type WalletTx<'a> = CanonicalTx<'a, Arc, ConfirmationBlockTime>; +pub type WalletTx<'a> = CanonicalTx; impl Wallet { /// Build a new single descriptor [`Wallet`]. @@ -419,6 +420,8 @@ impl Wallet { let indexed_graph = IndexedTxGraph::new(index); let indexed_graph_changeset = indexed_graph.initial_changeset(); + let mempool_oracle = LastSeenPrioritizer; + let stage = ChangeSet { descriptor, change_descriptor, @@ -433,6 +436,7 @@ impl Wallet { change_signers, network, chain, + mempool_oracle, indexed_graph, stage, secp, @@ -610,6 +614,8 @@ impl Wallet { indexed_graph.apply_changeset(changeset.indexer.into()); indexed_graph.apply_changeset(changeset.tx_graph.into()); + let mempool_oracle = LastSeenPrioritizer; + let stage = ChangeSet::default(); Ok(Some(Wallet { @@ -617,6 +623,7 @@ impl Wallet { change_signers, chain, indexed_graph, + mempool_oracle, stage, network, secp, @@ -818,12 +825,13 @@ impl Wallet { pub fn list_unspent(&self) -> impl Iterator + '_ { self.indexed_graph .graph() - .filter_chain_unspents( + .canonical_view( &self.chain, self.chain.tip().block_id(), - self.indexed_graph.index.outpoints().iter().cloned(), + &self.mempool_oracle, ) - .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) + .into_filter_unspents(self.indexed_graph.index.outpoints().iter().cloned()) + .map(new_local_utxo) } /// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed). @@ -832,12 +840,13 @@ impl Wallet { pub fn list_output(&self) -> impl Iterator + '_ { self.indexed_graph .graph() - .filter_chain_txouts( + .canonical_view( &self.chain, self.chain.tip().block_id(), - self.indexed_graph.index.outpoints().iter().cloned(), + &self.mempool_oracle, ) - .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) + .into_filter_txouts(self.indexed_graph.index.outpoints().iter().cloned()) + .map(new_local_utxo) } /// Get all the checkpoints the wallet is currently storing indexed by height. @@ -885,12 +894,13 @@ impl Wallet { let ((keychain, index), _) = self.indexed_graph.index.txout(op)?; self.indexed_graph .graph() - .filter_chain_unspents( + .canonical_view( &self.chain, self.chain.tip().block_id(), - core::iter::once(((), op)), + &self.mempool_oracle, ) - .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo)) + .into_filter_unspents(core::iter::once(((keychain, index), op))) + .map(new_local_utxo) .next() } @@ -1052,15 +1062,13 @@ impl Wallet { /// [`Anchor`]: bdk_chain::Anchor pub fn get_tx(&self, txid: Txid) -> Option { let graph = self.indexed_graph.graph(); - - Some(WalletTx { - chain_position: graph.get_chain_position( + graph + .canonical_view( &self.chain, self.chain.tip().block_id(), - txid, - )?, - tx_node: graph.get_tx_node(txid)?, - }) + &self.mempool_oracle, + ) + .tx(txid) } /// Add a new checkpoint to the wallet's internal view of the chain. @@ -1103,9 +1111,16 @@ impl Wallet { /// Iterate over the transactions in the wallet. pub fn transactions(&self) -> impl Iterator + '_ { - self.indexed_graph - .graph() - .list_canonical_txs(&self.chain, self.chain.tip().block_id()) + self.view().into_txs() + } + + /// Get the canonical view of transactions. + pub fn view(&self) -> CanonicalView { + self.indexed_graph.graph().canonical_view( + &self.chain, + self.chain.tip().block_id(), + &self.mempool_oracle, + ) } /// Array of transactions in the wallet sorted with a comparator function. @@ -1132,9 +1147,7 @@ impl Wallet { /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature /// values. pub fn balance(&self) -> Balance { - self.indexed_graph.graph().balance( - &self.chain, - self.chain.tip().block_id(), + self.view().balance( self.indexed_graph.index.outpoints().iter().cloned(), |&(k, _), _| k == KeychainKind::Internal, ) @@ -1616,31 +1629,28 @@ impl Wallet { &mut self, txid: Txid, ) -> Result, BuildFeeBumpError> { + let view = self.view(); let graph = self.indexed_graph.graph(); let txout_index = &self.indexed_graph.index; let chain_tip = self.chain.tip().block_id(); - let mut tx = graph - .get_tx(txid) - .ok_or(BuildFeeBumpError::TransactionNotFound(txid))? - .as_ref() - .clone(); - - let pos = graph - .get_chain_position(&self.chain, chain_tip, txid) - .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?; - if let ChainPosition::Confirmed(_) = pos { - return Err(BuildFeeBumpError::TransactionConfirmed(txid)); - } - + let (txid, mut tx) = { + // The tx must be canonical to replace it. If it is not canonical, it means the tx was + // already replaced. + let canon_tx = view + .tx(txid) + .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?; + if canon_tx.pos.is_confirmed() { + return Err(BuildFeeBumpError::TransactionConfirmed(txid)); + } + (canon_tx.txid, canon_tx.tx.as_ref().clone()) + }; if !tx .input .iter() .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) { - return Err(BuildFeeBumpError::IrreplaceableTransaction( - tx.compute_txid(), - )); + return Err(BuildFeeBumpError::IrreplaceableTransaction(txid)); } let fee = self @@ -1655,53 +1665,43 @@ impl Wallet { let original_utxos = original_txin .iter() .map(|txin| -> Result<_, BuildFeeBumpError> { - let prev_tx = graph - .get_tx(txin.previous_output.txid) - .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?; - let txout = &prev_tx.output[txin.previous_output.vout as usize]; - - let chain_position = graph - .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid) - .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))? - .cloned(); - - let weighted_utxo = match txout_index.index_of_spk(txout.script_pubkey.clone()) { - Some(&(keychain, derivation_index)) => { - let satisfaction_weight = self - .public_descriptor(keychain) + let canon_utxo = view.txout(txin.previous_output, |op| { + txout_index.txout(op).map(|(k, _)| k) + }); + let weighted_utxo = match canon_utxo { + Some(canon_txo) => WeightedUtxo { + satisfaction_weight: self + .public_descriptor(canon_txo.spk_index.0) .max_weight_to_satisfy() - .unwrap(); - WeightedUtxo { - utxo: Utxo::Local(LocalOutput { - outpoint: txin.previous_output, - txout: txout.clone(), - keychain, - is_spent: true, - derivation_index, - chain_position, - }), - satisfaction_weight, - } - } + .expect("descriptor must be satisfiable"), + utxo: Utxo::Local(new_local_utxo(canon_txo)), + }, None => { - let satisfaction_weight = Weight::from_wu_usize( - serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(), - ); + let prev_tx = view + .tx(txin.previous_output.txid) + .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?; + let prev_txout = prev_tx + .tx + .output + .get(txin.previous_output.vout as usize) + .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?; WeightedUtxo { + satisfaction_weight: Weight::from_wu_usize( + serialize(&txin.script_sig).len() * 4 + + serialize(&txin.witness).len(), + ), utxo: Utxo::Foreign { outpoint: txin.previous_output, sequence: Some(txin.sequence), psbt_input: Box::new(psbt::Input { - witness_utxo: Some(txout.clone()), - non_witness_utxo: Some(prev_tx.as_ref().clone()), + witness_utxo: Some(prev_txout.clone()), + non_witness_utxo: Some(prev_tx.tx.as_ref().clone()), ..Default::default() }), }, - satisfaction_weight, } } }; - Ok(weighted_utxo) }) .collect::, _>>()?; @@ -1878,11 +1878,9 @@ impl Wallet { let confirmation_height = self .indexed_graph .graph() - .get_chain_position(&self.chain, chain_tip, input.previous_output.txid) - .map(|chain_position| match chain_position { - ChainPosition::Confirmed(a) => a.block_id.height, - ChainPosition::Unconfirmed(_) => u32::MAX, - }); + .confirmation_anchor(&self.chain, chain_tip, input.previous_output.txid) + .map(|a| a.block_id.height) + .unwrap_or(u32::MAX); let current_height = sign_options .assume_height .unwrap_or_else(|| self.chain.tip().height()); @@ -1910,7 +1908,7 @@ impl Wallet { ( PsbtInputSatisfier::new(psbt, n), After::new(Some(current_height), false), - Older::new(Some(current_height), confirmation_height, false), + Older::new(Some(current_height), Some(confirmation_height), false), ), ) { Ok(_) => { @@ -2024,6 +2022,7 @@ impl Wallet { let must_only_use_confirmed_tx = bumping_fee.is_some(); let must_use_all_available = *drain_wallet; + let view = self.view(); let chain_tip = self.chain.tip().block_id(); // must_spend <- manually selected utxos // may_spend <- all other available utxos @@ -2050,33 +2049,29 @@ impl Wallet { Some(tx) => tx, None => return false, }; - let chain_position = match self.indexed_graph.graph().get_chain_position( - &self.chain, - chain_tip, - txid, - ) { - Some(chain_position) => chain_position.cloned(), + let canonical_pos = match view.tx(txid) { None => return false, + Some(tx) => tx.pos, }; // Whether the UTXO is mature and, if needed, confirmed let mut spendable = true; - if must_only_use_confirmed_tx && !chain_position.is_confirmed() { + if must_only_use_confirmed_tx && !canonical_pos.is_confirmed() { return false; } if tx.is_coinbase() { debug_assert!( - chain_position.is_confirmed(), + canonical_pos.is_confirmed(), "coinbase must always be confirmed" ); if let Some(current_height) = current_height { - match chain_position { - ChainPosition::Confirmed(a) => { + match canonical_pos { + CanonicalPos::Confirmed(a) => { // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 spendable &= (current_height.saturating_sub(a.block_id.height)) >= COINBASE_MATURITY; } - ChainPosition::Unconfirmed { .. } => spendable = false, + CanonicalPos::Unconfirmed(_) => spendable = false, } } } @@ -2537,17 +2532,15 @@ where } fn new_local_utxo( - keychain: KeychainKind, - derivation_index: u32, - full_txo: FullTxOut, + canonical_txout: CanonicalTxOut, ) -> LocalOutput { LocalOutput { - outpoint: full_txo.outpoint, - txout: full_txo.txout, - is_spent: full_txo.spent_by.is_some(), - chain_position: full_txo.chain_position, - keychain, - derivation_index, + outpoint: canonical_txout.outpoint, + txout: canonical_txout.txout, + is_spent: canonical_txout.spent_by.is_some(), + chain_position: canonical_txout.pos, + keychain: canonical_txout.spk_index.0, + derivation_index: canonical_txout.spk_index.1, } } diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs index 343734a8d..910a46b85 100644 --- a/crates/wallet/src/wallet/tx_builder.rs +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -1017,7 +1017,7 @@ mod test { txout: TxOut::NULL, keychain: KeychainKind::External, is_spent: false, - chain_position: chain::ChainPosition::Unconfirmed(0), + chain_position: chain::CanonicalPos::Unconfirmed(0), derivation_index: 0, }, LocalOutput { @@ -1028,7 +1028,7 @@ mod test { txout: TxOut::NULL, keychain: KeychainKind::Internal, is_spent: false, - chain_position: chain::ChainPosition::Confirmed(chain::ConfirmationBlockTime { + chain_position: chain::CanonicalPos::Confirmed(chain::ConfirmationBlockTime { block_id: chain::BlockId { height: 32, hash: bitcoin::BlockHash::all_zeros(), diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index a2870c807..9426beb0a 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,5 +1,5 @@ #![allow(unused)] -use bdk_chain::{tx_graph, BlockId, ChainPosition, ConfirmationBlockTime, TxGraph}; +use bdk_chain::{tx_graph, BlockId, CanonicalPos, ChainPosition, ConfirmationBlockTime, TxGraph}; use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet}; use bitcoin::{ hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, @@ -89,7 +89,7 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, insert_anchor_from_conf( &mut wallet, tx0.compute_txid(), - ChainPosition::Confirmed(ConfirmationBlockTime { + CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: BlockId { height: 1_000, hash: BlockHash::all_zeros(), @@ -102,7 +102,7 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, insert_anchor_from_conf( &mut wallet, tx1.compute_txid(), - ChainPosition::Confirmed(ConfirmationBlockTime { + CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: BlockId { height: 2_000, hash: BlockHash::all_zeros(), @@ -214,9 +214,9 @@ pub fn feerate_unchecked(sat_vb: f64) -> FeeRate { pub fn insert_anchor_from_conf( wallet: &mut Wallet, txid: Txid, - position: ChainPosition, + position: CanonicalPos, ) { - if let ChainPosition::Confirmed(anchor) = position { + if let CanonicalPos::Confirmed(anchor) = position { wallet .apply_update(Update { tx_update: tx_graph::TxUpdate { diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 44b194a83..3874e35f1 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -2,11 +2,12 @@ extern crate alloc; use std::path::Path; use std::str::FromStr; +use std::sync::Mutex; use anyhow::Context; use assert_matches::assert_matches; -use bdk_chain::{tx_graph, COINBASE_MATURITY}; -use bdk_chain::{BlockId, ChainPosition, ConfirmationBlockTime}; +use bdk_chain::{tx_graph, CanonicalPos, COINBASE_MATURITY}; +use bdk_chain::{BlockId, ConfirmationBlockTime}; use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; use bdk_wallet::error::CreateTxError; @@ -35,22 +36,39 @@ use common::*; fn receive_output( wallet: &mut Wallet, value: u64, - height: ChainPosition, + height: CanonicalPos, ) -> OutPoint { let addr = wallet.next_unused_address(KeychainKind::External).address; receive_output_to_address(wallet, addr, value, height) } +static V: Mutex = Mutex::new(0_u32); +fn inc() -> u32 { + let v = &mut *V.lock().expect("must lock"); + *v += 1; + *v +} + +fn unique_outpoint() -> OutPoint { + OutPoint::new( + bitcoin::hashes::Hash::hash(inc().to_le_bytes().as_slice()), + 0, + ) +} + fn receive_output_to_address( wallet: &mut Wallet, addr: Address, value: u64, - height: ChainPosition, + height: CanonicalPos, ) -> OutPoint { let tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, - input: vec![], + input: vec![TxIn { + previous_output: unique_outpoint(), + ..Default::default() + }], output: vec![TxOut { script_pubkey: addr.script_pubkey(), value: Amount::from_sat(value), @@ -61,10 +79,10 @@ fn receive_output_to_address( wallet.insert_tx(tx); match height { - ChainPosition::Confirmed { .. } => { + CanonicalPos::Confirmed(_) => { insert_anchor_from_conf(wallet, txid, height); } - ChainPosition::Unconfirmed(last_seen) => { + CanonicalPos::Unconfirmed(last_seen) => { insert_seen_at(wallet, txid, last_seen); } } @@ -76,9 +94,9 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { let latest_cp = wallet.latest_checkpoint(); let height = latest_cp.height(); let anchor = if height == 0 { - ChainPosition::Unconfirmed(0) + CanonicalPos::Unconfirmed(0) } else { - ChainPosition::Confirmed(ConfirmationBlockTime { + CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: latest_cp.block_id(), confirmation_time: 0, }) @@ -383,11 +401,11 @@ fn test_get_funded_wallet_sent_and_received() { let mut tx_amounts: Vec<(Txid, (Amount, Amount))> = wallet .transactions() - .map(|ct| (ct.tx_node.txid, wallet.sent_and_received(&ct.tx_node))) + .map(|ct| (ct.txid, wallet.sent_and_received(&ct.tx))) .collect(); tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx = wallet.get_tx(txid).expect("transaction").tx; let (sent, received) = wallet.sent_and_received(&tx); // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 @@ -401,7 +419,7 @@ fn test_get_funded_wallet_sent_and_received() { fn test_get_funded_wallet_tx_fees() { let (wallet, txid) = get_funded_wallet_wpkh(); - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx = wallet.get_tx(txid).expect("transaction").tx; let tx_fee = wallet.calculate_fee(&tx).expect("transaction fee"); // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 @@ -414,7 +432,7 @@ fn test_get_funded_wallet_tx_fees() { fn test_get_funded_wallet_tx_fee_rate() { let (wallet, txid) = get_funded_wallet_wpkh(); - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx = wallet.get_tx(txid).expect("transaction").tx; let tx_fee_rate = wallet .calculate_fee_rate(&tx) .expect("transaction fee rate"); @@ -1204,7 +1222,10 @@ fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() { fn test_create_tx_add_utxo() { let (mut wallet, _) = get_funded_wallet_wpkh(); let small_output_tx = Transaction { - input: vec![], + input: vec![TxIn { + previous_output: unique_outpoint(), + ..Default::default() + }], output: vec![TxOut { script_pubkey: wallet .next_unused_address(KeychainKind::External) @@ -1216,7 +1237,7 @@ fn test_create_tx_add_utxo() { }; let txid = small_output_tx.compute_txid(); wallet.insert_tx(small_output_tx); - let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + let chain_position = CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: wallet.latest_checkpoint().get(2000).unwrap().block_id(), confirmation_time: 200, }); @@ -1251,7 +1272,10 @@ fn test_create_tx_add_utxo() { fn test_create_tx_manually_selected_insufficient() { let (mut wallet, _) = get_funded_wallet_wpkh(); let small_output_tx = Transaction { - input: vec![], + input: vec![TxIn { + previous_output: unique_outpoint(), + ..Default::default() + }], output: vec![TxOut { script_pubkey: wallet .next_unused_address(KeychainKind::External) @@ -1263,7 +1287,7 @@ fn test_create_tx_manually_selected_insufficient() { }; let txid = small_output_tx.compute_txid(); wallet.insert_tx(small_output_tx.clone()); - let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + let chain_position = CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: wallet.latest_checkpoint().get(2000).unwrap().block_id(), confirmation_time: 200, }); @@ -1305,7 +1329,10 @@ fn test_create_tx_policy_path_no_csv() { let tx = Transaction { version: transaction::Version::non_standard(0), lock_time: absolute::LockTime::ZERO, - input: vec![], + input: vec![TxIn { + previous_output: unique_outpoint(), + ..Default::default() + }], output: vec![TxOut { script_pubkey: wallet .next_unused_address(KeychainKind::External) @@ -1492,7 +1519,7 @@ fn test_create_tx_increment_change_index() { .create_wallet_no_persist() .unwrap(); // fund wallet - receive_output(&mut wallet, amount, ChainPosition::Unconfirmed(0)); + receive_output(&mut wallet, amount, CanonicalPos::Unconfirmed(0)); // create tx let mut builder = wallet.build_tx(); builder.add_recipient(recipient.clone(), Amount::from_sat(test.to_send)); @@ -1645,8 +1672,8 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); let utxo2 = wallet2.list_unspent().next().unwrap(); - let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); - let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); + let tx1 = wallet1.get_tx(txid1).unwrap().tx.clone(); + let tx2 = wallet2.get_tx(txid2).unwrap().tx.clone(); let satisfaction_weight = wallet2 .public_descriptor(KeychainKind::External) @@ -1733,7 +1760,7 @@ fn test_add_foreign_utxo_only_witness_utxo() { { let mut builder = builder.clone(); - let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx; + let tx2 = wallet2.get_tx(txid2).unwrap().tx; let psbt_input = psbt::Input { non_witness_utxo: Some(tx2.as_ref().clone()), ..Default::default() @@ -1824,7 +1851,7 @@ fn test_bump_fee_confirmed_tx() { wallet.insert_tx(tx); - let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + let chain_position = CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: wallet.latest_checkpoint().get(42).unwrap().block_id(), confirmation_time: 42_000, }); @@ -2091,7 +2118,10 @@ fn test_bump_fee_drain_wallet() { let tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, - input: vec![], + input: vec![TxIn { + previous_output: OutPoint::new(bitcoin::hashes::Hash::hash(b"prev_tx"), 0), + ..Default::default() + }], output: vec![TxOut { script_pubkey: wallet .next_unused_address(KeychainKind::External) @@ -2101,7 +2131,7 @@ fn test_bump_fee_drain_wallet() { }; let txid = tx.compute_txid(); wallet.insert_tx(tx.clone()); - let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + let chain_position = CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: wallet.latest_checkpoint().block_id(), confirmation_time: 42_000, }); @@ -2153,7 +2183,10 @@ fn test_bump_fee_remove_output_manually_selected_only() { let init_tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, - input: vec![], + input: vec![TxIn { + previous_output: unique_outpoint(), + ..Default::default() + }], output: vec![TxOut { script_pubkey: wallet .next_unused_address(KeychainKind::External) @@ -2161,12 +2194,7 @@ fn test_bump_fee_remove_output_manually_selected_only() { value: Amount::from_sat(25_000), }], }; - let position: ChainPosition = wallet - .transactions() - .last() - .unwrap() - .chain_position - .cloned(); + let position = wallet.transactions().last().unwrap().pos; wallet.insert_tx(init_tx.clone()); insert_anchor_from_conf(&mut wallet, init_tx.compute_txid(), position); @@ -2205,7 +2233,10 @@ fn test_bump_fee_add_input() { let init_tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, - input: vec![], + input: vec![TxIn { + previous_output: unique_outpoint(), + ..Default::default() + }], output: vec![TxOut { script_pubkey: wallet .next_unused_address(KeychainKind::External) @@ -2214,12 +2245,7 @@ fn test_bump_fee_add_input() { }], }; let txid = init_tx.compute_txid(); - let pos: ChainPosition = wallet - .transactions() - .last() - .unwrap() - .chain_position - .cloned(); + let pos = wallet.transactions().last().unwrap().pos; wallet.insert_tx(init_tx); insert_anchor_from_conf(&mut wallet, txid, pos); @@ -2614,7 +2640,7 @@ fn test_bump_fee_unconfirmed_inputs_only() { let psbt = builder.finish().unwrap(); // Now we receive one transaction with 0 confirmations. We won't be able to use that for // fee bumping, as it's still unconfirmed! - receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); + receive_output(&mut wallet, 25_000, CanonicalPos::Unconfirmed(0)); let mut tx = psbt.extract_tx().expect("failed to extract tx"); let txid = tx.compute_txid(); for txin in &mut tx.input { @@ -2640,7 +2666,7 @@ fn test_bump_fee_unconfirmed_input() { .assume_checked(); // We receive a tx with 0 confirmations, which will be used as an input // in the drain tx. - receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); + receive_output(&mut wallet, 25_000, CanonicalPos::Unconfirmed(0)); let mut builder = wallet.build_tx(); builder.drain_wallet().drain_to(addr.script_pubkey()); let psbt = builder.finish().unwrap(); @@ -3475,8 +3501,7 @@ fn test_taproot_sign_using_non_witness_utxo() { let mut psbt = builder.finish().unwrap(); psbt.inputs[0].witness_utxo = None; - psbt.inputs[0].non_witness_utxo = - Some(wallet.get_tx(prev_txid).unwrap().tx_node.as_ref().clone()); + psbt.inputs[0].non_witness_utxo = Some(wallet.get_tx(prev_txid).unwrap().tx.as_ref().clone()); assert!( psbt.inputs[0].non_witness_utxo.is_some(), "Previous tx should be present in the database" @@ -3865,7 +3890,7 @@ fn test_spend_coinbase() { }; let txid = coinbase_tx.compute_txid(); wallet.insert_tx(coinbase_tx); - let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + let chain_position = CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: confirmation_block_id, confirmation_time: 30_000, }); @@ -4111,7 +4136,7 @@ fn test_keychains_with_overlapping_spks() { .last() .unwrap() .address; - let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { + let chain_position = CanonicalPos::Confirmed(ConfirmationBlockTime { block_id: BlockId { height: 8000, hash: BlockHash::all_zeros(), @@ -4250,7 +4275,7 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() { .unwrap(); assert_eq!(wallet.keychains().count(), 1); let amt = Amount::from_sat(5_000); - receive_output(&mut wallet, 2 * amt.to_sat(), ChainPosition::Unconfirmed(2)); + receive_output(&mut wallet, 2 * amt.to_sat(), CanonicalPos::Unconfirmed(2)); // create spend tx that produces a change output let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm") .unwrap() @@ -4277,14 +4302,13 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() { #[test] fn test_transactions_sort_by() { let (mut wallet, _txid) = get_funded_wallet_wpkh(); - receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); + receive_output(&mut wallet, 25_000, CanonicalPos::Unconfirmed(0)); // sort by chain position, unconfirmed then confirmed by descending block height - let sorted_txs: Vec = - wallet.transactions_sort_by(|t1, t2| t2.chain_position.cmp(&t1.chain_position)); + let sorted_txs: Vec = wallet.transactions_sort_by(|t1, t2| t2.pos.cmp(&t1.pos)); let conf_heights: Vec> = sorted_txs .iter() - .map(|tx| tx.chain_position.confirmation_height_upper_bound()) + .map(|tx| tx.pos.confirmation_height_upper_bound()) .collect(); assert_eq!([None, Some(2000), Some(1000)], conf_heights.as_slice()); } diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index 95c547967..ede6d2770 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -13,7 +13,7 @@ use bdk_bitcoind_rpc::{ }; use bdk_chain::{ bitcoin::{Block, Transaction}, - local_chain, Merge, + local_chain, LastSeenPrioritizer, Merge, }; use example_cli::{ anyhow, @@ -182,14 +182,12 @@ fn main() -> anyhow::Result<()> { if last_print.elapsed() >= STDOUT_PRINT_DELAY { last_print = Instant::now(); let synced_to = chain.tip(); - let balance = { - graph.graph().balance( - &*chain, - synced_to.block_id(), - graph.index.outpoints().iter().cloned(), - |(k, _), _| k == &Keychain::Internal, - ) - }; + let balance = graph + .graph() + .canonical_view(&*chain, synced_to.block_id(), &LastSeenPrioritizer) + .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| { + k == &Keychain::Internal + }); println!( "[{:>10}s] synced to {} @ {} | total: {}", start.elapsed().as_secs_f32(), @@ -319,14 +317,12 @@ fn main() -> anyhow::Result<()> { if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY { last_print = Some(Instant::now()); let synced_to = chain.tip(); - let balance = { - graph.graph().balance( - &*chain, - synced_to.block_id(), - graph.index.outpoints().iter().cloned(), - |(k, _), _| k == &Keychain::Internal, - ) - }; + let balance = graph + .graph() + .canonical_view(&*chain, synced_to.block_id(), &LastSeenPrioritizer) + .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| { + k == &Keychain::Internal + }); println!( "[{:>10}s] synced to {} @ {} / {} | total: {}", start.elapsed().as_secs_f32(), diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 6a97252fc..abfc374e5 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -1,3 +1,6 @@ +use bdk_chain::CanonicalError; +use bdk_chain::CanonicalTxOut; +use bdk_chain::LastSeenPrioritizer; use serde_json::json; use std::cmp; use std::collections::HashMap; @@ -24,7 +27,7 @@ use bdk_chain::{ indexed_tx_graph, indexer::keychain_txout::{self, KeychainTxOutIndex}, local_chain::{self, LocalChain}, - tx_graph, ChainOracle, DescriptorExt, FullTxOut, IndexedTxGraph, Merge, + tx_graph, ChainOracle, DescriptorExt, IndexedTxGraph, Merge, }; use bdk_coin_select::{ metrics::LowestFee, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target, @@ -275,9 +278,9 @@ where plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.txout.value)) } CoinSelectionAlgo::SmallestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.txout.value), - CoinSelectionAlgo::OldestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.chain_position), + CoinSelectionAlgo::OldestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.pos.clone()), CoinSelectionAlgo::NewestFirst => { - plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.chain_position)) + plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.pos.clone())) } CoinSelectionAlgo::BranchAndBound => plan_utxos.shuffle(&mut thread_rng()), } @@ -410,7 +413,10 @@ where } // Alias the elements of `planned_utxos` -pub type PlanUtxo = (Plan, FullTxOut); +pub type PlanUtxo = ( + Plan, + CanonicalTxOut, +); pub fn planned_utxos( graph: &KeychainTxGraph, @@ -421,12 +427,11 @@ pub fn planned_utxos( let outpoints = graph.index.outpoints(); graph .graph() - .try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned()) - .filter_map(|r| -> Option> { - let (k, i, full_txo) = match r { - Err(err) => return Some(Err(err)), - Ok(((k, i), full_txo)) => (k, i, full_txo), - }; + .try_canonical_view(chain, chain_tip, &LastSeenPrioritizer) + .map_err(CanonicalError::into_chain_oracle_error)? + .into_filter_txouts(outpoints.iter().cloned()) + .filter_map(|canon_txo| -> Option> { + let (k, i) = canon_txo.spk_index; let desc = graph .index .keychains() @@ -438,7 +443,7 @@ pub fn planned_utxos( let plan = desc.plan(assets).ok()?; - Some(Ok((plan, full_txo))) + Some(Ok((plan, canon_txo))) }) .collect() } @@ -516,12 +521,12 @@ pub fn handle_commands( } } - let balance = graph.graph().try_balance( - chain, - chain.get_chain_tip()?, - graph.index.outpoints().iter().cloned(), - |(k, _), _| k == &Keychain::Internal, - )?; + let balance = graph + .graph() + .canonical_view(chain, chain.get_chain_tip()?, &LastSeenPrioritizer) + .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| { + k == &Keychain::Internal + }); let confirmed_total = balance.confirmed + balance.immature; let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending; @@ -560,32 +565,25 @@ pub fn handle_commands( } => { let txouts = graph .graph() - .try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned()) - .filter(|r| match r { - Ok((_, full_txo)) => match (spent, unspent) { - (true, false) => full_txo.spent_by.is_some(), - (false, true) => full_txo.spent_by.is_none(), - _ => true, - }, - // always keep errored items - Err(_) => true, + .canonical_view(chain, chain_tip, &LastSeenPrioritizer) + .into_filter_txouts(outpoints.iter().cloned()) + .filter(|canon_txo| match (spent, unspent) { + (true, false) => canon_txo.spent_by.is_some(), + (false, true) => canon_txo.spent_by.is_none(), + _ => true, }) - .filter(|r| match r { - Ok((_, full_txo)) => match (confirmed, unconfirmed) { - (true, false) => full_txo.chain_position.is_confirmed(), - (false, true) => !full_txo.chain_position.is_confirmed(), - _ => true, - }, - // always keep errored items - Err(_) => true, + .filter(|canon_txo| match (confirmed, unconfirmed) { + (true, false) => canon_txo.pos.is_confirmed(), + (false, true) => !canon_txo.pos.is_confirmed(), + _ => true, }) - .collect::, _>>()?; + .collect::>(); - for (spk_i, full_txo) in txouts { - let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?; + for txo in txouts { + let addr = Address::from_script(&txo.txout.script_pubkey, network)?; println!( "{:?} {} {} {} spent:{:?}", - spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by + txo.spk_index, txo.txout.value, txo.outpoint, addr, txo.spent_by ) } Ok(()) diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 9c705a3df..7dca13c0a 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -5,7 +5,7 @@ use bdk_chain::{ collections::BTreeSet, indexed_tx_graph, spk_client::{FullScanRequest, SyncRequest}, - ConfirmationBlockTime, Merge, + ConfirmationBlockTime, LastSeenPrioritizer, Merge, }; use bdk_electrum::{ electrum_client::{self, Client, ElectrumApi}, @@ -226,21 +226,19 @@ fn main() -> anyhow::Result<()> { request = request.outpoints( graph .graph() - .filter_chain_unspents( - &*chain, - chain_tip.block_id(), - init_outpoints.iter().cloned(), - ) - .map(|(_, utxo)| utxo.outpoint), + .canonical_view(&*chain, chain_tip.block_id(), &LastSeenPrioritizer) + .into_filter_unspents(init_outpoints.iter().cloned()) + .map(|utxo| utxo.outpoint), ); }; if unconfirmed { request = request.txids( graph .graph() - .list_canonical_txs(&*chain, chain_tip.block_id()) - .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) - .map(|canonical_tx| canonical_tx.tx_node.txid), + .canonical_view(&*chain, chain_tip.block_id(), &LastSeenPrioritizer) + .into_txs() + .filter(|canonical_tx| !canonical_tx.pos.is_confirmed()) + .map(|canonical_tx| canonical_tx.txid), ); } diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index cba86b862..c32a31c8c 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -8,7 +8,7 @@ use bdk_chain::{ bitcoin::Network, keychain_txout::FullScanRequestBuilderExt, spk_client::{FullScanRequest, SyncRequest}, - Merge, + LastSeenPrioritizer, Merge, }; use bdk_esplora::{esplora_client, EsploraExt}; use example_cli::{ @@ -240,12 +240,9 @@ fn main() -> anyhow::Result<()> { request = request.outpoints( graph .graph() - .filter_chain_unspents( - &*chain, - local_tip.block_id(), - init_outpoints.iter().cloned(), - ) - .map(|(_, utxo)| utxo.outpoint), + .canonical_view(&*chain, local_tip.block_id(), &LastSeenPrioritizer) + .into_filter_unspents(init_outpoints.iter().cloned()) + .map(|utxo| utxo.outpoint), ); }; if unconfirmed { @@ -255,9 +252,10 @@ fn main() -> anyhow::Result<()> { request = request.txids( graph .graph() - .list_canonical_txs(&*chain, local_tip.block_id()) - .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) - .map(|canonical_tx| canonical_tx.tx_node.txid), + .canonical_view(&*chain, local_tip.block_id(), &LastSeenPrioritizer) + .into_txs() + .filter(|canonical_tx| !canonical_tx.pos.is_confirmed()) + .map(|canonical_tx| canonical_tx.txid), ); } }