From 37fca35ddede6cbc9d9428a2722eff82a405b1b2 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Fri, 29 Mar 2024 21:37:38 -0400 Subject: [PATCH 1/2] feat(tx_graph): Add method update_last_seen_unconfirmed That accepts a `u64` as param representing the latest timestamp and internally calls `insert_seen_at` for all transactions in graph that aren't yet anchored in a confirmed block. --- crates/chain/src/tx_graph.rs | 65 ++++++++++++++++++++++++++++- crates/chain/tests/test_tx_graph.rs | 28 +++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 34cbccf5c..06bbc2b32 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -541,7 +541,11 @@ impl TxGraph { /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. /// - /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. + /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch + /// update all unconfirmed transactions with the latest `seen_at`, see + /// [`update_last_seen_unconfirmed`]. + /// + /// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { let mut update = Self::default(); let (_, _, update_last_seen) = update.txs.entry(txid).or_default(); @@ -549,6 +553,65 @@ impl TxGraph { self.apply_update(update) } + /// Update the last seen time for all unconfirmed transactions. + /// + /// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting + /// the given `seen_at` for every transaction not yet anchored to a confirmed block, + /// and returns the [`ChangeSet`] after applying all updates to `self`. + /// + /// This is useful for keeping track of the latest time a transaction was seen + /// unconfirmed, which is important for evaluating transaction conflicts in the same + /// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for + /// [`try_get_chain_position`]. + /// + /// A normal use of this method is to call it with the current system time. Although + /// block headers contain a timestamp, using the header time would be less effective + /// at tracking mempool transactions, because it can drift from actual clock time, plus + /// we may want to update a transaction's last seen time repeatedly between blocks. + /// + /// # Example + /// + /// ```rust + /// # use bdk_chain::example_utils::*; + /// # use std::time::UNIX_EPOCH; + /// # let tx = tx_from_hex(RAW_TX_1); + /// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]); + /// let now = std::time::SystemTime::now() + /// .duration_since(UNIX_EPOCH) + /// .expect("valid duration") + /// .as_secs(); + /// let changeset = tx_graph.update_last_seen_unconfirmed(now); + /// assert!(!changeset.last_seen.is_empty()); + /// ``` + /// + /// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must + /// by strictly greater than what is currently stored for a transaction to have an effect. + /// To insert a last seen time for a single txid, see [`insert_seen_at`]. + /// + /// [`insert_seen_at`]: Self::insert_seen_at + /// [`try_get_chain_position`]: Self::try_get_chain_position + pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet { + let mut changeset = ChangeSet::default(); + let unanchored_txs: Vec = self + .txs + .iter() + .filter_map( + |(&txid, (_, anchors, _))| { + if anchors.is_empty() { + Some(txid) + } else { + None + } + }, + ) + .collect(); + + for txid in unanchored_txs { + changeset.append(self.insert_seen_at(txid, seen_at)); + } + changeset + } + /// Extends this graph with another so that `self` becomes the union of the two sets of /// transactions. /// diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 37e8c7192..c646d431f 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1048,6 +1048,34 @@ fn test_changeset_last_seen_append() { } } +#[test] +fn update_last_seen_unconfirmed() { + let mut graph = TxGraph::<()>::default(); + let tx = new_tx(0); + let txid = tx.txid(); + + // insert a new tx + // initially we have a last_seen of 0, and no anchors + let _ = graph.insert_tx(tx); + let tx = graph.full_txs().next().unwrap(); + assert_eq!(tx.last_seen_unconfirmed, 0); + assert!(tx.anchors.is_empty()); + + // higher timestamp should update last seen + let changeset = graph.update_last_seen_unconfirmed(2); + assert_eq!(changeset.last_seen.get(&txid).unwrap(), &2); + + // lower timestamp has no effect + let changeset = graph.update_last_seen_unconfirmed(1); + assert!(changeset.last_seen.is_empty()); + + // once anchored, last seen is not updated + let _ = graph.insert_anchor(txid, ()); + let changeset = graph.update_last_seen_unconfirmed(4); + assert!(changeset.is_empty()); + assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2); +} + #[test] fn test_missing_blocks() { /// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`. From a2a64ffb6e92baf46a912f36294f3f4f521a528a Mon Sep 17 00:00:00 2001 From: valued mammal Date: Tue, 2 Apr 2024 10:19:56 -0400 Subject: [PATCH 2/2] fix(electrum)!: Remove `seen_at` param from `into_tx_graph` and `into_confirmation_time_tx_graph`, since now it makes sense to use `TxGraph::update_last_seen_unconfirmed`. Also, use `update_last_seen_unconfirmed` in examples for electrum/esplora. We show how to update the last seen time for transactions by calling `update_last_seen_unconfirmed` on the graph update returned from a blockchain source, passing in the current time, before applying it to another `TxGraph`. --- crates/electrum/src/electrum_ext.rs | 7 +------ crates/electrum/tests/test_electrum.rs | 7 +++---- example-crates/example_electrum/src/main.rs | 4 ++-- example-crates/example_esplora/src/main.rs | 12 ++++++++++-- example-crates/wallet_electrum/src/main.rs | 4 +++- example-crates/wallet_esplora_async/src/main.rs | 5 ++++- example-crates/wallet_esplora_blocking/src/main.rs | 5 ++++- 7 files changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/electrum/src/electrum_ext.rs b/crates/electrum/src/electrum_ext.rs index 5501b1495..3ff467fce 100644 --- a/crates/electrum/src/electrum_ext.rs +++ b/crates/electrum/src/electrum_ext.rs @@ -40,15 +40,11 @@ impl RelevantTxids { pub fn into_tx_graph( self, client: &Client, - seen_at: Option, missing: Vec, ) -> Result, Error> { let new_txs = client.batch_transaction_get(&missing)?; let mut graph = TxGraph::::new(new_txs); for (txid, anchors) in self.0 { - if let Some(seen_at) = seen_at { - let _ = graph.insert_seen_at(txid, seen_at); - } for anchor in anchors { let _ = graph.insert_anchor(txid, anchor); } @@ -67,10 +63,9 @@ impl RelevantTxids { pub fn into_confirmation_time_tx_graph( self, client: &Client, - seen_at: Option, missing: Vec, ) -> Result, Error> { - let graph = self.into_tx_graph(client, seen_at, missing)?; + let graph = self.into_tx_graph(client, missing)?; let relevant_heights = { let mut visited_heights = HashSet::new(); diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 0dc80ac61..e6c93651d 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -68,7 +68,7 @@ fn scan_detects_confirmed_tx() -> Result<()> { } = client.sync(recv_chain.tip(), [spk_to_track], None, None, 5)?; let missing = relevant_txids.missing_full_txs(recv_graph.graph()); - let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; + let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?; let _ = recv_chain .apply_update(chain_update) .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; @@ -134,7 +134,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> { } = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?; let missing = relevant_txids.missing_full_txs(recv_graph.graph()); - let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; + let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?; let _ = recv_chain .apply_update(chain_update) .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; @@ -164,8 +164,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> { } = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?; let missing = relevant_txids.missing_full_txs(recv_graph.graph()); - let graph_update = - relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; + let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?; let _ = recv_chain .apply_update(chain_update) .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index df34795bd..f651b85e2 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -299,12 +299,12 @@ fn main() -> anyhow::Result<()> { relevant_txids.missing_full_txs(graph.graph()) }; + let mut graph_update = relevant_txids.into_tx_graph(&client, missing_txids)?; let now = std::time::UNIX_EPOCH .elapsed() .expect("must get time") .as_secs(); - - let graph_update = relevant_txids.into_tx_graph(&client, Some(now), missing_txids)?; + let _ = graph_update.update_last_seen_unconfirmed(now); let db_changeset = { let mut chain = chain.lock().unwrap(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index e92205706..1d11cc68f 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -189,10 +189,14 @@ fn main() -> anyhow::Result<()> { // is reached. It returns a `TxGraph` update (`graph_update`) and a structure that // represents the last active spk derivation indices of keychains // (`keychain_indices_update`). - let (graph_update, last_active_indices) = client + let (mut graph_update, last_active_indices) = client .full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests) .context("scanning for transactions")?; + // We want to keep track of the latest time a transaction was seen unconfirmed. + let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); + let _ = graph_update.update_last_seen_unconfirmed(now); + let mut graph = graph.lock().expect("mutex must not be poisoned"); // Because we did a stop gap based scan we are likely to have some updates to our // deriviation indices. Usually before a scan you are on a fresh wallet with no @@ -307,9 +311,13 @@ fn main() -> anyhow::Result<()> { } } - let graph_update = + let mut graph_update = client.sync(spks, txids, outpoints, scan_options.parallel_requests)?; + // Update last seen unconfirmed + let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); + let _ = graph_update.update_last_seen_unconfirmed(now); + graph.lock().unwrap().apply_update(graph_update) } }; diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 4f8aba9fd..e53e19d4f 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -66,7 +66,9 @@ fn main() -> Result<(), anyhow::Error> { println!(); let missing = relevant_txids.missing_full_txs(wallet.as_ref()); - let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?; + let mut graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?; + let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); + let _ = graph_update.update_last_seen_unconfirmed(now); let wallet_update = Update { last_active_indices: keychain_update, diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index 690cd87e2..02dde7e5b 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -53,9 +53,12 @@ async fn main() -> Result<(), anyhow::Error> { (k, k_spks) }) .collect(); - let (update_graph, last_active_indices) = client + let (mut update_graph, last_active_indices) = client .full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS) .await?; + + let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); + let _ = update_graph.update_last_seen_unconfirmed(now); let missing_heights = update_graph.missing_heights(wallet.local_chain()); let chain_update = client.update_local_chain(prev_tip, missing_heights).await?; let update = Update { diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 73bfdd559..653f768b9 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -53,8 +53,11 @@ fn main() -> Result<(), anyhow::Error> { }) .collect(); - let (update_graph, last_active_indices) = + let (mut update_graph, last_active_indices) = client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)?; + + let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); + let _ = update_graph.update_last_seen_unconfirmed(now); let missing_heights = update_graph.missing_heights(wallet.local_chain()); let chain_update = client.update_local_chain(prev_tip, missing_heights)?; let update = Update {