From: 志宇 Date: Fri, 23 Aug 2024 09:53:42 +0000 (+0000) Subject: feat(chain)!: `TxGraph::apply_update` auto-adds `seen_at` for unanchored X-Git-Tag: v1.0.0-beta.2~3^2~2 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/struct.EncoderStringWriter.html?a=commitdiff_plain;h=1adcb62ff0a7dd62e2c644e69365008e7f8a4389;p=bdk feat(chain)!: `TxGraph::apply_update` auto-adds `seen_at` for unanchored Change `apply_update` to use the current timestamp as `seen_at` for unanchored transactions of the update. This makes `apply_update` only avaliable with the "std" feature. Introduce `apply_update_at` which includes an optional `seen_at` input. This is the no-std version of `apply_update`. Also update docs. --- diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index d24b1b30..73ae458f 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -90,13 +90,38 @@ where /// Apply an `update` directly. /// - /// `update` is a [`TxGraph`] and the resultant changes is returned as [`ChangeSet`]. + /// `update` is a [`tx_graph::Update`] and the resultant changes is returned as [`ChangeSet`]. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: tx_graph::Update) -> ChangeSet { let tx_graph = self.graph.apply_update(update); let indexer = self.index_tx_graph_changeset(&tx_graph); ChangeSet { tx_graph, indexer } } + /// Apply the given `update` with an optional `seen_at` timestamp. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](IndexedTxGraph::apply_update) to have the `seen_at` value automatically + /// set to the current time. + pub fn apply_update_at( + &mut self, + update: tx_graph::Update, + seen_at: Option, + ) -> ChangeSet { + let tx_graph = self.graph.apply_update_at(update, seen_at); + let indexer = self.index_tx_graph_changeset(&tx_graph); + ChangeSet { tx_graph, indexer } + } + /// Insert a floating `txout` of given `outpoint`. pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { let graph = self.graph.insert_txout(outpoint, txout); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index ba894fa9..6da4d8af 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -146,29 +146,12 @@ impl From> for Update { impl From> for TxGraph { fn from(update: Update) -> Self { let mut graph = TxGraph::::default(); - let _ = graph.apply_update(update); + let _ = graph.apply_update_at(update, None); graph } } impl Update { - /// Update the [`seen_ats`](Self::seen_ats) for all unanchored transactions. - pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) { - let seen_ats = &mut self.seen_ats; - let anchors = &self.anchors; - let unanchored_txids = self.txs.iter().map(|tx| tx.compute_txid()).filter(|txid| { - for (_, anchor_txid) in anchors { - if txid == anchor_txid { - return false; - } - } - true - }); - for txid in unanchored_txids { - seen_ats.insert(txid, seen_at); - } - } - /// Extend this update with `other`. pub fn extend(&mut self, other: Update) { self.txs.extend(other.txs); @@ -762,25 +745,56 @@ impl TxGraph { changeset } - /// Extends this graph with another so that `self` becomes the union of the two sets of - /// transactions. + /// Extends this graph with the given `update`. /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: Update) -> ChangeSet { + use std::time::*; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("current time must be greater than epoch anchor"); + self.apply_update_at(update, Some(now.as_secs())) + } + + /// Extends this graph with the given `update` alongside an optional `seen_at` timestamp. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](TxGraph::apply_update) to have the `seen_at` value automatically set + /// to the current time. + pub fn apply_update_at(&mut self, update: Update, seen_at: Option) -> ChangeSet { let mut changeset = ChangeSet::::default(); + let mut unanchored_txs = HashSet::::new(); for tx in update.txs { - changeset.merge(self.insert_tx(tx)); + if unanchored_txs.insert(tx.compute_txid()) { + changeset.merge(self.insert_tx(tx)); + } } for (outpoint, txout) in update.txouts { changeset.merge(self.insert_txout(outpoint, txout)); } for (anchor, txid) in update.anchors { + unanchored_txs.remove(&txid); changeset.merge(self.insert_anchor(txid, anchor)); } for (txid, seen_at) in update.seen_ats { changeset.merge(self.insert_seen_at(txid, seen_at)); } + if let Some(seen_at) = seen_at { + for txid in unanchored_txs { + changeset.merge(self.insert_seen_at(txid, seen_at)); + } + } changeset } diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index e8b054d3..d5e4a159 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -38,19 +38,12 @@ where Spks: IntoIterator, Spks::IntoIter: ExactSizeIterator + Send + 'static, { - let mut update = client.sync( + let update = client.sync( SyncRequest::builder().chain_tip(chain.tip()).spks(spks), BATCH_SIZE, true, )?; - // Update `last_seen` to be able to calculate balance for unconfirmed transactions. - let now = std::time::UNIX_EPOCH - .elapsed() - .expect("must get time") - .as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); - if let Some(chain_update) = update.chain_update.clone() { let _ = chain .apply_update(chain_update) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 0d6cdf18..638bb575 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2277,7 +2277,34 @@ impl Wallet { /// to persist staged wallet changes see [`Wallet::reveal_next_address`]. ` /// /// [`commit`]: Self::commit + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { + use std::time::*; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time now must surpass epoch anchor"); + self.apply_update_at(update, Some(now.as_secs())) + } + + /// Applies an `update` alongside an optional `seen_at` timestamp and stages the changes. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](Wallet::apply_update) to have the `seen_at` value automatically set to + /// the current time. + pub fn apply_update_at( + &mut self, + update: impl Into, + seen_at: Option, + ) -> Result<(), CannotConnectError> { let update = update.into(); let mut changeset = match update.chain { Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?), @@ -2289,7 +2316,11 @@ impl Wallet { .index .reveal_to_target_multi(&update.last_active_indices); changeset.merge(index_changeset.into()); - changeset.merge(self.indexed_graph.apply_update(update.graph).into()); + changeset.merge( + self.indexed_graph + .apply_update_at(update.graph, seen_at) + .into(), + ); self.stage.merge(changeset); Ok(()) } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 7212547d..662bc423 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -129,7 +129,7 @@ fn main() -> anyhow::Result<()> { // Tell the electrum client about the txs we've already got locally so it doesn't re-download them client.populate_tx_cache(&*graph.lock().unwrap()); - let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() { + let (chain_update, graph_update, keychain_update) = match electrum_cmd.clone() { ElectrumCommands::Scan { stop_gap, scan_options, @@ -248,12 +248,6 @@ fn main() -> anyhow::Result<()> { } }; - let now = std::time::UNIX_EPOCH - .elapsed() - .expect("must get time") - .as_secs(); - graph_update.update_last_seen_unconfirmed(now); - let db_changeset = { let mut chain = chain.lock().unwrap(); let mut graph = graph.lock().unwrap(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index d188eab7..d4692e35 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -166,14 +166,10 @@ 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 mut update = client + let update = client .full_scan(request, *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(); - update.graph_update.update_last_seen_unconfirmed(now); - let mut graph = graph.lock().expect("mutex must not be poisoned"); let mut chain = chain.lock().expect("mutex must not be poisoned"); // Because we did a stop gap based scan we are likely to have some updates to our @@ -265,11 +261,7 @@ fn main() -> anyhow::Result<()> { } } - let mut update = client.sync(request, scan_options.parallel_requests)?; - - // Update last seen unconfirmed - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.sync(request, scan_options.parallel_requests)?; ( chain diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 4cc698a0..c0518405 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -64,10 +64,7 @@ fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; - - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; println!(); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index d4dae1f3..6fd215df 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -57,11 +57,9 @@ async fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client + let update = client .full_scan(request, STOP_GAP, PARALLEL_REQUESTS) .await?; - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; wallet.persist(&mut conn)?; diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 9f79d6bf..45e4685b 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -59,9 +59,7 @@ fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; wallet.apply_update(update)?; if let Some(changeset) = wallet.take_staged() {