From 8605283fae34ab354da233c0e20349c0da08f676 Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Wed, 17 Jul 2024 21:44:39 +0800 Subject: [PATCH] refactor(chain)!: Remove `Anchor` trait The `Anchor` trait has been removed and anchors are now unique to (`Txid`, `BlockId`). --- crates/bitcoind_rpc/tests/test_emitter.rs | 22 +- crates/chain/src/chain_data.rs | 144 +++-------- crates/chain/src/changeset.rs | 5 +- crates/chain/src/indexed_tx_graph.rs | 47 ++-- crates/chain/src/spk_client.rs | 8 +- crates/chain/src/tx_data_traits.rs | 112 +++------ crates/chain/src/tx_graph.rs | 123 ++++----- crates/chain/tests/common/mod.rs | 16 ++ crates/chain/tests/common/tx_template.rs | 16 +- crates/chain/tests/test_indexed_tx_graph.rs | 234 ++++++++++-------- crates/chain/tests/test_tx_graph.rs | 194 ++++----------- crates/chain/tests/test_tx_graph_conflicts.rs | 46 ++-- crates/electrum/src/bdk_electrum_client.rs | 47 ++-- crates/electrum/tests/test_electrum.rs | 36 ++- crates/esplora/src/async_ext.rs | 69 +++--- crates/esplora/src/blocking_ext.rs | 79 +++--- crates/esplora/src/lib.rs | 12 +- crates/sqlite/src/store.rs | 111 +++++---- crates/wallet/src/wallet/export.rs | 11 +- crates/wallet/src/wallet/mod.rs | 51 ++-- crates/wallet/tests/common.rs | 11 +- .../example_bitcoind_rpc_polling/src/main.rs | 4 +- example-crates/example_cli/src/lib.rs | 10 +- example-crates/example_electrum/src/main.rs | 7 +- example-crates/example_esplora/src/main.rs | 4 +- 25 files changed, 628 insertions(+), 791 deletions(-) diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index d7c8b60f7..bc2901457 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -4,7 +4,7 @@ use bdk_bitcoind_rpc::Emitter; use bdk_chain::{ bitcoin::{Address, Amount, Txid}, local_chain::{CheckPoint, LocalChain}, - Balance, BlockId, IndexedTxGraph, Merge, SpkTxOutIndex, + Balance, BlockId, BlockTime, IndexedTxGraph, Merge, SpkTxOutIndex, }; use bdk_testenv::{anyhow, TestEnv}; use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash}; @@ -149,7 +149,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> { println!("mined blocks!"); let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?); - let mut indexed_tx_graph = IndexedTxGraph::::new({ + let mut indexed_tx_graph = IndexedTxGraph::new({ let mut index = SpkTxOutIndex::::default(); index.insert_spk(0, addr_0.script_pubkey()); index.insert_spk(1, addr_1.script_pubkey()); @@ -206,17 +206,19 @@ fn test_into_tx_graph() -> anyhow::Result<()> { // mine a block that confirms the 3 txs let exp_block_hash = env.mine_blocks(1, None)?[0]; - let exp_block_height = env.rpc_client().get_block_info(&exp_block_hash)?.height as u32; + let exp_block = env.rpc_client().get_block_info(&exp_block_hash)?; + let exp_block_height = exp_block.height as u32; + let exp_block_time = exp_block.time as u32; let exp_anchors = exp_txids .iter() .map({ - let anchor = BlockId { + let blockid = BlockId { height: exp_block_height, hash: exp_block_hash, }; - move |&txid| (anchor, txid) + move |&txid| ((txid, blockid), BlockTime::new(exp_block_time)) }) - .collect::>(); + .collect::>(); // must receive mined block which will confirm the transactions. { @@ -277,7 +279,7 @@ fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> { fn process_block( recv_chain: &mut LocalChain, - recv_graph: &mut IndexedTxGraph>, + recv_graph: &mut IndexedTxGraph>, block: Block, block_height: u32, ) -> anyhow::Result<()> { @@ -288,7 +290,7 @@ fn process_block( fn sync_from_emitter( recv_chain: &mut LocalChain, - recv_graph: &mut IndexedTxGraph>, + recv_graph: &mut IndexedTxGraph>, emitter: &mut Emitter, ) -> anyhow::Result<()> where @@ -303,7 +305,7 @@ where fn get_balance( recv_chain: &LocalChain, - recv_graph: &IndexedTxGraph>, + recv_graph: &IndexedTxGraph>, ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); let outpoints = recv_graph.index.outpoints().clone(); @@ -341,7 +343,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { // setup receiver let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?); - let mut recv_graph = IndexedTxGraph::::new({ + let mut recv_graph = IndexedTxGraph::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); recv_index diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 92ccdb65e..5421d3174 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -1,14 +1,12 @@ use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; -use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY}; +use crate::{Anchor, BlockTime, COINBASE_MATURITY}; /// Represents the observed position of some chain data. -/// -/// The generic `A` should be a [`Anchor`] implementation. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)] pub enum ChainPosition { - /// The chain data is seen as confirmed, and in anchored by `A`. - Confirmed(A), + /// The chain data is seen as confirmed, and in anchored by `Anchor`. + Confirmed(Anchor, A), /// The chain data is not confirmed and last seen in the mempool at this timestamp. Unconfirmed(u64), } @@ -16,30 +14,20 @@ pub enum ChainPosition { impl ChainPosition { /// Returns whether [`ChainPosition`] is confirmed or not. pub fn is_confirmed(&self) -> bool { - matches!(self, Self::Confirmed(_)) + matches!(self, Self::Confirmed(_, _)) } } -impl ChainPosition<&A> { - /// Maps a [`ChainPosition<&A>`] into a [`ChainPosition`] by cloning the contents. +impl ChainPosition { + /// Maps a [`ChainPosition`] into a [`ChainPosition`] by cloning the contents. pub fn cloned(self) -> ChainPosition { match self { - ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()), + ChainPosition::Confirmed(anchor, a) => ChainPosition::Confirmed(anchor, a), ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen), } } } -impl ChainPosition { - /// Determines the upper bound of the confirmation height. - pub fn confirmation_height_upper_bound(&self) -> Option { - match self { - ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()), - ChainPosition::Unconfirmed(_) => None, - } - } -} - /// Block height and timestamp at which a transaction is confirmed. #[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] #[cfg_attr( @@ -74,12 +62,12 @@ impl ConfirmationTime { } } -impl From> for ConfirmationTime { - fn from(observed_as: ChainPosition) -> Self { +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::Confirmed((_txid, blockid), anchor_meta) => Self::Confirmed { + height: blockid.height, + time: *anchor_meta.as_ref() as u64, }, ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen }, } @@ -103,18 +91,6 @@ pub struct BlockId { pub hash: BlockHash, } -impl Anchor for BlockId { - fn anchor_block(&self) -> Self { - *self - } -} - -impl AnchorFromBlockPosition for BlockId { - fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { - block_id - } -} - impl Default for BlockId { fn default() -> Self { Self { @@ -145,41 +121,6 @@ impl From<(&u32, &BlockHash)> for BlockId { } } -/// An [`Anchor`] implementation that also records the exact confirmation time of the transaction. -/// -/// Refer to [`Anchor`] for more details. -#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] -#[cfg_attr( - feature = "serde", - derive(serde::Deserialize, serde::Serialize), - serde(crate = "serde_crate") -)] -pub struct ConfirmationBlockTime { - /// The anchor block. - pub block_id: BlockId, - /// The confirmation time of the transaction being anchored. - pub confirmation_time: u64, -} - -impl Anchor for ConfirmationBlockTime { - fn anchor_block(&self) -> BlockId { - self.block_id - } - - fn confirmation_height_upper_bound(&self) -> u32 { - self.block_id.height - } -} - -impl AnchorFromBlockPosition for ConfirmationBlockTime { - fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { - Self { - block_id, - confirmation_time: block.header.time as _, - } - } -} - /// A `TxOut` with as much data as we can retrieve about it #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct FullTxOut { @@ -195,18 +136,12 @@ pub struct FullTxOut { pub is_on_coinbase: bool, } -impl FullTxOut { +impl FullTxOut { /// 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: u32) -> bool { if self.is_on_coinbase { let tx_height = match &self.chain_position { - ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), + ChainPosition::Confirmed((_, blockid), _) => blockid.height, ChainPosition::Unconfirmed(_) => { debug_assert!(false, "coinbase tx can never be unconfirmed"); return false; @@ -224,19 +159,13 @@ impl FullTxOut { /// Whether the utxo is/was/will be spendable with chain `tip`. /// /// This method does not take into account the lock time. - /// - /// 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_confirmed_and_spendable(&self, tip: u32) -> bool { if !self.is_mature(tip) { return false; } let confirmation_height = match &self.chain_position { - ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), + ChainPosition::Confirmed((_, blockid), _) => blockid.height, ChainPosition::Unconfirmed(_) => return false, }; if confirmation_height > tip { @@ -244,8 +173,8 @@ impl FullTxOut { } // if the spending tx is confirmed within tip height, the txout is no longer spendable - if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by { - if spending_anchor.anchor_block().height <= tip { + if let Some((ChainPosition::Confirmed((_, spending_blockid), _), _)) = &self.spent_by { + if spending_blockid.height <= tip { return false; } } @@ -257,25 +186,32 @@ impl FullTxOut { #[cfg(test)] mod test { use super::*; + use crate::BlockTime; #[test] fn chain_position_ord() { - let unconf1 = ChainPosition::::Unconfirmed(10); - let unconf2 = ChainPosition::::Unconfirmed(20); - let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime { - confirmation_time: 20, - block_id: BlockId { - height: 9, - ..Default::default() - }, - }); - let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime { - confirmation_time: 15, - block_id: BlockId { - height: 12, - ..Default::default() - }, - }); + let unconf1 = ChainPosition::Unconfirmed(10); + let unconf2 = ChainPosition::Unconfirmed(20); + let conf1 = ChainPosition::Confirmed( + ( + Txid::all_zeros(), + BlockId { + height: 9, + ..Default::default() + }, + ), + BlockTime::new(20), + ); + let conf2 = ChainPosition::Confirmed( + ( + Txid::all_zeros(), + BlockId { + height: 12, + ..Default::default() + }, + ), + BlockTime::new(15), + ); assert!(unconf2 > unconf1, "higher last_seen means higher ord"); assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed"); diff --git a/crates/chain/src/changeset.rs b/crates/chain/src/changeset.rs index e5777f46c..1a0523a97 100644 --- a/crates/chain/src/changeset.rs +++ b/crates/chain/src/changeset.rs @@ -34,7 +34,10 @@ impl core::default::Default for CombinedChangeSet { } #[cfg(feature = "miniscript")] -impl crate::Merge for CombinedChangeSet { +impl crate::Merge for CombinedChangeSet +where + A: Ord + Clone, +{ fn merge(&mut self, other: Self) { crate::Merge::merge(&mut self.chain, other.chain); crate::Merge::merge(&mut self.indexed_tx_graph, other.indexed_tx_graph); diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 7181768a1..b1119bbe3 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -5,7 +5,7 @@ use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; use crate::{ tx_graph::{self, TxGraph}, - Anchor, AnchorFromBlockPosition, BlockId, Indexer, Merge, + Anchor, AnchorMetaFromBlock, BlockId, Indexer, Merge, }; /// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation. @@ -42,7 +42,10 @@ impl IndexedTxGraph { } } -impl IndexedTxGraph { +impl IndexedTxGraph +where + A: Ord + Clone, +{ /// Applies the [`ChangeSet`] to the [`IndexedTxGraph`]. pub fn apply_changeset(&mut self, changeset: ChangeSet) { self.index.apply_changeset(changeset.indexer); @@ -65,9 +68,10 @@ impl IndexedTxGraph { } } -impl IndexedTxGraph +impl IndexedTxGraph where I::ChangeSet: Default + Merge, + A: Ord + Clone, { fn index_tx_graph_changeset( &mut self, @@ -107,8 +111,8 @@ where } /// Insert an `anchor` for a given transaction. - pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet { - self.graph.insert_anchor(txid, anchor).into() + pub fn insert_anchor(&mut self, anchor: Anchor, anchor_meta: A) -> ChangeSet { + self.graph.insert_anchor(anchor, anchor_meta).into() } /// Insert a unix timestamp of when a transaction is seen in the mempool. @@ -125,7 +129,7 @@ where /// transactions in `txs` will be ignored. `txs` do not need to be in topological order. pub fn batch_insert_relevant<'t>( &mut self, - txs: impl IntoIterator)>, + txs: impl IntoIterator)>, ) -> ChangeSet { // The algorithm below allows for non-topologically ordered transactions by using two loops. // This is achieved by: @@ -143,10 +147,9 @@ where let mut graph = tx_graph::ChangeSet::default(); for (tx, anchors) in txs { if self.index.is_tx_relevant(tx) { - let txid = tx.compute_txid(); graph.merge(self.graph.insert_tx(tx.clone())); - for anchor in anchors { - graph.merge(self.graph.insert_anchor(txid, anchor)); + for (anchor, anchor_meta) in anchors { + graph.merge(self.graph.insert_anchor(anchor, anchor_meta)); } } } @@ -207,17 +210,17 @@ where } } -/// Methods are available if the anchor (`A`) implements [`AnchorFromBlockPosition`]. -impl IndexedTxGraph +/// Methods are available if the anchor metadata implements [`AnchorMetaFromBlock`]. +impl IndexedTxGraph where I::ChangeSet: Default + Merge, - A: AnchorFromBlockPosition, + A: AnchorMetaFromBlock + Ord + Clone, { /// Batch insert all transactions of the given `block` of `height`, filtering out those that are /// irrelevant. /// /// Each inserted transaction's anchor will be constructed from - /// [`AnchorFromBlockPosition::from_block_position`]. + /// [`AnchorMetaFromBlock::from_block`]. /// /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`. /// Irrelevant transactions in `txs` will be ignored. @@ -234,12 +237,12 @@ where for (tx_pos, tx) in block.txdata.iter().enumerate() { changeset.indexer.merge(self.index.index_tx(tx)); if self.index.is_tx_relevant(tx) { - let txid = tx.compute_txid(); - let anchor = A::from_block_position(block, block_id, tx_pos); + let anchor = (tx.compute_txid(), block_id); + let anchor_meta = A::from_block(block, block_id, tx_pos); changeset.graph.merge(self.graph.insert_tx(tx.clone())); changeset .graph - .merge(self.graph.insert_anchor(txid, anchor)); + .merge(self.graph.insert_anchor(anchor, anchor_meta)); } } changeset @@ -248,7 +251,7 @@ where /// Batch insert all transactions of the given `block` of `height`. /// /// Each inserted transaction's anchor will be constructed from - /// [`AnchorFromBlockPosition::from_block_position`]. + /// [`AnchorMetaFromBlock::from_block`]. /// /// To only insert relevant transactions, use [`apply_block_relevant`] instead. /// @@ -260,8 +263,9 @@ where }; let mut graph = tx_graph::ChangeSet::default(); for (tx_pos, tx) in block.txdata.iter().enumerate() { - let anchor = A::from_block_position(&block, block_id, tx_pos); - graph.merge(self.graph.insert_anchor(tx.compute_txid(), anchor)); + let anchor = (tx.compute_txid(), block_id); + let anchor_meta = A::from_block(&block, block_id, tx_pos); + graph.merge(self.graph.insert_anchor(anchor, anchor_meta)); graph.merge(self.graph.insert_tx(tx.clone())); } let indexer = self.index_tx_graph_changeset(&graph); @@ -299,7 +303,10 @@ impl Default for ChangeSet { } } -impl Merge for ChangeSet { +impl Merge for ChangeSet +where + A: Ord + Clone, +{ fn merge(&mut self, other: Self) { self.graph.merge(other.graph); self.indexer.merge(other.indexer); diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs index 3457dfef7..d08248a37 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/chain/src/spk_client.rs @@ -1,8 +1,6 @@ //! Helper types for spk-based blockchain clients. -use crate::{ - collections::BTreeMap, local_chain::CheckPoint, ConfirmationBlockTime, Indexed, TxGraph, -}; +use crate::{collections::BTreeMap, local_chain::CheckPoint, BlockTime, Indexed, TxGraph}; use alloc::boxed::Box; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; use core::marker::PhantomData; @@ -176,7 +174,7 @@ impl SyncRequest { /// Data returned from a spk-based blockchain client sync. /// /// See also [`SyncRequest`]. -pub struct SyncResult { +pub struct SyncResult { /// The update to apply to the receiving [`TxGraph`]. pub graph_update: TxGraph, /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). @@ -317,7 +315,7 @@ impl FullScanRequest { /// Data returned from a spk-based blockchain client full scan. /// /// See also [`FullScanRequest`]. -pub struct FullScanResult { +pub struct FullScanResult { /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). pub graph_update: TxGraph, /// The update to apply to the receiving [`TxGraph`]. diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 8a324f6a5..caff7dd47 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -2,94 +2,44 @@ use crate::collections::BTreeMap; use crate::collections::BTreeSet; use crate::BlockId; use alloc::vec::Vec; +use bitcoin::Txid; + +/// TODO: New [`Anchor`] docs +pub type Anchor = (Txid, BlockId); + +/// TODO: [`BlockTime`] docs +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde(crate = "serde_crate",) +)] +pub struct BlockTime(u32); + +impl BlockTime { + /// TODO: new() docs + pub fn new(confirmation_time: u32) -> Self { + BlockTime(confirmation_time) + } +} -/// Trait that "anchors" blockchain data to a specific block of height and hash. -/// -/// If transaction A is anchored in block B, and block B is in the best chain, we can -/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean -/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a -/// parent block of B. -/// -/// Every [`Anchor`] implementation must contain a [`BlockId`] parameter, and must implement -/// [`Ord`]. When implementing [`Ord`], the anchors' [`BlockId`]s should take precedence -/// over other elements inside the [`Anchor`]s for comparison purposes, i.e., you should first -/// compare the anchors' [`BlockId`]s and then care about the rest. -/// -/// The example shows different types of anchors: -/// ``` -/// # use bdk_chain::local_chain::LocalChain; -/// # use bdk_chain::tx_graph::TxGraph; -/// # use bdk_chain::BlockId; -/// # use bdk_chain::ConfirmationBlockTime; -/// # use bdk_chain::example_utils::*; -/// # use bitcoin::hashes::Hash; -/// // Initialize the local chain with two blocks. -/// let chain = LocalChain::from_blocks( -/// [ -/// (1, Hash::hash("first".as_bytes())), -/// (2, Hash::hash("second".as_bytes())), -/// ] -/// .into_iter() -/// .collect(), -/// ); -/// -/// // Transaction to be inserted into `TxGraph`s with different anchor types. -/// let tx = tx_from_hex(RAW_TX_1); -/// -/// // Insert `tx` into a `TxGraph` that uses `BlockId` as the anchor type. -/// // When a transaction is anchored with `BlockId`, the anchor block and the confirmation block of -/// // the transaction is the same block. -/// let mut graph_a = TxGraph::::default(); -/// let _ = graph_a.insert_tx(tx.clone()); -/// graph_a.insert_anchor( -/// tx.compute_txid(), -/// BlockId { -/// height: 1, -/// hash: Hash::hash("first".as_bytes()), -/// }, -/// ); -/// -/// // Insert `tx` into a `TxGraph` that uses `ConfirmationBlockTime` as the anchor type. -/// // This anchor records the anchor block and the confirmation time of the transaction. When a -/// // transaction is anchored with `ConfirmationBlockTime`, the anchor block and confirmation block -/// // of the transaction is the same block. -/// let mut graph_c = TxGraph::::default(); -/// let _ = graph_c.insert_tx(tx.clone()); -/// graph_c.insert_anchor( -/// tx.compute_txid(), -/// ConfirmationBlockTime { -/// block_id: BlockId { -/// height: 2, -/// hash: Hash::hash("third".as_bytes()), -/// }, -/// confirmation_time: 123, -/// }, -/// ); -/// ``` -pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash { - /// Returns the [`BlockId`] that the associated blockchain data is "anchored" in. - fn anchor_block(&self) -> BlockId; - - /// Get the upper bound of the chain data's confirmation height. - /// - /// The default definition gives a pessimistic answer. This can be overridden by the `Anchor` - /// implementation for a more accurate value. - fn confirmation_height_upper_bound(&self) -> u32 { - self.anchor_block().height +impl AsRef for BlockTime { + fn as_ref(&self) -> &u32 { + &self.0 } } -impl<'a, A: Anchor> Anchor for &'a A { - fn anchor_block(&self) -> BlockId { - ::anchor_block(self) +impl AnchorMetaFromBlock for BlockTime { + fn from_block(block: &bitcoin::Block, _block_id: BlockId, _tx_pos: usize) -> Self { + BlockTime(block.header.time) } } -/// An [`Anchor`] that can be constructed from a given block, block height and transaction position -/// within the block. -pub trait AnchorFromBlockPosition: Anchor { - /// Construct the anchor from a given `block`, block height and `tx_pos` within the block. - fn from_block_position(block: &bitcoin::Block, block_id: BlockId, tx_pos: usize) -> Self; +/// [`Anchor`] metadata that can be constructed from a given block, block height and transaction +/// position within the block. +pub trait AnchorMetaFromBlock { + /// Construct the anchor metadata from a given `block`, block height and `tx_pos` within the block. + fn from_block(cblock: &bitcoin::Block, block_id: BlockId, tx_pos: usize) -> Self; } /// Trait that makes an object mergeable. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 8c11e737a..c66cdd69d 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -109,9 +109,9 @@ use core::{ #[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: BTreeMap, last_seen: HashMap, // This atrocity exists so that `TxGraph::outspends()` can return a reference. @@ -139,7 +139,7 @@ pub struct TxNode<'a, T, A> { /// A partial or full representation of the transaction. pub tx: T, /// The blocks that the transaction is "anchored" in. - pub anchors: &'a BTreeSet, + pub anchors: &'a BTreeMap, /// The last-seen unix timestamp of the transaction as unconfirmed. pub last_seen_unconfirmed: Option, } @@ -172,7 +172,7 @@ impl Default for TxNodeInternal { #[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>, + pub chain_position: ChainPosition, /// The transaction node (as part of the graph). pub tx_node: TxNode<'a, T, A>, } @@ -469,7 +469,7 @@ impl TxGraph { } /// Get all transaction anchors known by [`TxGraph`]. - pub fn all_anchors(&self) -> &BTreeSet<(A, Txid)> { + pub fn all_anchors(&self) -> &BTreeMap { &self.anchors } @@ -480,19 +480,6 @@ impl TxGraph { } impl TxGraph { - /// Transform the [`TxGraph`] to have [`Anchor`]s of another type. - /// - /// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to - /// transform it. - pub fn map_anchors(self, f: F) -> TxGraph - where - F: FnMut(A) -> A2, - { - let mut new_graph = TxGraph::::default(); - new_graph.apply_changeset(self.initial_changeset().map_anchors(f)); - new_graph - } - /// Construct a new [`TxGraph`] from a list of transactions. pub fn new(txs: impl IntoIterator) -> Self { let mut new = Self::default(); @@ -517,7 +504,7 @@ impl TxGraph { outpoint.txid, ( TxNodeInternal::Partial([(outpoint.vout, txout)].into()), - BTreeSet::new(), + BTreeMap::new(), ), ); self.apply_update(update) @@ -531,7 +518,7 @@ impl TxGraph { let mut update = Self::default(); update.txs.insert( tx.compute_txid(), - (TxNodeInternal::Whole(tx), BTreeSet::new()), + (TxNodeInternal::Whole(tx), BTreeMap::new()), ); self.apply_update(update) } @@ -545,7 +532,7 @@ impl TxGraph { &mut self, txs: impl IntoIterator, ) -> ChangeSet { - let mut changeset = ChangeSet::::default(); + let mut changeset = ChangeSet::default(); for (tx, seen_at) in txs { changeset.merge(self.insert_seen_at(tx.compute_txid(), seen_at)); changeset.merge(self.insert_tx(tx)); @@ -557,9 +544,9 @@ impl TxGraph { /// /// The [`ChangeSet`] returned will be empty if graph already knows that `txid` exists in /// `anchor`. - pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet { + pub fn insert_anchor(&mut self, anchor: Anchor, anchor_meta: A) -> ChangeSet { let mut update = Self::default(); - update.anchors.insert((anchor, txid)); + update.anchors.insert(anchor, anchor_meta); self.apply_update(update) } @@ -680,7 +667,7 @@ impl TxGraph { } None => { self.txs - .insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new())); + .insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeMap::new())); } } } @@ -696,10 +683,14 @@ impl TxGraph { } } - for (anchor, txid) in changeset.anchors { - if self.anchors.insert((anchor.clone(), txid)) { + for ((txid, blockid), anchor_meta) in changeset.anchors { + if self + .anchors + .insert((txid, blockid), anchor_meta.clone()) + .is_none() + { let (_, anchors) = self.txs.entry(txid).or_default(); - anchors.insert(anchor); + anchors.insert((txid, blockid), anchor_meta); } } @@ -716,7 +707,7 @@ impl TxGraph { /// The [`ChangeSet`] would be the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). pub(crate) fn determine_changeset(&self, update: TxGraph) -> ChangeSet { - let mut changeset = ChangeSet::::default(); + let mut changeset = ChangeSet::default(); for (&txid, (update_tx_node, _)) in &update.txs { match (self.txs.get(&txid), update_tx_node) { @@ -755,13 +746,18 @@ impl TxGraph { } } - changeset.anchors = update.anchors.difference(&self.anchors).cloned().collect(); + changeset.anchors = update + .anchors + .iter() + .filter(|(k, _)| !self.anchors.contains_key(k)) + .map(|(k, v)| (*k, v.clone())) + .collect::>(); changeset } } -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. @@ -790,15 +786,17 @@ impl TxGraph { chain: &C, chain_tip: BlockId, txid: Txid, - ) -> Result>, C::Error> { + ) -> Result>, C::Error> { let (tx_node, anchors) = match self.txs.get(&txid) { Some(v) => v, None => return Ok(None), }; - for anchor in anchors { - match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? { - Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))), + for (anchor, anchor_meta) in anchors { + match chain.is_block_in_chain(anchor.1, chain_tip)? { + Some(true) => { + return Ok(Some(ChainPosition::Confirmed(*anchor, anchor_meta.clone()))) + } _ => continue, } } @@ -841,8 +839,8 @@ impl TxGraph { 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) { + for (_, block) in tx_node.anchors.keys() { + match chain.is_block_in_chain(*block, chain_tip) { Ok(Some(true)) => return None, Err(e) => return Some(Err(e)), _ => continue, @@ -861,8 +859,8 @@ impl TxGraph { 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) { + for (_, block) in tx_node.anchors.keys() { + match chain.is_block_in_chain(*block, chain_tip) { Ok(Some(true)) => return None, Err(e) => return Some(Err(e)), _ => continue, @@ -888,8 +886,8 @@ impl TxGraph { // 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) { + for (_, block) in conflicting_tx.anchors.keys() { + if chain.is_block_in_chain(*block, chain_tip)? == Some(true) { return Ok(None); } } @@ -918,7 +916,7 @@ impl TxGraph { chain: &C, chain_tip: BlockId, txid: Txid, - ) -> Option> { + ) -> Option> { self.try_get_chain_position(chain, chain_tip, txid) .expect("error is infallible") } @@ -940,7 +938,7 @@ impl TxGraph { chain: &C, chain_tip: BlockId, outpoint: OutPoint, - ) -> Result, Txid)>, C::Error> { + ) -> Result, Txid)>, C::Error> { if self .try_get_chain_position(chain, chain_tip, outpoint.txid)? .is_none() @@ -968,7 +966,7 @@ impl TxGraph { chain: &C, static_block: BlockId, outpoint: OutPoint, - ) -> Option<(ChainPosition<&A>, Txid)> { + ) -> Option<(ChainPosition, Txid)> { self.try_get_chain_spend(chain, static_block, outpoint) .expect("error is infallible") } @@ -1045,7 +1043,7 @@ impl TxGraph { outpoints .into_iter() .map( - move |(spk_i, op)| -> Result)>, C::Error> { + move |(spk_i, op)| -> Result)>, C::Error> { let tx_node = match self.get_tx_node(op.txid) { Some(n) => n, None => return Ok(None), @@ -1058,13 +1056,11 @@ impl TxGraph { let chain_position = match self.try_get_chain_position(chain, chain_tip, op.txid)? { - Some(pos) => pos.cloned(), + Some(pos) => pos, None => return Ok(None), }; - let spent_by = self - .try_get_chain_spend(chain, chain_tip, op)? - .map(|(a, txid)| (a.cloned(), txid)); + let spent_by = self.try_get_chain_spend(chain, chain_tip, op)?; Ok(Some(( spk_i, @@ -1174,7 +1170,7 @@ impl TxGraph { let (spk_i, txout) = res?; match &txout.chain_position { - ChainPosition::Confirmed(_) => { + ChainPosition::Confirmed(_, _) => { if txout.is_confirmed_and_spendable(chain_tip.height) { confirmed += txout.txout.value; } else if !txout.is_mature(chain_tip.height) { @@ -1243,7 +1239,7 @@ pub struct ChangeSet { /// Added txouts. pub txouts: BTreeMap, /// Added anchors. - pub anchors: BTreeSet<(A, Txid)>, + pub anchors: BTreeMap, /// Added last-seen unix timestamps of transactions. pub last_seen: BTreeMap, } @@ -1277,14 +1273,11 @@ impl ChangeSet { /// /// This is useful if you want to find which heights you need to fetch data about in order to /// confirm or exclude these anchors. - pub fn anchor_heights(&self) -> impl Iterator + '_ - where - A: Anchor, - { + pub fn anchor_heights(&self) -> impl Iterator + '_ { let mut dedup = None; self.anchors .iter() - .map(|(a, _)| a.anchor_block().height) + .map(|((_, blockid), _)| blockid.height) .filter(move |height| { let duplicate = dedup == Some(*height); dedup = Some(*height); @@ -1319,26 +1312,6 @@ impl Merge for ChangeSet { } } -impl ChangeSet { - /// Transform the [`ChangeSet`] to have [`Anchor`]s of another type. - /// - /// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to - /// transform it. - pub fn map_anchors(self, mut f: F) -> ChangeSet - where - F: FnMut(A) -> A2, - { - ChangeSet { - txs: self.txs, - txouts: self.txouts, - anchors: BTreeSet::<(A2, Txid)>::from_iter( - self.anchors.into_iter().map(|(a, txid)| (f(a), txid)), - ), - last_seen: self.last_seen, - } - } -} - impl AsRef> for TxGraph { fn as_ref(&self) -> &TxGraph { self diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs index 3fad37f93..7862a0fad 100644 --- a/crates/chain/tests/common/mod.rs +++ b/crates/chain/tests/common/mod.rs @@ -4,6 +4,22 @@ mod tx_template; #[allow(unused_imports)] pub use tx_template::*; +#[allow(unused_macros)] +macro_rules! anchor { + ($height:expr, $hash:literal) => { + ( + ( + bdk_chain::bitcoin::Txid::all_zeros(), + bdk_chain::BlockId { + height: $height, + hash: bitcoin::hashes::Hash::hash($hash.as_bytes()), + }, + ), + (), + ) + }; +} + #[allow(unused_macros)] macro_rules! block_id { ($height:expr, $hash:literal) => {{ diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs index 3337fb436..b168e5d1a 100644 --- a/crates/chain/tests/common/tx_template.rs +++ b/crates/chain/tests/common/tx_template.rs @@ -16,12 +16,12 @@ use miniscript::Descriptor; /// avoid having to explicitly hash previous transactions to form previous outpoints of later /// transactions. #[derive(Clone, Copy, Default)] -pub struct TxTemplate<'a, A> { +pub struct TxTemplate<'a> { /// Uniquely identifies the transaction, before it can have a txid. pub tx_name: &'a str, pub inputs: &'a [TxInTemplate<'a>], pub outputs: &'a [TxOutTemplate], - pub anchors: &'a [A], + pub anchors: &'a [(Anchor, ())], pub last_seen: Option, } @@ -51,12 +51,12 @@ impl TxOutTemplate { } #[allow(dead_code)] -pub fn init_graph<'a, A: Anchor + Clone + 'a>( - tx_templates: impl IntoIterator>, -) -> (TxGraph, 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(), super::DESCRIPTORS[2]).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( @@ -128,8 +128,8 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( tx_ids.insert(tx_tmp.tx_name, tx.compute_txid()); spk_index.scan(&tx); let _ = graph.insert_tx(tx.clone()); - for anchor in tx_tmp.anchors.iter() { - let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone()); + for ((_, blockid), _) in tx_tmp.anchors.iter() { + let _ = graph.insert_anchor((tx.compute_txid(), *blockid), ()); } let _ = graph.insert_seen_at(tx.compute_txid(), tx_tmp.last_seen.unwrap_or(0)); } diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 01d25c061..be3fff21e 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -10,10 +10,11 @@ use bdk_chain::{ indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, - tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt, Merge, + tx_graph, Balance, BlockTime, ChainPosition, DescriptorExt, Merge, }; use bitcoin::{ - secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, + hashes::Hash, secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, + TxOut, }; use miniscript::Descriptor; @@ -32,9 +33,8 @@ fn insert_relevant_txs() { let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey(); let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey(); - let mut graph = IndexedTxGraph::>::new( - KeychainTxOutIndex::new(10), - ); + let mut graph = + IndexedTxGraph::>::new(KeychainTxOutIndex::new(10)); let _ = graph .index .insert_descriptor((), descriptor.clone()) @@ -140,9 +140,8 @@ fn test_list_owned_txouts() { let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap(); - let mut graph = IndexedTxGraph::>::new( - KeychainTxOutIndex::new(10), - ); + let mut graph = + IndexedTxGraph::>::new(KeychainTxOutIndex::new(10)); assert!(!graph .index @@ -250,99 +249,95 @@ fn test_list_owned_txouts() { local_chain .get(height) .map(|cp| cp.block_id()) - .map(|block_id| ConfirmationBlockTime { - block_id, - confirmation_time: 100, - }), + .map(|block_id| ((tx.compute_txid(), block_id), BlockTime::new(100))), ) })); let _ = graph.batch_insert_relevant_unconfirmed([&tx4, &tx5].iter().map(|tx| (*tx, 100))); // A helper lambda to extract and filter data from the graph. - let fetch = - |height: u32, graph: &IndexedTxGraph>| { - let chain_tip = local_chain - .get(height) - .map(|cp| cp.block_id()) - .unwrap_or_else(|| panic!("block must exist at {}", height)); - let txouts = graph - .graph() - .filter_chain_txouts( - &local_chain, - chain_tip, - graph.index.outpoints().iter().cloned(), - ) - .collect::>(); - - let utxos = graph - .graph() - .filter_chain_unspents( - &local_chain, - chain_tip, - graph.index.outpoints().iter().cloned(), - ) - .collect::>(); - - let balance = graph.graph().balance( + let fetch = |height: u32, graph: &IndexedTxGraph>| { + let chain_tip = local_chain + .get(height) + .map(|cp| cp.block_id()) + .unwrap_or_else(|| panic!("block must exist at {}", height)); + let txouts = graph + .graph() + .filter_chain_txouts( &local_chain, chain_tip, graph.index.outpoints().iter().cloned(), - |_, spk: &Script| 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) - } else { - None - } - }) - .collect::>(); - - let unconfirmed_txouts_txid = txouts - .iter() - .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) { - Some(full_txout.outpoint.txid) - } else { - None - } - }) - .collect::>(); - - let confirmed_utxos_txid = utxos - .iter() - .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) { - Some(full_txout.outpoint.txid) - } else { - None - } - }) - .collect::>(); - - let unconfirmed_utxos_txid = utxos - .iter() - .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) { - Some(full_txout.outpoint.txid) - } else { - None - } - }) - .collect::>(); + ) + .collect::>(); - ( - confirmed_txouts_txid, - unconfirmed_txouts_txid, - confirmed_utxos_txid, - unconfirmed_utxos_txid, - balance, + let utxos = graph + .graph() + .filter_chain_unspents( + &local_chain, + chain_tip, + graph.index.outpoints().iter().cloned(), ) - }; + .collect::>(); + + let balance = graph.graph().balance( + &local_chain, + chain_tip, + graph.index.outpoints().iter().cloned(), + |_, spk: &Script| 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) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_txouts_txid = txouts + .iter() + .filter_map(|(_, full_txout)| { + if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let confirmed_utxos_txid = utxos + .iter() + .filter_map(|(_, full_txout)| { + if matches!(full_txout.chain_position, ChainPosition::Confirmed(_, _)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + let unconfirmed_utxos_txid = utxos + .iter() + .filter_map(|(_, full_txout)| { + if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) { + Some(full_txout.outpoint.txid) + } else { + None + } + }) + .collect::>(); + + ( + confirmed_txouts_txid, + unconfirmed_txouts_txid, + confirmed_utxos_txid, + unconfirmed_utxos_txid, + balance, + ) + }; // ----- TEST BLOCK ----- @@ -532,15 +527,14 @@ fn test_list_owned_txouts() { #[test] fn test_get_chain_position() { use bdk_chain::local_chain::CheckPoint; - use bdk_chain::BlockId; - use bdk_chain::SpkTxOutIndex; + use bdk_chain::{Anchor, SpkTxOutIndex}; struct TestCase { name: &'static str, tx: Transaction, - anchor: Option, + anchor: Option<(Anchor, A)>, last_seen: Option, - exp_pos: Option>, + exp_pos: Option>, } // addr: bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm @@ -552,9 +546,13 @@ fn test_get_chain_position() { }); // Anchors to test - let blocks = vec![block_id!(0, "g"), block_id!(1, "A"), block_id!(2, "B")]; + let anchors = vec![anchor!(0, "g"), anchor!(1, "A"), anchor!(2, "B")]; + let blocks = anchors + .iter() + .map(|((_, blockid), _)| *blockid) + .collect::>(); - let cp = CheckPoint::from_block_ids(blocks.clone()).unwrap(); + let cp = CheckPoint::from_block_ids(blocks).unwrap(); let chain = LocalChain::from_tip(cp).unwrap(); // The test will insert a transaction into the indexed tx graph @@ -562,8 +560,8 @@ fn test_get_chain_position() { // returned by `get_chain_position`. fn run( chain: &LocalChain, - graph: &mut IndexedTxGraph>, - test: TestCase, + graph: &mut IndexedTxGraph<(), SpkTxOutIndex>, + test: TestCase<()>, ) { let TestCase { name, @@ -576,8 +574,8 @@ fn test_get_chain_position() { // add data to graph let txid = tx.compute_txid(); let _ = graph.insert_tx(tx); - if let Some(anchor) = anchor { - let _ = graph.insert_anchor(txid, anchor); + if let Some(((_, blockid), _anchor_meta)) = anchor { + let _ = graph.insert_anchor((txid, blockid), ()); } if let Some(seen_at) = last_seen { let _ = graph.insert_seen_at(txid, seen_at); @@ -587,11 +585,26 @@ fn test_get_chain_position() { let res = graph .graph() .get_chain_position(chain, chain.tip().block_id(), txid); - assert_eq!( - res.map(ChainPosition::cloned), - exp_pos, - "failed test case: {name}" - ); + if let Some(chain_pos) = res { + match chain_pos { + // We do not have the proper txids when initializing anchors, so we compare against + // `BlockId` inside the anchor for confirmed transactions. + ChainPosition::Confirmed((_txid, blockid), _anchor_meta) => { + if let ChainPosition::Confirmed((_, exp_block), _) = exp_pos.unwrap() { + assert_eq!(blockid, exp_block, "failed test case: {name}"); + } else { + panic!("failed test case: {name}"); + } + } + ChainPosition::Unconfirmed(last_seen) => { + assert_eq!( + ChainPosition::Unconfirmed(last_seen), + exp_pos.unwrap(), + "failed test case: {name}" + ) + } + }; + } } [ @@ -630,9 +643,12 @@ fn test_get_chain_position() { }], ..common::new_tx(2) }, - anchor: Some(blocks[1]), + anchor: Some(anchors[1]), last_seen: None, - exp_pos: Some(ChainPosition::Confirmed(blocks[1])), + exp_pos: { + let (anchor, anchor_meta) = anchors[1]; + Some(ChainPosition::Confirmed(anchor, anchor_meta)) + }, }, TestCase { name: "tx unknown anchor with last_seen - unconfirmed", @@ -643,7 +659,7 @@ fn test_get_chain_position() { }], ..common::new_tx(3) }, - anchor: Some(block_id!(2, "B'")), + anchor: Some(anchor!(2, "B'")), last_seen: Some(2), exp_pos: Some(ChainPosition::Unconfirmed(2)), }, @@ -656,7 +672,7 @@ fn test_get_chain_position() { }], ..common::new_tx(4) }, - anchor: Some(block_id!(2, "B'")), + anchor: Some(anchor!(2, "B'")), last_seen: None, exp_pos: None, }, diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 8ddf7f30a..9b6f295da 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -7,7 +7,7 @@ use bdk_chain::{ collections::*, local_chain::LocalChain, tx_graph::{ChangeSet, TxGraph}, - Anchor, BlockId, ChainOracle, ChainPosition, ConfirmationBlockTime, Merge, + BlockId, BlockTime, ChainOracle, ChainPosition, Merge, }; use bitcoin::{ absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount, @@ -15,7 +15,6 @@ use bitcoin::{ }; use common::*; use core::iter; -use rand::RngCore; use std::sync::Arc; use std::vec; @@ -63,17 +62,14 @@ fn insert_txouts() { }; // Conf anchor used to mark the full transaction as confirmed. - let conf_anchor = ChainPosition::Confirmed(BlockId { + let anchor_block = BlockId { height: 100, hash: h!("random blockhash"), - }); - - // Unconfirmed anchor to mark the partial transactions as unconfirmed - let unconf_anchor = ChainPosition::::Unconfirmed(1000000); + }; // Make the original graph let mut graph = { - let mut graph = TxGraph::>::default(); + let mut graph = TxGraph::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -98,16 +94,6 @@ fn insert_txouts() { ..Default::default() } ); - // Mark them unconfirmed. - assert_eq!( - graph.insert_anchor(outpoint.txid, unconf_anchor), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [(unconf_anchor, outpoint.txid)].into(), - last_seen: [].into() - } - ); // Mark them last seen at. assert_eq!( graph.insert_seen_at(outpoint.txid, 1000000), @@ -130,11 +116,18 @@ fn insert_txouts() { // Mark it as confirmed. assert_eq!( - graph.insert_anchor(update_txs.compute_txid(), conf_anchor), + graph.insert_anchor( + (update_txs.compute_txid(), anchor_block), + BlockTime::new(100) + ), ChangeSet { txs: [].into(), txouts: [].into(), - anchors: [(conf_anchor, update_txs.compute_txid())].into(), + anchors: [( + (update_txs.compute_txid(), anchor_block), + BlockTime::new(100) + )] + .into(), last_seen: [].into() } ); @@ -149,10 +142,10 @@ fn insert_txouts() { ChangeSet { txs: [Arc::new(update_txs.clone())].into(), txouts: update_ops.clone().into(), - anchors: [ - (conf_anchor, update_txs.compute_txid()), - (unconf_anchor, h!("tx2")) - ] + anchors: [( + (update_txs.compute_txid(), anchor_block), + BlockTime::new(100) + )] .into(), last_seen: [(h!("tx2"), 1000000)].into() } @@ -206,10 +199,10 @@ fn insert_txouts() { ChangeSet { txs: [Arc::new(update_txs.clone())].into(), txouts: update_ops.into_iter().chain(original_ops).collect(), - anchors: [ - (conf_anchor, update_txs.compute_txid()), - (unconf_anchor, h!("tx2")) - ] + anchors: [( + (update_txs.compute_txid(), anchor_block), + BlockTime::new(100) + )] .into(), last_seen: [(h!("tx2"), 1000000)].into() } @@ -662,7 +655,7 @@ fn test_walk_ancestors() { ..common::new_tx(0) }; - let mut graph = TxGraph::::new([ + let mut graph = TxGraph::::new([ tx_a0.clone(), tx_b0.clone(), tx_b1.clone(), @@ -677,7 +670,8 @@ fn test_walk_ancestors() { ]); [&tx_a0, &tx_b1].iter().for_each(|&tx| { - let changeset = graph.insert_anchor(tx.compute_txid(), tip.block_id()); + let changeset = + graph.insert_anchor((tx.compute_txid(), tip.block_id()), BlockTime::new(100)); assert!(!changeset.is_empty()); }); @@ -695,8 +689,8 @@ fn test_walk_ancestors() { graph .walk_ancestors(tx_e0.clone(), |depth, tx| { let tx_node = graph.get_tx_node(tx.compute_txid())?; - for block in tx_node.anchors { - match local_chain.is_block_in_chain(block.anchor_block(), tip.block_id()) { + for (_, blockid) in tx_node.anchors.keys() { + match local_chain.is_block_in_chain(*blockid, tip.block_id()) { Ok(Some(true)) => return None, _ => continue, } @@ -935,7 +929,7 @@ fn test_chain_spends() { ..common::new_tx(0) }; - let mut graph = TxGraph::::default(); + let mut graph = TxGraph::default(); let _ = graph.insert_tx(tx_0.clone()); let _ = graph.insert_tx(tx_1.clone()); @@ -943,11 +937,8 @@ fn test_chain_spends() { for (ht, tx) in [(95, &tx_0), (98, &tx_1)] { let _ = graph.insert_anchor( - tx.compute_txid(), - ConfirmationBlockTime { - block_id: tip.get(ht).unwrap().block_id(), - confirmation_time: 100, - }, + (tx.compute_txid(), tip.get(ht).unwrap().block_id()), + BlockTime::new(100), ); } @@ -959,28 +950,34 @@ fn test_chain_spends() { 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 - }), + ChainPosition::Confirmed( + ( + tx_1.compute_txid(), + BlockId { + hash: tip.get(98).unwrap().hash(), + height: 98, + } + ), + BlockTime::new(100) + ), 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 { - block_id: BlockId { - hash: tip.get(95).unwrap().hash(), - height: 95, - }, - confirmation_time: 100 - })) + Some(ChainPosition::Confirmed( + ( + tx_0.compute_txid(), + BlockId { + hash: tip.get(95).unwrap().hash(), + height: 95, + } + ), + BlockTime::new(100) + )) ); // Mark the unconfirmed as seen and check correct ObservedAs status is returned. @@ -1090,7 +1087,7 @@ fn test_changeset_last_seen_merge() { #[test] fn update_last_seen_unconfirmed() { - let mut graph = TxGraph::<()>::default(); + let mut graph = TxGraph::default(); let tx = new_tx(0); let txid = tx.compute_txid(); @@ -1110,7 +1107,7 @@ fn update_last_seen_unconfirmed() { assert!(changeset.last_seen.is_empty()); // once anchored, last seen is not updated - let _ = graph.insert_anchor(txid, ()); + let _ = graph.insert_anchor((txid, BlockId::default()), BlockTime::new(100)); let changeset = graph.update_last_seen_unconfirmed(4); assert!(changeset.is_empty()); assert_eq!( @@ -1130,7 +1127,7 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch let txids: Vec = txs.iter().map(Transaction::compute_txid).collect(); // graph - let mut graph = TxGraph::::new(txs); + let mut graph = TxGraph::::new(txs); let full_txs: Vec<_> = graph.full_txs().collect(); assert_eq!(full_txs.len(), 2); let unseen_txs: Vec<_> = graph.txs_with_no_anchor_or_last_seen().collect(); @@ -1156,7 +1153,7 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch drop(canonical_txs); // tx1 with anchor is also canonical - let _ = graph.insert_anchor(txids[1], block_id!(2, "B")); + let _ = graph.insert_anchor((txids[1], block_id!(2, "B")), BlockTime::new(100)); let canonical_txids: Vec<_> = graph .list_canonical_txs(&chain, chain.tip().block_id()) .map(|tx| tx.tx_node.txid) @@ -1164,86 +1161,3 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch assert!(canonical_txids.contains(&txids[1])); assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none()); } - -#[test] -/// The `map_anchors` allow a caller to pass a function to reconstruct the [`TxGraph`] with any [`Anchor`], -/// even though the function is non-deterministic. -fn call_map_anchors_with_non_deterministic_anchor() { - #[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] - /// A non-deterministic anchor - pub struct NonDeterministicAnchor { - pub anchor_block: BlockId, - pub non_deterministic_field: u32, - } - - let template = [ - TxTemplate { - tx_name: "tx1", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(1))], - anchors: &[block_id!(1, "A")], - last_seen: None, - }, - TxTemplate { - tx_name: "tx2", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(20000, Some(2))], - anchors: &[block_id!(2, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "tx3", - inputs: &[TxInTemplate::PrevTx("tx2", 0)], - outputs: &[TxOutTemplate::new(30000, Some(3))], - anchors: &[block_id!(3, "C"), block_id!(4, "D")], - ..Default::default() - }, - ]; - let (graph, _, _) = init_graph(&template); - let new_graph = graph.clone().map_anchors(|a| NonDeterministicAnchor { - anchor_block: a, - // A non-deterministic value - non_deterministic_field: rand::thread_rng().next_u32(), - }); - - // Check all the details in new_graph reconstruct as well - - let mut full_txs_vec: Vec<_> = graph.full_txs().collect(); - full_txs_vec.sort(); - let mut new_txs_vec: Vec<_> = new_graph.full_txs().collect(); - new_txs_vec.sort(); - let mut new_txs = new_txs_vec.iter(); - - for tx_node in full_txs_vec.iter() { - let new_txnode = new_txs.next().unwrap(); - assert_eq!(new_txnode.txid, tx_node.txid); - assert_eq!(new_txnode.tx, tx_node.tx); - assert_eq!( - new_txnode.last_seen_unconfirmed, - tx_node.last_seen_unconfirmed - ); - assert_eq!(new_txnode.anchors.len(), tx_node.anchors.len()); - - let mut new_anchors: Vec<_> = new_txnode.anchors.iter().map(|a| a.anchor_block).collect(); - new_anchors.sort(); - let mut old_anchors: Vec<_> = tx_node.anchors.iter().copied().collect(); - old_anchors.sort(); - assert_eq!(new_anchors, old_anchors); - } - assert!(new_txs.next().is_none()); - - let new_graph_anchors: Vec<_> = new_graph - .all_anchors() - .iter() - .map(|i| i.0.anchor_block) - .collect(); - assert_eq!( - new_graph_anchors, - vec![ - block_id!(1, "A"), - block_id!(2, "B"), - block_id!(3, "C"), - block_id!(4, "D"), - ] - ); -} diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 802ba5c7e..c655fd0ca 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -5,8 +5,8 @@ mod common; use std::collections::{BTreeSet, HashSet}; -use bdk_chain::{Balance, BlockId}; -use bitcoin::{Amount, OutPoint, Script}; +use bdk_chain::Balance; +use bitcoin::{hashes::Hash, Amount, OutPoint, Script}; use common::*; #[allow(dead_code)] @@ -14,7 +14,7 @@ struct Scenario<'a> { /// Name of the test scenario name: &'a str, /// Transaction templates - tx_templates: &'a [TxTemplate<'a, BlockId>], + tx_templates: &'a [TxTemplate<'a>], /// Names of txs that must exist in the output of `list_canonical_txs` exp_chain_txs: HashSet<&'a str>, /// Outpoints that must exist in the output of `filter_chain_txouts` @@ -57,7 +57,7 @@ fn test_tx_conflict_handling() { tx_name: "confirmed_genesis", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(1))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -73,7 +73,7 @@ fn test_tx_conflict_handling() { tx_name: "confirmed_conflict", inputs: &[TxInTemplate::PrevTx("confirmed_genesis", 0)], outputs: &[TxOutTemplate::new(20000, Some(3))], - anchors: &[block_id!(4, "E")], + anchors: &[anchor!(4, "E")], ..Default::default() }, ], @@ -93,7 +93,7 @@ fn test_tx_conflict_handling() { TxTemplate { tx_name: "tx1", outputs: &[TxOutTemplate::new(40000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, ..Default::default() }, @@ -130,7 +130,7 @@ fn test_tx_conflict_handling() { tx_name: "tx1", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -165,7 +165,7 @@ fn test_tx_conflict_handling() { tx_name: "tx1", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -207,7 +207,7 @@ fn test_tx_conflict_handling() { tx_name: "tx1", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -221,7 +221,7 @@ fn test_tx_conflict_handling() { tx_name: "tx_orphaned_conflict", inputs: &[TxInTemplate::PrevTx("tx1", 0)], outputs: &[TxOutTemplate::new(30000, Some(2))], - anchors: &[block_id!(4, "Orphaned Block")], + anchors: &[anchor!(4, "Orphaned Block")], last_seen: Some(300), }, ], @@ -242,7 +242,7 @@ fn test_tx_conflict_handling() { tx_name: "tx1", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -256,7 +256,7 @@ fn test_tx_conflict_handling() { tx_name: "tx_orphaned_conflict", inputs: &[TxInTemplate::PrevTx("tx1", 0)], outputs: &[TxOutTemplate::new(30000, Some(2))], - anchors: &[block_id!(4, "Orphaned Block")], + anchors: &[anchor!(4, "Orphaned Block")], last_seen: Some(100), }, ], @@ -277,7 +277,7 @@ fn test_tx_conflict_handling() { tx_name: "tx1", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -305,7 +305,7 @@ fn test_tx_conflict_handling() { tx_name: "tx_confirmed_conflict", inputs: &[TxInTemplate::PrevTx("tx1", 0)], outputs: &[TxOutTemplate::new(50000, Some(4))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], ..Default::default() }, ], @@ -371,7 +371,7 @@ fn test_tx_conflict_handling() { tx_name: "A", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -384,7 +384,7 @@ fn test_tx_conflict_handling() { tx_name: "B'", inputs: &[TxInTemplate::PrevTx("A", 0)], outputs: &[TxOutTemplate::new(20000, Some(2))], - anchors: &[block_id!(4, "E")], + anchors: &[anchor!(4, "E")], ..Default::default() }, TxTemplate { @@ -412,7 +412,7 @@ fn test_tx_conflict_handling() { tx_name: "A", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -425,7 +425,7 @@ fn test_tx_conflict_handling() { tx_name: "B'", inputs: &[TxInTemplate::PrevTx("A", 0)], outputs: &[TxOutTemplate::new(20000, Some(2))], - anchors: &[block_id!(4, "E")], + anchors: &[anchor!(4, "E")], ..Default::default() }, TxTemplate { @@ -457,7 +457,7 @@ fn test_tx_conflict_handling() { tx_name: "A", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -502,7 +502,7 @@ fn test_tx_conflict_handling() { tx_name: "A", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -516,7 +516,7 @@ fn test_tx_conflict_handling() { tx_name: "B'", inputs: &[TxInTemplate::PrevTx("A", 0)], outputs: &[TxOutTemplate::new(50000, Some(4))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], ..Default::default() }, TxTemplate { @@ -547,7 +547,7 @@ fn test_tx_conflict_handling() { tx_name: "A", inputs: &[TxInTemplate::Bogus], outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], last_seen: None, }, TxTemplate { @@ -561,7 +561,7 @@ fn test_tx_conflict_handling() { tx_name: "B'", inputs: &[TxInTemplate::PrevTx("A", 0)], outputs: &[TxOutTemplate::new(50000, Some(4))], - anchors: &[block_id!(1, "B")], + anchors: &[anchor!(1, "B")], ..Default::default() }, TxTemplate { diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 93c9dea74..a2c1a23bf 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -4,13 +4,10 @@ use bdk_chain::{ local_chain::CheckPoint, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, tx_graph::TxGraph, - Anchor, BlockId, ConfirmationBlockTime, + Anchor, BlockId, BlockTime, }; use electrum_client::{ElectrumApi, Error, HeaderNotification}; -use std::{ - collections::BTreeSet, - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, Mutex}; /// We include a chain suffix of a certain length for the purpose of robustness. const CHAIN_SUFFIX_LENGTH: u32 = 8; @@ -123,7 +120,7 @@ impl BdkElectrumClient { ) -> Result, Error> { let (tip, latest_blocks) = fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?; - let mut graph_update = TxGraph::::default(); + let mut graph_update = TxGraph::::default(); let mut last_active_indices = BTreeMap::::new(); for (keychain, spks) in request.spks_by_keychain { @@ -201,7 +198,7 @@ impl BdkElectrumClient { /// also included. fn populate_with_spks( &self, - graph_update: &mut TxGraph, + graph_update: &mut TxGraph, mut spks: impl Iterator, stop_gap: usize, batch_size: usize, @@ -247,7 +244,7 @@ impl BdkElectrumClient { /// included. Anchors of the aforementioned transactions are included. fn populate_with_outpoints( &self, - graph_update: &mut TxGraph, + graph_update: &mut TxGraph, outpoints: impl IntoIterator, ) -> Result<(), Error> { for outpoint in outpoints { @@ -295,7 +292,7 @@ impl BdkElectrumClient { /// Populate the `graph_update` with transactions/anchors of the provided `txids`. fn populate_with_txids( &self, - graph_update: &mut TxGraph, + graph_update: &mut TxGraph, txids: impl IntoIterator, ) -> Result<(), Error> { for txid in txids { @@ -331,7 +328,7 @@ impl BdkElectrumClient { // An anchor is inserted if the transaction is validated to be in a confirmed block. fn validate_merkle_for_anchor( &self, - graph_update: &mut TxGraph, + graph_update: &mut TxGraph, txid: Txid, confirmation_height: i32, ) -> Result<(), Error> { @@ -358,16 +355,11 @@ impl BdkElectrumClient { } if is_confirmed_tx { - let _ = graph_update.insert_anchor( - txid, - ConfirmationBlockTime { - confirmation_time: header.time as u64, - block_id: BlockId { - height: merkle_res.block_height as u32, - hash: header.block_hash(), - }, - }, - ); + let blockid = BlockId { + height: merkle_res.block_height as u32, + hash: header.block_hash(), + }; + let _ = graph_update.insert_anchor((txid, blockid), BlockTime::new(header.time)); } } Ok(()) @@ -375,10 +367,7 @@ impl BdkElectrumClient { // Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions, // which we do not have by default. This data is needed to calculate the transaction fee. - fn fetch_prev_txout( - &self, - graph_update: &mut TxGraph, - ) -> Result<(), Error> { + fn fetch_prev_txout(&self, graph_update: &mut TxGraph) -> Result<(), Error> { let full_txs: Vec> = graph_update.full_txs().map(|tx_node| tx_node.tx).collect(); for tx in full_txs { @@ -469,20 +458,20 @@ fn fetch_tip_and_latest_blocks( // Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not // surpass `latest_blocks`. -fn chain_update( +fn chain_update( mut tip: CheckPoint, latest_blocks: &BTreeMap, - anchors: &BTreeSet<(A, Txid)>, + anchors: &BTreeMap, ) -> Result { - for anchor in anchors { - let height = anchor.0.anchor_block().height; + for (_txid, blockid) in anchors.keys() { + let height = blockid.height; // Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent // in case of a re-org. if tip.get(height).is_none() && height <= tip.height() { let hash = match latest_blocks.get(&height) { Some(&hash) => hash, - None => anchor.0.anchor_block().hash, + None => blockid.hash, }; tip = tip.insert(BlockId { hash, height }); } diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 825454331..018e36653 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -2,7 +2,7 @@ use bdk_chain::{ bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash}, local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest}, - Balance, ConfirmationBlockTime, IndexedTxGraph, SpkTxOutIndex, + Balance, BlockTime, IndexedTxGraph, SpkTxOutIndex, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; @@ -11,7 +11,7 @@ use std::str::FromStr; fn get_balance( recv_chain: &LocalChain, - recv_graph: &IndexedTxGraph>, + recv_graph: &IndexedTxGraph>, ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); let outpoints = recv_graph.index.outpoints().clone(); @@ -262,7 +262,7 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> { // Setup receiver. let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); - let mut recv_graph = IndexedTxGraph::::new({ + let mut recv_graph = IndexedTxGraph::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); recv_index @@ -352,7 +352,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { // Setup receiver. let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); - let mut recv_graph = IndexedTxGraph::::new({ + let mut recv_graph = IndexedTxGraph::::new({ let mut recv_index = SpkTxOutIndex::default(); recv_index.insert_spk((), spk_to_track.clone()); recv_index @@ -362,12 +362,13 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { env.mine_blocks(101, Some(addr_to_mine))?; // Create transactions that are tracked by our receiver. - let mut txids = vec![]; - let mut hashes = vec![]; + let mut txids_and_hashes = vec![]; for _ in 0..REORG_COUNT { - txids.push(env.send(&addr_to_track, SEND_AMOUNT)?); - hashes.extend(env.mine_blocks(1, None)?); + let txid = env.send(&addr_to_track, SEND_AMOUNT)?; + let hash = env.mine_blocks(1, None)?[0]; + txids_and_hashes.push((txid, hash)); } + txids_and_hashes.sort(); // Sync up to tip. env.wait_until_electrum_sees_block()?; @@ -383,13 +384,16 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { let _ = recv_graph.apply_update(update.graph_update.clone()); // Retain a snapshot of all anchors before reorg process. - let initial_anchors = update.graph_update.all_anchors(); + let initial_anchors = update + .graph_update + .all_anchors() + .keys() + .collect::>(); let anchors: Vec<_> = initial_anchors.iter().cloned().collect(); assert_eq!(anchors.len(), REORG_COUNT); for i in 0..REORG_COUNT { - let (anchor, txid) = anchors[i]; - assert_eq!(anchor.block_id.hash, hashes[i]); - assert_eq!(txid, txids[i]); + let (txid, blockid) = anchors[i]; + assert_eq!(txids_and_hashes[i], (*txid, blockid.hash)); } // Check if initial balance is correct. @@ -418,7 +422,13 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; // Check that no new anchors are added during current reorg. - assert!(initial_anchors.is_superset(update.graph_update.all_anchors())); + assert!(initial_anchors.is_superset( + &update + .graph_update + .all_anchors() + .keys() + .collect::>() + )); let _ = recv_graph.apply_update(update.graph_update); assert_eq!( diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 70895a43a..35d9b3d69 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -1,12 +1,10 @@ -use std::collections::BTreeSet; - use async_trait::async_trait; use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; use bdk_chain::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid}, collections::BTreeMap, local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, TxGraph, + BlockId, BlockTime, TxGraph, }; use bdk_chain::{Anchor, Indexed}; use esplora_client::{Amount, TxStatus}; @@ -177,11 +175,11 @@ async fn fetch_block( /// /// We want to have a corresponding checkpoint per anchor height. However, checkpoints fetched /// should not surpass `latest_blocks`. -async fn chain_update( +async fn chain_update( client: &esplora_client::AsyncClient, latest_blocks: &BTreeMap, local_tip: &CheckPoint, - anchors: &BTreeSet<(A, Txid)>, + anchors: &BTreeMap, ) -> Result { let mut point_of_agreement = None; let mut conflicts = vec![]; @@ -210,8 +208,8 @@ async fn chain_update( .extend(conflicts.into_iter().rev()) .expect("evicted are in order"); - for anchor in anchors { - let height = anchor.0.anchor_block().height; + for (_txid, blockid) in anchors.keys() { + let height = blockid.height; if tip.get(height).is_none() { let hash = match fetch_block(client, latest_blocks, height).await? { Some(hash) => hash, @@ -240,10 +238,10 @@ async fn full_scan_for_index_and_graph( >, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, BTreeMap), Error> { +) -> Result<(TxGraph, BTreeMap), Error> { type TxsOfSpkIndex = (u32, Vec); let parallel_requests = Ord::max(parallel_requests, 1); - let mut graph = TxGraph::::default(); + let mut graph = TxGraph::::default(); let mut last_active_indexes = BTreeMap::::new(); for (keychain, spks) in keychain_spks { @@ -284,8 +282,8 @@ async fn full_scan_for_index_and_graph( } for tx in txs { let _ = graph.insert_tx(tx.to_tx()); - if let Some(anchor) = anchor_from_status(&tx.status) { - let _ = graph.insert_anchor(tx.txid, anchor); + if let Some((anchor, anchor_meta)) = anchor_from_status(tx.txid, &tx.status) { + let _ = graph.insert_anchor(anchor, anchor_meta); } let previous_outputs = tx.vin.iter().filter_map(|vin| { @@ -333,7 +331,7 @@ async fn sync_for_index_and_graph( txids: impl IntoIterator + Send> + Send, outpoints: impl IntoIterator + Send> + Send, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { let mut graph = full_scan_for_index_and_graph( client, [( @@ -367,8 +365,8 @@ async fn sync_for_index_and_graph( } for (txid, status) in handles.try_collect::>().await? { - if let Some(anchor) = anchor_from_status(&status) { - let _ = graph.insert_anchor(txid, anchor); + if let Some((anchor, anchor_meta)) = anchor_from_status(txid, &status) { + let _ = graph.insert_anchor(anchor, anchor_meta); } } } @@ -379,8 +377,8 @@ async fn sync_for_index_and_graph( let _ = graph.insert_tx(tx); } let status = client.get_tx_status(&op.txid).await?; - if let Some(anchor) = anchor_from_status(&status) { - let _ = graph.insert_anchor(op.txid, anchor); + if let Some((anchor, anchor_meta)) = anchor_from_status(op.txid, &status) { + let _ = graph.insert_anchor(anchor, anchor_meta); } } @@ -391,8 +389,8 @@ async fn sync_for_index_and_graph( let _ = graph.insert_tx(tx); } let status = client.get_tx_status(&txid).await?; - if let Some(anchor) = anchor_from_status(&status) { - let _ = graph.insert_anchor(txid, anchor); + if let Some((anchor, anchor_meta)) = anchor_from_status(txid, &status) { + let _ = graph.insert_anchor(anchor, anchor_meta); } } } @@ -404,12 +402,15 @@ async fn sync_for_index_and_graph( #[cfg(test)] mod test { - use std::{collections::BTreeSet, time::Duration}; + use std::{ + collections::{BTreeMap, BTreeSet}, + time::Duration, + }; use bdk_chain::{ bitcoin::{hashes::Hash, Txid}, local_chain::LocalChain, - BlockId, + BlockId, BlockTime, }; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; use esplora_client::Builder; @@ -489,14 +490,17 @@ mod test { .iter() .map(|&height| -> anyhow::Result<_> { Ok(( - BlockId { - height, - hash: env.bitcoind.client.get_block_hash(height as _)?, - }, - Txid::all_zeros(), + ( + Txid::all_zeros(), + BlockId { + height, + hash: env.bitcoind.client.get_block_hash(height as _)?, + }, + ), + BlockTime::new(100), )) }) - .collect::>>()?; + .collect::>>()?; let update = chain_update( &client, &fetch_latest_blocks(&client).await?, @@ -527,11 +531,14 @@ mod test { .iter() .map(|&(height, txid)| -> anyhow::Result<_> { Ok(( - BlockId { - height, - hash: env.bitcoind.client.get_block_hash(height as _)?, - }, - txid, + ( + txid, + BlockId { + height, + hash: env.bitcoind.client.get_block_hash(height as _)?, + }, + ), + BlockTime::new(100), )) }) .collect::>()?; diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index dc95a350b..e73ce6f0a 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; use std::thread::JoinHandle; use bdk_chain::collections::BTreeMap; @@ -6,7 +5,7 @@ use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncRe use bdk_chain::{ bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid}, local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, TxGraph, + BlockId, BlockTime, TxGraph, }; use bdk_chain::{Anchor, Indexed}; use esplora_client::TxStatus; @@ -159,11 +158,11 @@ fn fetch_block( /// /// We want to have a corresponding checkpoint per anchor height. However, checkpoints fetched /// should not surpass `latest_blocks`. -fn chain_update( +fn chain_update( client: &esplora_client::BlockingClient, latest_blocks: &BTreeMap, local_tip: &CheckPoint, - anchors: &BTreeSet<(A, Txid)>, + anchors: &BTreeMap, ) -> Result { let mut point_of_agreement = None; let mut conflicts = vec![]; @@ -173,7 +172,7 @@ fn chain_update( None => continue, }; if remote_hash == local_cp.hash() { - point_of_agreement = Some(local_cp.clone()); + point_of_agreement = Some(local_cp); break; } else { // it is not strictly necessary to include all the conflicted heights (we do need the @@ -192,8 +191,8 @@ fn chain_update( .extend(conflicts.into_iter().rev()) .expect("evicted are in order"); - for anchor in anchors { - let height = anchor.0.anchor_block().height; + for (_txid, blockid) in anchors.keys() { + let height = blockid.height; if tip.get(height).is_none() { let hash = match fetch_block(client, latest_blocks, height)? { Some(hash) => hash, @@ -219,10 +218,10 @@ fn full_scan_for_index_and_graph_blocking( keychain_spks: BTreeMap>>, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, BTreeMap), Error> { +) -> Result<(TxGraph, BTreeMap), Error> { type TxsOfSpkIndex = (u32, Vec); let parallel_requests = Ord::max(parallel_requests, 1); - let mut tx_graph = TxGraph::::default(); + let mut tx_graph = TxGraph::::default(); let mut last_active_indices = BTreeMap::::new(); for (keychain, spks) in keychain_spks { @@ -266,8 +265,8 @@ fn full_scan_for_index_and_graph_blocking( } for tx in txs { let _ = tx_graph.insert_tx(tx.to_tx()); - if let Some(anchor) = anchor_from_status(&tx.status) { - let _ = tx_graph.insert_anchor(tx.txid, anchor); + if let Some((anchor, anchor_meta)) = anchor_from_status(tx.txid, &tx.status) { + let _ = tx_graph.insert_anchor(anchor, anchor_meta); } let previous_outputs = tx.vin.iter().filter_map(|vin| { @@ -315,7 +314,7 @@ fn sync_for_index_and_graph_blocking( txids: impl IntoIterator, outpoints: impl IntoIterator, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { let (mut tx_graph, _) = full_scan_for_index_and_graph_blocking( client, { @@ -358,8 +357,8 @@ fn sync_for_index_and_graph_blocking( for handle in handles { let (txid, status) = handle.join().expect("thread must not panic")?; - if let Some(anchor) = anchor_from_status(&status) { - let _ = tx_graph.insert_anchor(txid, anchor); + if let Some((anchor, anchor_meta)) = anchor_from_status(txid, &status) { + let _ = tx_graph.insert_anchor(anchor, anchor_meta); } } } @@ -370,8 +369,8 @@ fn sync_for_index_and_graph_blocking( let _ = tx_graph.insert_tx(tx); } let status = client.get_tx_status(&op.txid)?; - if let Some(anchor) = anchor_from_status(&status) { - let _ = tx_graph.insert_anchor(op.txid, anchor); + if let Some((anchor, anchor_meta)) = anchor_from_status(op.txid, &status) { + let _ = tx_graph.insert_anchor(anchor, anchor_meta); } } @@ -382,8 +381,8 @@ fn sync_for_index_and_graph_blocking( let _ = tx_graph.insert_tx(tx); } let status = client.get_tx_status(&txid)?; - if let Some(anchor) = anchor_from_status(&status) { - let _ = tx_graph.insert_anchor(txid, anchor); + if let Some((anchor, anchor_meta)) = anchor_from_status(txid, &status) { + let _ = tx_graph.insert_anchor(anchor, anchor_meta); } } } @@ -396,10 +395,12 @@ fn sync_for_index_and_graph_blocking( #[cfg(test)] mod test { use crate::blocking_ext::{chain_update, fetch_latest_blocks}; - use bdk_chain::bitcoin::hashes::Hash; - use bdk_chain::bitcoin::Txid; - use bdk_chain::local_chain::LocalChain; - use bdk_chain::BlockId; + + use bdk_chain::{ + bitcoin::{hashes::Hash, Txid}, + local_chain::LocalChain, + BlockId, BlockTime, + }; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; use esplora_client::{BlockHash, Builder}; use std::collections::{BTreeMap, BTreeSet}; @@ -486,14 +487,17 @@ mod test { .iter() .map(|&height| -> anyhow::Result<_> { Ok(( - BlockId { - height, - hash: env.bitcoind.client.get_block_hash(height as _)?, - }, - Txid::all_zeros(), + ( + Txid::all_zeros(), + BlockId { + height, + hash: env.bitcoind.client.get_block_hash(height as _)?, + }, + ), + BlockTime::new(100), )) }) - .collect::>>()?; + .collect::>>()?; let update = chain_update( &client, &fetch_latest_blocks(&client)?, @@ -523,11 +527,14 @@ mod test { .iter() .map(|&(height, txid)| -> anyhow::Result<_> { Ok(( - BlockId { - height, - hash: env.bitcoind.client.get_block_hash(height as _)?, - }, - txid, + ( + txid, + BlockId { + height, + hash: env.bitcoind.client.get_block_hash(height as _)?, + }, + ), + BlockTime::new(100), )) }) .collect::>()?; @@ -719,13 +726,13 @@ mod test { let txid: Txid = bdk_chain::bitcoin::hashes::Hash::hash( &format!("txid_at_height_{}", h).into_bytes(), ); - let anchor = BlockId { + let blockid = BlockId { height: h, hash: anchor_blockhash, }; - (anchor, txid) + ((txid, blockid), BlockTime::new(100)) }) - .collect::>(); + .collect::>(); let chain_update = chain_update( &client, &fetch_latest_blocks(&client)?, diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs index 718d3cf9c..efadfda4d 100644 --- a/crates/esplora/src/lib.rs +++ b/crates/esplora/src/lib.rs @@ -16,7 +16,7 @@ //! [`TxGraph`]: bdk_chain::tx_graph::TxGraph //! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora -use bdk_chain::{BlockId, ConfirmationBlockTime}; +use bdk_chain::{bitcoin::Txid, Anchor, BlockId, BlockTime}; use esplora_client::TxStatus; pub use esplora_client; @@ -31,7 +31,7 @@ mod async_ext; #[cfg(feature = "async")] pub use async_ext::*; -fn anchor_from_status(status: &TxStatus) -> Option { +fn anchor_from_status(txid: Txid, status: &TxStatus) -> Option<(Anchor, BlockTime)> { if let TxStatus { block_height: Some(height), block_hash: Some(hash), @@ -39,10 +39,10 @@ fn anchor_from_status(status: &TxStatus) -> Option { .. } = status.clone() { - Some(ConfirmationBlockTime { - block_id: BlockId { height, hash }, - confirmation_time: time, - }) + Some(( + (txid, BlockId { height, hash }), + BlockTime::new(time as u32), + )) } else { None } diff --git a/crates/sqlite/src/store.rs b/crates/sqlite/src/store.rs index 5b7992518..7f146e7c3 100644 --- a/crates/sqlite/src/store.rs +++ b/crates/sqlite/src/store.rs @@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex}; use crate::Error; use bdk_chain::CombinedChangeSet; use bdk_chain::{ - indexed_tx_graph, indexer::keychain_txout, local_chain, tx_graph, Anchor, DescriptorExt, + indexed_tx_graph, indexer::keychain_txout, local_chain, tx_graph, BlockId, DescriptorExt, DescriptorId, Merge, }; @@ -39,7 +39,6 @@ impl Debug for Store { impl Store where K: Ord + for<'de> Deserialize<'de> + Serialize + Send, - A: Anchor + for<'de> Deserialize<'de> + Serialize + Send, { /// Creates a new store from a [`Connection`]. pub fn new(mut conn: Connection) -> Result { @@ -181,7 +180,6 @@ impl Store { impl Store where K: Ord + for<'de> Deserialize<'de> + Serialize + Send, - A: Anchor + Send, { /// Insert keychain with descriptor and last active index. /// @@ -414,7 +412,7 @@ impl Store { impl Store where K: Ord + for<'de> Deserialize<'de> + Serialize + Send, - A: Anchor + for<'de> Deserialize<'de> + Serialize + Send, + A: Ord + Clone + for<'de> Deserialize<'de> + Serialize + Send, { /// Insert anchors. fn insert_anchors( @@ -422,13 +420,13 @@ where tx_graph_changeset: &indexed_tx_graph::ChangeSet>, ) -> Result<(), Error> { // serde_json::to_string - for anchor in tx_graph_changeset.graph.anchors.iter() { + for ((txid, blockid), anchor_meta) in tx_graph_changeset.graph.anchors.iter() { let insert_anchor_stmt = &mut db_transaction .prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)") .expect("insert anchor statement"); - let block_hash = anchor.0.anchor_block().hash.to_string(); - let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json"); - let txid = anchor.1.to_string(); + let block_hash = blockid.hash.to_string(); + let anchor_json = serde_json::to_string(&(blockid, anchor_meta)).expect("anchor json"); + let txid = txid.to_string(); insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid }) .map_err(Error::Sqlite)?; } @@ -438,7 +436,7 @@ where /// Select all anchors. fn select_anchors( db_transaction: &rusqlite::Transaction, - ) -> Result, Error> { + ) -> Result, Error> { // serde_json::from_str let mut select_anchor_stmt = db_transaction .prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx") @@ -448,12 +446,13 @@ where let hash = row.get_unwrap::(0); let hash = BlockHash::from_str(hash.as_str()).expect("block hash"); let anchor = row.get_unwrap::(1); - let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor"); + let (blockid, anchor_meta): (BlockId, A) = + serde_json::from_str(anchor.as_str()).expect("anchor"); // double check anchor blob block hash matches - assert_eq!(hash, anchor.anchor_block().hash); + assert_eq!(hash, blockid.hash); let txid = row.get_unwrap::(2); let txid = Txid::from_str(&txid).expect("txid"); - Ok((anchor, txid)) + Ok(((txid, blockid), anchor_meta)) }) .map_err(Error::Sqlite)?; anchors @@ -467,7 +466,7 @@ where impl Store where K: Ord + for<'de> Deserialize<'de> + Serialize + Send, - A: Anchor + for<'de> Deserialize<'de> + Serialize + Send, + A: Ord + Clone + for<'de> Deserialize<'de> + Serialize + Send, { /// Write the given `changeset` atomically. pub fn write(&mut self, changeset: &CombinedChangeSet) -> Result<(), Error> { @@ -547,7 +546,7 @@ mod test { use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint}; use bdk_chain::miniscript::Descriptor; use bdk_chain::CombinedChangeSet; - use bdk_chain::{indexed_tx_graph, tx_graph, BlockId, ConfirmationBlockTime, DescriptorExt}; + use bdk_chain::{indexed_tx_graph, tx_graph, BlockId, BlockTime, DescriptorExt}; use std::str::FromStr; use std::sync::Arc; @@ -558,16 +557,13 @@ mod test { } #[test] - fn insert_and_load_aggregate_changesets_with_confirmation_block_time_anchor() { + fn insert_and_load_aggregate_changesets_with_block_time_anchor() { let (test_changesets, agg_test_changesets) = - create_test_changesets(&|height, time, hash| ConfirmationBlockTime { - confirmation_time: time, - block_id: (height, hash).into(), - }); + create_test_changesets(&|_height, time, _hash| BlockTime::new(time as u32)); let conn = Connection::open_in_memory().expect("in memory connection"); - let mut store = Store::::new(conn) - .expect("create new memory db store"); + let mut store = + Store::::new(conn).expect("create new memory db store"); test_changesets.iter().for_each(|changeset| { store.write(changeset).expect("write changeset"); @@ -578,24 +574,7 @@ mod test { assert_eq!(agg_changeset, Some(agg_test_changesets)); } - #[test] - fn insert_and_load_aggregate_changesets_with_blockid_anchor() { - let (test_changesets, agg_test_changesets) = - create_test_changesets(&|height, _time, hash| BlockId { height, hash }); - - let conn = Connection::open_in_memory().expect("in memory connection"); - let mut store = Store::::new(conn).expect("create new memory db store"); - - test_changesets.iter().for_each(|changeset| { - store.write(changeset).expect("write changeset"); - }); - - let agg_changeset = store.read().expect("aggregated changeset"); - - assert_eq!(agg_changeset, Some(agg_test_changesets)); - } - - fn create_test_changesets( + fn create_test_changesets( anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A, ) -> ( Vec>, @@ -645,13 +624,35 @@ mod test { let outpoint1_0 = OutPoint::new(tx1.compute_txid(), 0); let txout1_0 = tx1.output.first().unwrap().clone(); - let anchor1 = anchor_fn(1, 1296667328, block_hash_1); - let anchor2 = anchor_fn(2, 1296688946, block_hash_2); + let anchor_meta1 = anchor_fn(1, 1296667328, block_hash_1); + let anchor_meta2 = anchor_fn(2, 1296688946, block_hash_2); let tx_graph_changeset = tx_graph::ChangeSet:: { txs: [tx0.clone(), tx1.clone()].into(), txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(), - anchors: [(anchor1, tx0.compute_txid()), (anchor1, tx1.compute_txid())].into(), + anchors: [ + ( + ( + tx0.compute_txid(), + BlockId { + height: 1, + hash: block_hash_1, + }, + ), + anchor_meta1, + ), + ( + ( + tx1.compute_txid(), + BlockId { + height: 1, + hash: block_hash_1, + }, + ), + anchor_meta1, + ), + ] + .into(), last_seen: [ (tx0.compute_txid(), 1598918400), (tx1.compute_txid(), 1598919121), @@ -684,7 +685,7 @@ mod test { let tx_graph_changeset2 = tx_graph::ChangeSet:: { txs: [tx2.clone()].into(), txouts: BTreeMap::default(), - anchors: BTreeSet::default(), + anchors: BTreeMap::default(), last_seen: [(tx2.compute_txid(), 1708919121)].into(), }; @@ -704,7 +705,29 @@ mod test { let tx_graph_changeset3 = tx_graph::ChangeSet:: { txs: BTreeSet::default(), txouts: BTreeMap::default(), - anchors: [(anchor2, tx0.compute_txid()), (anchor2, tx1.compute_txid())].into(), + anchors: [ + ( + ( + tx0.compute_txid(), + BlockId { + height: 2, + hash: block_hash_2, + }, + ), + anchor_meta2, + ), + ( + ( + tx1.compute_txid(), + BlockId { + height: 2, + hash: block_hash_2, + }, + ), + anchor_meta2, + ), + ] + .into(), last_seen: BTreeMap::default(), }; diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 4b49db144..05155fcda 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -128,7 +128,7 @@ impl FullyNodedExport { 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::Confirmed((_, blockid), _) => blockid.height, bdk_chain::ChainPosition::Unconfirmed(_) => 0, } }) @@ -214,7 +214,7 @@ mod test { use core::str::FromStr; use crate::std::string::ToString; - use bdk_chain::{BlockId, ConfirmationBlockTime}; + use bdk_chain::{BlockId, BlockTime}; use bitcoin::hashes::Hash; use bitcoin::{transaction, BlockHash, Network, Transaction}; @@ -244,12 +244,9 @@ mod test { }) .unwrap(); wallet.insert_tx(transaction); - let anchor = ConfirmationBlockTime { - confirmation_time: 0, - block_id, - }; + let (anchor, anchor_meta) = ((txid, block_id), BlockTime::new(0)); let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); + let _ = graph.insert_anchor(anchor, anchor_meta); wallet .apply_update(Update { graph, diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 9db21ac71..e2fe238a3 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -28,8 +28,7 @@ use bdk_chain::{ }, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, tx_graph::{CanonicalTx, TxGraph, TxNode}, - BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, FullTxOut, Indexed, - IndexedTxGraph, Merge, + BlockId, BlockTime, ChainPosition, ConfirmationTime, FullTxOut, Indexed, IndexedTxGraph, Merge, }; use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; use bitcoin::{ @@ -104,7 +103,7 @@ pub struct Wallet { signers: Arc, change_signers: Arc, chain: LocalChain, - indexed_graph: IndexedTxGraph>, + indexed_graph: IndexedTxGraph>, stage: ChangeSet, network: Network, secp: SecpCtx, @@ -120,7 +119,7 @@ pub struct Update { pub last_active_indices: BTreeMap, /// Update for the wallet's internal [`TxGraph`]. - pub graph: TxGraph, + pub graph: TxGraph, /// Update for the wallet's internal [`LocalChain`]. /// @@ -149,7 +148,7 @@ impl From for Update { } /// The changes made to a wallet by applying an [`Update`]. -pub type ChangeSet = bdk_chain::CombinedChangeSet; +pub type ChangeSet = bdk_chain::CombinedChangeSet; /// A derived address and the index it was found at. /// For convenience this automatically derefs to `Address` @@ -996,18 +995,15 @@ impl Wallet { /// println!("my tx: {:#?}", canonical_tx.tx_node.tx); /// /// // list all transaction anchors - /// for anchor in canonical_tx.tx_node.anchors { - /// println!( - /// "tx is anchored by block of hash {}", - /// anchor.anchor_block().hash - /// ); + /// for ((txid, blockid), anchor_meta) in canonical_tx.tx_node.anchors { + /// println!("tx is anchored by block of hash {}", blockid.hash); /// } /// /// // get confirmation status of transaction /// match canonical_tx.chain_position { - /// ChainPosition::Confirmed(anchor) => println!( + /// ChainPosition::Confirmed((txid, blockid), anchor_meta) => println!( /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain", - /// anchor.block_id.height, anchor.block_id.height, anchor.block_id.hash, + /// blockid.height, blockid.height, blockid.hash, /// ), /// ChainPosition::Unconfirmed(last_seen) => println!( /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain", @@ -1017,10 +1013,7 @@ impl Wallet { /// ``` /// /// [`Anchor`]: bdk_chain::Anchor - pub fn get_tx( - &self, - txid: Txid, - ) -> Option, ConfirmationBlockTime>> { + pub fn get_tx(&self, txid: Txid) -> Option, BlockTime>> { let graph = self.indexed_graph.graph(); Some(CanonicalTx { @@ -1076,7 +1069,7 @@ impl Wallet { /// Iterate over the transactions in the wallet. pub fn transactions( &self, - ) -> impl Iterator, ConfirmationBlockTime>> + '_ { + ) -> impl Iterator, BlockTime>> + '_ { self.indexed_graph .graph() .list_canonical_txs(&self.chain, self.chain.tip().block_id()) @@ -1554,7 +1547,7 @@ impl Wallet { let pos = graph .get_chain_position(&self.chain, chain_tip, txid) .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?; - if let ChainPosition::Confirmed(_) = pos { + if let ChainPosition::Confirmed(_, _) = pos { return Err(BuildFeeBumpError::TransactionConfirmed(txid)); } @@ -1806,7 +1799,7 @@ impl Wallet { .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::Confirmed((_, blockid), _) => blockid.height, ChainPosition::Unconfirmed(_) => u32::MAX, }); let current_height = sign_options @@ -2244,7 +2237,7 @@ impl Wallet { } /// Get a reference to the inner [`TxGraph`]. - pub fn tx_graph(&self) -> &TxGraph { + pub fn tx_graph(&self) -> &TxGraph { self.indexed_graph.graph() } @@ -2252,7 +2245,7 @@ impl Wallet { /// because they haven't been broadcast. pub fn unbroadcast_transactions( &self, - ) -> impl Iterator, ConfirmationBlockTime>> { + ) -> impl Iterator, BlockTime>> { self.tx_graph().txs_with_no_anchor_or_last_seen() } @@ -2372,8 +2365,8 @@ impl Wallet { } } -impl AsRef> for Wallet { - fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { +impl AsRef> for Wallet { + fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { self.indexed_graph.graph() } } @@ -2412,7 +2405,7 @@ where fn new_local_utxo( keychain: KeychainKind, derivation_index: u32, - full_txo: FullTxOut, + full_txo: FullTxOut, ) -> LocalOutput { LocalOutput { outpoint: full_txo.outpoint, @@ -2475,7 +2468,7 @@ macro_rules! floating_rate { macro_rules! doctest_wallet { () => {{ use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; - use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph}; + use $crate::chain::{BlockTime, BlockId, TxGraph}; use $crate::wallet::{Update, Wallet}; use $crate::KeychainKind; let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; @@ -2502,12 +2495,10 @@ macro_rules! doctest_wallet { let _ = wallet.insert_checkpoint(block_id); let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() }); let _ = wallet.insert_tx(tx); - let anchor = ConfirmationBlockTime { - confirmation_time: 50_000, - block_id, - }; + let anchor = (txid, block_id); + let anchor_meta = BlockTime::new(50_000); let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); + let _ = graph.insert_anchor(anchor, anchor_meta); let update = Update { graph, ..Default::default() }; wallet.apply_update(update).unwrap(); wallet diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 9774ec985..9c6c45b05 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,5 +1,5 @@ #![allow(unused)] -use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; +use bdk_chain::{BlockId, BlockTime, ConfirmationTime, TxGraph}; use bdk_wallet::{ wallet::{Update, Wallet}, KeychainKind, LocalOutput, @@ -207,18 +207,15 @@ pub fn feerate_unchecked(sat_vb: f64) -> FeeRate { 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 + let (anchor, anchor_meta) = wallet .local_chain() .range(height..) .last() - .map(|anchor_cp| ConfirmationBlockTime { - block_id: anchor_cp.block_id(), - confirmation_time: time, - }) + .map(|anchor_cp| ((txid, anchor_cp.block_id()), BlockTime::new(time as u32))) .expect("confirmation height cannot be greater than tip"); let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); + let _ = graph.insert_anchor(anchor, anchor_meta); wallet .apply_update(Update { graph, diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index c71b18fed..9a9f0e9e8 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -16,7 +16,7 @@ use bdk_chain::{ indexed_tx_graph, indexer::keychain_txout, local_chain::{self, LocalChain}, - ConfirmationBlockTime, IndexedTxGraph, Merge, + BlockTime, IndexedTxGraph, Merge, }; use example_cli::{ anyhow, @@ -38,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60); type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); #[derive(Debug)] diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 9327f7873..b8160ba69 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -20,7 +20,7 @@ use bdk_chain::{ descriptor::{DescriptorSecretKey, KeyMap}, Descriptor, DescriptorPublicKey, }, - Anchor, ChainOracle, DescriptorExt, FullTxOut, Merge, + ChainOracle, DescriptorExt, FullTxOut, Merge, }; pub use bdk_file_store; pub use clap; @@ -196,7 +196,7 @@ pub struct CreateTxChange { pub index: u32, } -pub fn create_tx( +pub fn create_tx( graph: &mut KeychainTxGraph, chain: &O, keymap: &BTreeMap, @@ -206,6 +206,7 @@ pub fn create_tx( ) -> anyhow::Result<(Transaction, Option)> where O::Error: std::error::Error + Send + Sync + 'static, + A: Ord + Clone, { let mut changeset = keychain_txout::ChangeSet::default(); @@ -415,7 +416,7 @@ where // Alias the elements of `Result` of `planned_utxos` pub type PlannedUtxo = (bdk_tmp_plan::Plan, FullTxOut); -pub fn planned_utxos( +pub fn planned_utxos( graph: &KeychainTxGraph, chain: &O, assets: &bdk_tmp_plan::Assets, @@ -444,7 +445,7 @@ pub fn planned_utxos( +pub fn handle_commands( graph: &Mutex>, db: &Mutex>, chain: &Mutex, @@ -454,6 +455,7 @@ pub fn handle_commands, ) -> anyhow::Result<()> where + A: Ord + Clone + Debug, O::Error: std::error::Error + Send + Sync + 'static, C: Default + Merge diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 31e8e7041..86109ceaf 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -10,7 +10,7 @@ use bdk_chain::{ indexer::keychain_txout, local_chain::{self, LocalChain}, spk_client::{FullScanRequest, SyncRequest}, - ConfirmationBlockTime, Merge, + BlockTime, Merge, }; use bdk_electrum::{ electrum_client::{self, Client, ElectrumApi}, @@ -100,7 +100,7 @@ pub struct ScanOptions { type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); fn main() -> anyhow::Result<()> { @@ -337,8 +337,7 @@ fn main() -> anyhow::Result<()> { let chain_changeset = chain.apply_update(chain_update)?; - let mut indexed_tx_graph_changeset = - indexed_tx_graph::ChangeSet::::default(); + let mut indexed_tx_graph_changeset = indexed_tx_graph::ChangeSet::::default(); if let Some(keychain_update) = keychain_update { let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update); indexed_tx_graph_changeset.merge(keychain_changeset.into()); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index ffa2ea24e..45cf5f7a5 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -10,7 +10,7 @@ use bdk_chain::{ indexer::keychain_txout, local_chain::{self, LocalChain}, spk_client::{FullScanRequest, SyncRequest}, - ConfirmationBlockTime, Merge, + BlockTime, Merge, }; use bdk_esplora::{esplora_client, EsploraExt}; @@ -26,7 +26,7 @@ const DB_PATH: &str = ".bdk_esplora_example.db"; type ChangeSet = ( local_chain::ChangeSet, - indexed_tx_graph::ChangeSet>, + indexed_tx_graph::ChangeSet>, ); #[derive(Subcommand, Debug, Clone)]