diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index ac6084cfc..2c13c1d93 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -738,6 +738,7 @@ mod test { use core::str::FromStr; use bdk_chain::ConfirmationTime; + use bitcoin::absolute::{Time, LOCK_TIME_THRESHOLD}; use bitcoin::{OutPoint, ScriptBuf, TxOut}; use super::*; @@ -780,13 +781,27 @@ mod test { fn get_test_utxos() -> Vec { vec![ - utxo(100_000, 0, ConfirmationTime::Unconfirmed { last_seen: 0 }), + utxo( + 100_000, + 0, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ), utxo( FEE_AMOUNT - 40, 1, - ConfirmationTime::Unconfirmed { last_seen: 0 }, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ), + utxo( + 200_000, + 2, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, ), - utxo(200_000, 2, ConfirmationTime::Unconfirmed { last_seen: 0 }), ] } @@ -797,7 +812,7 @@ mod test { 1, ConfirmationTime::Confirmed { height: 1, - time: 1231006505, + time: Time::from_consensus(1231006505).unwrap(), }, ); let utxo2 = utxo( @@ -805,7 +820,7 @@ mod test { 2, ConfirmationTime::Confirmed { height: 2, - time: 1231006505, + time: Time::from_consensus(1231006505).unwrap(), }, ); let utxo3 = utxo( @@ -813,7 +828,7 @@ mod test { 3, ConfirmationTime::Confirmed { height: 3, - time: 1231006505, + time: Time::from_consensus(1231006505).unwrap(), }, ); vec![utxo1, utxo2, utxo3] @@ -822,6 +837,8 @@ mod test { fn generate_random_utxos(rng: &mut StdRng, utxos_number: usize) -> Vec { let mut res = Vec::new(); for i in 0..utxos_number { + let random_time = + Time::from_consensus(rng.gen_range(LOCK_TIME_THRESHOLD..=u32::MAX)).unwrap(); res.push(WeightedUtxo { satisfaction_weight: P2WPKH_SATISFACTION_SIZE, utxo: Utxo::Local(LocalOutput { @@ -840,10 +857,12 @@ mod test { confirmation_time: if rng.gen_bool(0.5) { ConfirmationTime::Confirmed { height: rng.next_u32(), - time: rng.next_u64(), + time: random_time, } } else { - ConfirmationTime::Unconfirmed { last_seen: 0 } + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + } }, }), }); @@ -868,7 +887,9 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: 42, - confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, + confirmation_time: ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, }), }) .collect() @@ -1159,7 +1180,9 @@ mod test { optional.push(utxo( 500_000, 3, - ConfirmationTime::Unconfirmed { last_seen: 0 }, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, )); // Defensive assertions, for sanity and in case someone changes the test utxos vector. @@ -1520,7 +1543,7 @@ mod test { derivation_index: 0, confirmation_time: ConfirmationTime::Confirmed { height: 12345, - time: 12345, + time: Time::from_consensus(LOCK_TIME_THRESHOLD + 12345).unwrap(), }, }), } diff --git a/crates/bdk/src/wallet/export.rs b/crates/bdk/src/wallet/export.rs index f2d656891..2a243d10b 100644 --- a/crates/bdk/src/wallet/export.rs +++ b/crates/bdk/src/wallet/export.rs @@ -215,6 +215,7 @@ mod test { use core::str::FromStr; use bdk_chain::{BlockId, ConfirmationTime}; + use bitcoin::absolute::Time; use bitcoin::hashes::Hash; use bitcoin::{BlockHash, Network, Transaction}; @@ -244,7 +245,7 @@ mod test { transaction, ConfirmationTime::Confirmed { height: 5000, - time: 0, + time: Time::MIN, }, ) .unwrap(); diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 4db035fb6..e22cac190 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -2479,7 +2479,7 @@ impl Wallet { /// `last_seen` is prioritized. pub fn apply_unconfirmed_txs<'t>( &mut self, - unconfirmed_txs: impl IntoIterator, + unconfirmed_txs: impl IntoIterator, ) where D: PersistBackend, { @@ -2572,6 +2572,7 @@ fn create_signers( macro_rules! doctest_wallet { () => {{ use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; + use $crate::bitcoin::absolute::{LOCK_TIME_THRESHOLD, Time}; use $crate::chain::{ConfirmationTime, BlockId}; use $crate::wallet::{AddressIndex, Wallet}; let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; @@ -2596,7 +2597,7 @@ macro_rules! doctest_wallet { let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() }); let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed { height: 500, - time: 50_000 + time: Time::from_consensus(LOCK_TIME_THRESHOLD+50_000).unwrap(), }); wallet diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index 45d215fd8..65412b6d2 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -931,6 +931,7 @@ mod test { } use bdk_chain::ConfirmationTime; + use bitcoin::absolute::{Time, LOCK_TIME_THRESHOLD}; use bitcoin::consensus::deserialize; use bitcoin::hashes::hex::FromHex; @@ -1023,7 +1024,9 @@ mod test { txout: Default::default(), keychain: KeychainKind::External, is_spent: false, - confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 }, + confirmation_time: ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, derivation_index: 0, }, LocalOutput { @@ -1036,7 +1039,7 @@ mod test { is_spent: false, confirmation_time: ConfirmationTime::Confirmed { height: 32, - time: 42, + time: Time::from_consensus(LOCK_TIME_THRESHOLD + 42).unwrap(), }, derivation_index: 1, }, diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs index 3e0292a29..f2a9ff329 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -3,6 +3,7 @@ use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet}; use bdk_chain::indexed_tx_graph::Indexer; use bdk_chain::{BlockId, ConfirmationTime}; +use bitcoin::absolute::{Time, LOCK_TIME_THRESHOLD}; use bitcoin::hashes::Hash; use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid}; use std::str::FromStr; @@ -82,7 +83,7 @@ pub fn get_funded_wallet_with_change( tx0, ConfirmationTime::Confirmed { height: 1_000, - time: 100, + time: Time::from_consensus(LOCK_TIME_THRESHOLD + 100).unwrap(), }, ) .unwrap(); @@ -91,7 +92,7 @@ pub fn get_funded_wallet_with_change( tx1.clone(), ConfirmationTime::Confirmed { height: 2_000, - time: 200, + time: Time::from_consensus(LOCK_TIME_THRESHOLD + 200).unwrap(), }, ) .unwrap(); diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 271b87163..1ee20803c 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -12,6 +12,7 @@ use bdk::wallet::{AddressIndex::*, NewError}; use bdk::{FeeRate, KeychainKind}; use bdk_chain::COINBASE_MATURITY; use bdk_chain::{BlockId, ConfirmationTime}; +use bitcoin::absolute::{Time, LOCK_TIME_THRESHOLD}; use bitcoin::hashes::Hash; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::ScriptBuf; @@ -48,9 +49,14 @@ 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 } + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + } } else { - ConfirmationTime::Confirmed { height, time: 0 } + ConfirmationTime::Confirmed { + height, + time: Time::MIN, + } }; receive_output(wallet, value, anchor) } @@ -979,7 +985,9 @@ fn test_create_tx_add_utxo() { wallet .insert_tx( small_output_tx.clone(), - ConfirmationTime::Unconfirmed { last_seen: 0 }, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, ) .unwrap(); @@ -1025,7 +1033,9 @@ fn test_create_tx_manually_selected_insufficient() { wallet .insert_tx( small_output_tx.clone(), - ConfirmationTime::Unconfirmed { last_seen: 0 }, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, ) .unwrap(); @@ -1072,7 +1082,12 @@ fn test_create_tx_policy_path_no_csv() { }], }; wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); @@ -1450,7 +1465,12 @@ fn test_bump_fee_irreplaceable_tx() { let tx = psbt.extract_tx(); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); wallet.build_fee_bump(txid).unwrap().finish().unwrap(); } @@ -1472,7 +1492,7 @@ fn test_bump_fee_confirmed_tx() { tx, ConfirmationTime::Confirmed { height: 42, - time: 42_000, + time: Time::from_consensus(LOCK_TIME_THRESHOLD + 42_000).unwrap(), }, ) .unwrap(); @@ -1495,7 +1515,12 @@ fn test_bump_fee_low_fee_rate() { let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1518,7 +1543,12 @@ fn test_bump_fee_low_abs() { let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1540,7 +1570,12 @@ fn test_bump_fee_zero_abs() { let tx = psbt.extract_tx(); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1565,7 +1600,12 @@ fn test_bump_fee_reduce_change() { let tx = psbt.extract_tx(); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1660,7 +1700,12 @@ fn test_bump_fee_reduce_single_recipient() { let original_fee = check_fee!(wallet, psbt); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1699,7 +1744,12 @@ fn test_bump_fee_absolute_reduce_single_recipient() { let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1739,7 +1789,7 @@ fn test_bump_fee_drain_wallet() { tx.clone(), ConfirmationTime::Confirmed { height: wallet.latest_checkpoint().height(), - time: 42_000, + time: Time::from_consensus(LOCK_TIME_THRESHOLD + 42_000).unwrap(), }, ) .unwrap(); @@ -1763,7 +1813,12 @@ fn test_bump_fee_drain_wallet() { let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); assert_eq!(original_sent_received.0, 25_000); @@ -1830,7 +1885,12 @@ fn test_bump_fee_remove_output_manually_selected_only() { let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); assert_eq!(original_sent_received.0, 25_000); @@ -1874,7 +1934,12 @@ fn test_bump_fee_add_input() { let original_details = wallet.sent_and_received(&tx); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1924,7 +1989,12 @@ fn test_bump_fee_absolute_add_input() { let original_sent_received = wallet.sent_and_received(&tx); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1982,7 +2052,12 @@ fn test_bump_fee_no_change_add_input_and_change() { let tx = psbt.extract_tx(); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); // now bump the fees without using `allow_shrinking`. the wallet should add an @@ -2047,7 +2122,12 @@ fn test_bump_fee_add_input_change_dust() { assert_eq!(tx.output.len(), 2); let txid = tx.txid(); wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -2111,7 +2191,12 @@ fn test_bump_fee_force_add_input() { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature } wallet - .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx.clone(), + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); // the new fee_rate is low enough that just reducing the change would be fine, but we force // the addition of an extra input with `add_utxo()` @@ -2171,7 +2256,12 @@ fn test_bump_fee_absolute_force_add_input() { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature } wallet - .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx.clone(), + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); // the new fee_rate is low enough that just reducing the change would be fine, but we force @@ -2232,7 +2322,9 @@ fn test_bump_fee_unconfirmed_inputs_only() { receive_output( &mut wallet, 25_000, - ConfirmationTime::Unconfirmed { last_seen: 0 }, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, ); let mut tx = psbt.extract_tx(); let txid = tx.txid(); @@ -2240,7 +2332,12 @@ fn test_bump_fee_unconfirmed_inputs_only() { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature } wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_rate(FeeRate::from_sat_per_vb(25.0)); @@ -2260,7 +2357,11 @@ 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, + ConfirmationTime::unconfirmed(Time::MIN), + ); let mut builder = wallet.build_tx(); builder .drain_wallet() @@ -2273,7 +2374,12 @@ fn test_bump_fee_unconfirmed_input() { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature } wallet - .insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 }) + .insert_tx( + tx, + ConfirmationTime::Unconfirmed { + last_seen: Time::MIN, + }, + ) .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -3392,7 +3498,7 @@ fn test_spend_coinbase() { coinbase_tx, ConfirmationTime::Confirmed { height: confirmation_height, - time: 30_000, + time: Time::from_consensus(LOCK_TIME_THRESHOLD + 30_000).unwrap(), }, ) .unwrap(); diff --git a/crates/bitcoind_rpc/src/lib.rs b/crates/bitcoind_rpc/src/lib.rs index ce5e863bb..ea356df02 100644 --- a/crates/bitcoind_rpc/src/lib.rs +++ b/crates/bitcoind_rpc/src/lib.rs @@ -10,7 +10,7 @@ #![warn(missing_docs)] use bdk_chain::{local_chain::CheckPoint, BlockId}; -use bitcoin::{block::Header, Block, BlockHash, Transaction}; +use bitcoin::{absolute::Time, block::Header, Block, BlockHash, Transaction}; pub use bitcoincore_rpc; use bitcoincore_rpc::bitcoincore_rpc_json; @@ -35,7 +35,7 @@ pub struct Emitter<'c, C> { /// The latest first-seen epoch of emitted mempool transactions. This is used to determine /// whether a mempool transaction is already emitted. - last_mempool_time: usize, + last_mempool_time: Time, /// The last emitted block during our last mempool emission. This is used to determine whether /// there has been a reorg since our last mempool emission. @@ -56,7 +56,7 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> { start_height, last_cp, last_block: None, - last_mempool_time: 0, + last_mempool_time: Time::MIN, last_mempool_tip: None, } } @@ -71,7 +71,7 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> { /// tracked UTXO which is confirmed at height `h`, but the receiver has only seen up to block /// of height `h-1`, we want to re-emit this transaction until the receiver has seen the block /// at height `h`. - pub fn mempool(&mut self) -> Result, bitcoincore_rpc::Error> { + pub fn mempool(&mut self) -> Result, bitcoincore_rpc::Error> { let client = self.client; // This is the emitted tip height during the last mempool emission. @@ -94,7 +94,10 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> { .filter_map({ let latest_time = &mut latest_time; move |(txid, tx_entry)| -> Option> { - let tx_time = tx_entry.time as usize; + let tx_time = Time::from_consensus( + tx_entry.time.try_into().expect("consensus valid time"), + ) + .expect("consensus valid time"); if tx_time > *latest_time { *latest_time = tx_time; } @@ -117,7 +120,7 @@ impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> { Err(err) => return Some(Err(err)), }; - Some(Ok((tx, tx_time as u64))) + Some(Ok((tx, tx_time))) } }) .collect::, _>>()?; diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index ae0976de5..d108233a9 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -1,3 +1,4 @@ +use bitcoin::absolute::Time; use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY}; @@ -10,7 +11,7 @@ pub enum ChainPosition { /// The chain data is seen as confirmed, and in anchored by `A`. Confirmed(A), /// The chain data is not confirmed and last seen in the mempool at this timestamp. - Unconfirmed(u64), + Unconfirmed(Time), } impl ChainPosition { @@ -53,18 +54,18 @@ pub enum ConfirmationTime { /// Confirmation height. height: u32, /// Confirmation time in unix seconds. - time: u64, + time: Time, }, /// The transaction is unconfirmed Unconfirmed { /// The last-seen timestamp in unix seconds. - last_seen: u64, + last_seen: Time, }, } impl ConfirmationTime { /// Construct an unconfirmed variant using the given `last_seen` time in unix seconds. - pub fn unconfirmed(last_seen: u64) -> Self { + pub fn unconfirmed(last_seen: Time) -> Self { Self::Unconfirmed { last_seen } } @@ -190,7 +191,7 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor { /// Note that the confirmation block and the anchor block can be different here. /// /// Refer to [`Anchor`] for more details. -#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] #[cfg_attr( feature = "serde", derive(serde::Deserialize, serde::Serialize), @@ -200,11 +201,21 @@ pub struct ConfirmationTimeHeightAnchor { /// The confirmation height of the transaction being anchored. pub confirmation_height: u32, /// The confirmation time of the transaction being anchored. - pub confirmation_time: u64, + pub confirmation_time: Time, /// The anchor block. pub anchor_block: BlockId, } +impl Default for ConfirmationTimeHeightAnchor { + fn default() -> Self { + Self { + confirmation_height: 0, + confirmation_time: Time::MIN, + anchor_block: Default::default(), + } + } +} + impl Anchor for ConfirmationTimeHeightAnchor { fn anchor_block(&self) -> BlockId { self.anchor_block @@ -220,7 +231,7 @@ impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor { Self { anchor_block: block_id, confirmation_height: block_id.height, - confirmation_time: block.header.time as _, + confirmation_time: Time::from_consensus(block.header.time).expect("consensus time"), } } } @@ -302,11 +313,16 @@ impl FullTxOut { #[cfg(test)] mod test { use super::*; + use bitcoin::absolute::LOCK_TIME_THRESHOLD; #[test] fn chain_position_ord() { - let unconf1 = ChainPosition::::Unconfirmed(10); - let unconf2 = ChainPosition::::Unconfirmed(20); + let unconf1 = ChainPosition::::Unconfirmed( + Time::from_consensus(LOCK_TIME_THRESHOLD + 10).unwrap(), + ); + let unconf2 = ChainPosition::::Unconfirmed( + Time::from_consensus(LOCK_TIME_THRESHOLD + 20).unwrap(), + ); let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor { confirmation_height: 9, anchor_block: BlockId { diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index c2b83600b..1298f9594 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,6 +1,7 @@ //! Contains the [`IndexedTxGraph`] and associated types. Refer to the //! [`IndexedTxGraph`] documentation for more. use alloc::vec::Vec; +use bitcoin::absolute::Time; use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; use crate::{ @@ -116,7 +117,7 @@ where /// /// This is used for transaction conflict resolution in [`TxGraph`] where the transaction with /// the later last-seen is prioritized. - pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { + pub fn insert_seen_at(&mut self, txid: Txid, seen_at: Time) -> ChangeSet { self.graph.insert_seen_at(txid, seen_at).into() } @@ -165,7 +166,7 @@ where /// conflict-resolution in [`TxGraph`] (refer to [`TxGraph::insert_seen_at`] for details). pub fn batch_insert_relevant_unconfirmed<'t>( &mut self, - unconfirmed_txs: impl IntoIterator, + unconfirmed_txs: impl IntoIterator, ) -> ChangeSet { // The algorithm below allows for non-topologically ordered transactions by using two loops. // This is achieved by: @@ -200,7 +201,7 @@ where /// [`batch_insert_relevant_unconfirmed`]: IndexedTxGraph::batch_insert_relevant_unconfirmed pub fn batch_insert_unconfirmed( &mut self, - txs: impl IntoIterator, + txs: impl IntoIterator, ) -> ChangeSet { let graph = self.graph.batch_insert_unconfirmed(txs); let indexer = self.index_tx_graph_changeset(&graph); diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 8fa17ff90..140c160d6 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -17,7 +17,8 @@ use alloc::vec::Vec; /// /// The example shows different types of anchors: /// ``` -/// # use bdk_chain::local_chain::LocalChain; +/// # use bitcoin::absolute::{LOCK_TIME_THRESHOLD, Time}; +/// use bdk_chain::local_chain::LocalChain; /// # use bdk_chain::tx_graph::TxGraph; /// # use bdk_chain::BlockId; /// # use bdk_chain::ConfirmationHeightAnchor; @@ -83,7 +84,7 @@ use alloc::vec::Vec; /// hash: Hash::hash("third".as_bytes()), /// }, /// confirmation_height: 1, -/// confirmation_time: 123, +/// confirmation_time: Time::from_consensus(LOCK_TIME_THRESHOLD + 123).unwrap(), /// }, /// ); /// ``` diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 34cbccf5c..3aa4e3797 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -81,6 +81,7 @@ use crate::{ }; use alloc::collections::vec_deque::VecDeque; use alloc::vec::Vec; +use bitcoin::absolute::Time; use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use core::fmt::{self, Formatter}; use core::{ @@ -96,7 +97,7 @@ use core::{ #[derive(Clone, Debug, PartialEq)] pub struct TxGraph { // all transactions that the graph is aware of in format: `(tx_node, tx_anchors, tx_last_seen)` - txs: HashMap, u64)>, + txs: HashMap, Time)>, spends: BTreeMap>, anchors: BTreeSet<(A, Txid)>, @@ -126,7 +127,7 @@ pub struct TxNode<'a, T, A> { /// The blocks that the transaction is "anchored" in. pub anchors: &'a BTreeSet, /// The last-seen unix timestamp of the transaction as unconfirmed. - pub last_seen_unconfirmed: u64, + pub last_seen_unconfirmed: Time, } impl<'a, T, A> Deref for TxNode<'a, T, A> { @@ -495,7 +496,7 @@ impl TxGraph { ( TxNodeInternal::Partial([(outpoint.vout, txout)].into()), BTreeSet::new(), - 0, + Time::MIN, ), ); self.apply_update(update) @@ -506,9 +507,10 @@ impl TxGraph { /// The [`ChangeSet`] returned will be empty if `tx` already exists. pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet { let mut update = Self::default(); - update - .txs - .insert(tx.txid(), (TxNodeInternal::Whole(tx), BTreeSet::new(), 0)); + update.txs.insert( + tx.txid(), + (TxNodeInternal::Whole(tx), BTreeSet::new(), Time::MIN), + ); self.apply_update(update) } @@ -519,7 +521,7 @@ impl TxGraph { /// conflict-resolution (refer to [`TxGraph::insert_seen_at`] for details). pub fn batch_insert_unconfirmed( &mut self, - txs: impl IntoIterator, + txs: impl IntoIterator, ) -> ChangeSet { let mut changeset = ChangeSet::::default(); for (tx, seen_at) in txs { @@ -542,9 +544,13 @@ impl TxGraph { /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. /// /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. - pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { + pub fn insert_seen_at(&mut self, txid: Txid, seen_at: Time) -> ChangeSet { let mut update = Self::default(); - let (_, _, update_last_seen) = update.txs.entry(txid).or_default(); + let (_, _, update_last_seen) = + update + .txs + .entry(txid) + .or_insert((Default::default(), Default::default(), Time::MIN)); *update_last_seen = seen_at; self.apply_update(update) } @@ -592,14 +598,20 @@ impl TxGraph { ); } None => { - self.txs - .insert(txid, (TxNodeInternal::Whole(tx), BTreeSet::new(), 0)); + self.txs.insert( + txid, + (TxNodeInternal::Whole(tx), BTreeSet::new(), Time::MIN), + ); } } } for (outpoint, txout) in changeset.txouts { - let tx_entry = self.txs.entry(outpoint.txid).or_default(); + let tx_entry = self.txs.entry(outpoint.txid).or_insert(( + Default::default(), + Default::default(), + Time::MIN, + )); match tx_entry { (TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */ @@ -612,13 +624,20 @@ impl TxGraph { for (anchor, txid) in changeset.anchors { if self.anchors.insert((anchor.clone(), txid)) { - let (_, anchors, _) = self.txs.entry(txid).or_default(); + let (_, anchors, _) = self.txs.entry(txid).or_insert(( + Default::default(), + Default::default(), + Time::MIN, + )); anchors.insert(anchor); } } for (txid, new_last_seen) in changeset.last_seen { - let (_, _, last_seen) = self.txs.entry(txid).or_default(); + let (_, _, last_seen) = + self.txs + .entry(txid) + .or_insert((Default::default(), Default::default(), Time::MIN)); if new_last_seen > *last_seen { *last_seen = new_last_seen; } @@ -633,10 +652,10 @@ impl TxGraph { let mut changeset = ChangeSet::default(); for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs { - let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) { + let prev_last_seen: Time = match (self.txs.get(&txid), update_tx_node) { (None, TxNodeInternal::Whole(update_tx)) => { changeset.txs.insert(update_tx.clone()); - 0 + Time::MIN } (None, TxNodeInternal::Partial(update_txos)) => { changeset.txouts.extend( @@ -644,7 +663,7 @@ impl TxGraph { .iter() .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), ); - 0 + Time::MIN } (Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen, ( @@ -1215,7 +1234,7 @@ pub struct ChangeSet { /// Added anchors. pub anchors: BTreeSet<(A, Txid)>, /// Added last-seen unix timestamps of transactions. - pub last_seen: BTreeMap, + pub last_seen: BTreeMap, } impl Default for ChangeSet { diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs index ec2eb1159..55619b96c 100644 --- a/crates/chain/tests/common/tx_template.rs +++ b/crates/chain/tests/common/tx_template.rs @@ -1,7 +1,8 @@ use rand::distributions::{Alphanumeric, DistString}; use std::collections::HashMap; -use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex}; +use bdk_chain::{tx_graph::TxGraph, BlockId, SpkTxOutIndex}; +use bitcoin::absolute::Time; use bitcoin::{ locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, @@ -20,7 +21,7 @@ pub struct TxTemplate<'a, A> { pub inputs: &'a [TxInTemplate<'a>], pub outputs: &'a [TxOutTemplate], pub anchors: &'a [A], - pub last_seen: Option, + pub last_seen: Option, SpkTxOutIndex, HashMap<&'a str, Txid>) { +pub fn init_graph<'a>( + tx_templates: impl IntoIterator>, +) -> (TxGraph, SpkTxOutIndex, HashMap<&'a str, Txid>) { let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap(); - let mut graph = TxGraph::::default(); + let mut graph = TxGraph::::default(); let mut spk_index = SpkTxOutIndex::default(); (0..10).for_each(|index| { spk_index.insert_spk( @@ -126,7 +127,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( spk_index.scan(&tx); let _ = graph.insert_tx(tx.clone()); for anchor in tx_tmp.anchors.iter() { - let _ = graph.insert_anchor(tx.txid(), anchor.clone()); + let _ = graph.insert_anchor(tx.txid(), *anchor); } if let Some(seen_at) = tx_tmp.last_seen { let _ = graph.insert_seen_at(tx.txid(), seen_at); diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 41b1d4d3e..de63a80ff 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -9,6 +9,7 @@ use bdk_chain::{ local_chain::LocalChain, tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor, }; +use bitcoin::absolute::{Time, LOCK_TIME_THRESHOLD}; use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut}; use miniscript::Descriptor; @@ -224,7 +225,12 @@ fn test_list_owned_txouts() { ) })); - let _ = graph.batch_insert_relevant_unconfirmed([&tx4, &tx5].iter().map(|tx| (*tx, 100))); + let _ = graph.batch_insert_relevant_unconfirmed([&tx4, &tx5].iter().map(|tx| { + ( + *tx, + Time::from_consensus(LOCK_TIME_THRESHOLD + 100).unwrap(), + ) + })); // A helper lambda to extract and filter data from the graph. let fetch = diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 37e8c7192..b0eb4b2cb 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -7,6 +7,7 @@ use bdk_chain::{ tx_graph::{ChangeSet, TxGraph}, Anchor, Append, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor, }; +use bitcoin::absolute::{Time, LOCK_TIME_THRESHOLD}; use bitcoin::{ absolute, hashes::Hash, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, }; @@ -65,7 +66,9 @@ fn insert_txouts() { }); // Unconfirmed anchor to mark the partial transactions as unconfirmed - let unconf_anchor = ChainPosition::::Unconfirmed(1000000); + let unconf_anchor = ChainPosition::::Unconfirmed( + Time::from_consensus(LOCK_TIME_THRESHOLD + 1000000).unwrap(), + ); // Make the original graph let mut graph = { @@ -106,12 +109,19 @@ fn insert_txouts() { ); // Mark them last seen at. assert_eq!( - graph.insert_seen_at(outpoint.txid, 1000000), + graph.insert_seen_at( + outpoint.txid, + Time::from_consensus(LOCK_TIME_THRESHOLD + 1000000).unwrap() + ), ChangeSet { txs: [].into(), txouts: [].into(), anchors: [].into(), - last_seen: [(outpoint.txid, 1000000)].into() + last_seen: [( + outpoint.txid, + Time::from_consensus(LOCK_TIME_THRESHOLD + 1000000).unwrap() + )] + .into() } ); } @@ -146,7 +156,11 @@ fn insert_txouts() { txs: [update_txs.clone()].into(), txouts: update_ops.clone().into(), anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(), - last_seen: [(h!("tx2"), 1000000)].into() + last_seen: [( + h!("tx2"), + Time::from_consensus(LOCK_TIME_THRESHOLD + 1000000).unwrap() + )] + .into() } ); @@ -197,7 +211,11 @@ fn insert_txouts() { txs: [update_txs.clone()].into(), txouts: update_ops.into_iter().chain(original_ops).collect(), anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(), - last_seen: [(h!("tx2"), 1000000)].into() + last_seen: [( + h!("tx2"), + Time::from_consensus(LOCK_TIME_THRESHOLD + 1000000).unwrap() + )] + .into() } ); } @@ -951,18 +969,26 @@ fn test_chain_spends() { // Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend. assert_eq!( graph.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1)), - Some((ChainPosition::Unconfirmed(0), tx_2.txid())), + Some((ChainPosition::Unconfirmed(Time::MIN), tx_2.txid())), ); // Mark the unconfirmed as seen and check correct ObservedAs status is returned. - let _ = graph.insert_seen_at(tx_2.txid(), 1234567); + let _ = graph.insert_seen_at( + tx_2.txid(), + Time::from_consensus(LOCK_TIME_THRESHOLD + 1234567).unwrap(), + ); // Check chain spend returned correctly. assert_eq!( graph .get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1)) .unwrap(), - (ChainPosition::Unconfirmed(1234567), tx_2.txid()) + ( + ChainPosition::Unconfirmed( + Time::from_consensus(LOCK_TIME_THRESHOLD + 1234567).unwrap() + ), + tx_2.txid() + ) ); // A conflicting transaction that conflicts with tx_1. @@ -991,14 +1017,17 @@ fn test_chain_spends() { // Insert in graph and mark it as seen. let _ = graph.insert_tx(tx_2_conflict.clone()); - let _ = graph.insert_seen_at(tx_2_conflict.txid(), 1234568); + let _ = graph.insert_seen_at( + tx_2_conflict.txid(), + Time::from_consensus(LOCK_TIME_THRESHOLD + 1234568).unwrap(), + ); // This should return a valid observation with correct last seen. assert_eq!( graph .get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.txid()) .expect("position expected"), - ChainPosition::Unconfirmed(1234568) + ChainPosition::Unconfirmed(Time::from_consensus(LOCK_TIME_THRESHOLD + 1234568).unwrap()) ); // Chain_spend now catches the new transaction as the spend. @@ -1006,7 +1035,12 @@ fn test_chain_spends() { graph .get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1)) .expect("expect observation"), - (ChainPosition::Unconfirmed(1234568), tx_2_conflict.txid()) + ( + ChainPosition::Unconfirmed( + Time::from_consensus(LOCK_TIME_THRESHOLD + 1234568).unwrap() + ), + tx_2_conflict.txid() + ) ); // Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict` @@ -1020,12 +1054,27 @@ fn test_chain_spends() { fn test_changeset_last_seen_append() { let txid: Txid = h!("test txid"); - let test_cases: &[(Option, Option)] = &[ - (Some(5), Some(6)), - (Some(5), Some(5)), - (Some(6), Some(5)), - (None, Some(5)), - (Some(5), None), + let test_cases: &[(Option