Skip to content

Commit 62619d3

Browse files
committed
Merge #1385: Fix last seen unconfirmed
a2a64ff fix(electrum)!: Remove `seen_at` param from `into_tx_graph` (valued mammal) 37fca35 feat(tx_graph): Add method update_last_seen_unconfirmed (valued mammal) Pull request description: A method `update_last_seen_unconfirmed` is added for `TxGraph` that allows inserting a `seen_at` time for all unanchored transactions. fixes #1336 ### Changelog notice Changed: - Methods `into_tx_graph` and `into_confirmation_time_tx_graph`for `RelevantTxids` are changed to no longer accept a `seen_at` parameter. Added: - Added method `update_last_seen_unconfirmed` for `TxGraph`. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing ACKs for top commit: evanlinjin: ACK a2a64ff Tree-SHA512: 9011e63314b0e3ffcd50dbf5024f82b365bab1cc834c0455d7410b682338339ed5284caa721ffc267c65fa26d35ff6ee86cde6052e82a6a79768547fbb7a45eb
2 parents 53791eb + a2a64ff commit 62619d3

File tree

9 files changed

+119
-18
lines changed

9 files changed

+119
-18
lines changed

crates/chain/src/tx_graph.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,14 +554,77 @@ impl<A: Clone + Ord> TxGraph<A> {
554554

555555
/// Inserts the given `seen_at` for `txid` into [`TxGraph`].
556556
///
557-
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`.
557+
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch
558+
/// update all unconfirmed transactions with the latest `seen_at`, see
559+
/// [`update_last_seen_unconfirmed`].
560+
///
561+
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
558562
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
559563
let mut update = Self::default();
560564
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
561565
*update_last_seen = seen_at;
562566
self.apply_update(update)
563567
}
564568

569+
/// Update the last seen time for all unconfirmed transactions.
570+
///
571+
/// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting
572+
/// the given `seen_at` for every transaction not yet anchored to a confirmed block,
573+
/// and returns the [`ChangeSet`] after applying all updates to `self`.
574+
///
575+
/// This is useful for keeping track of the latest time a transaction was seen
576+
/// unconfirmed, which is important for evaluating transaction conflicts in the same
577+
/// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for
578+
/// [`try_get_chain_position`].
579+
///
580+
/// A normal use of this method is to call it with the current system time. Although
581+
/// block headers contain a timestamp, using the header time would be less effective
582+
/// at tracking mempool transactions, because it can drift from actual clock time, plus
583+
/// we may want to update a transaction's last seen time repeatedly between blocks.
584+
///
585+
/// # Example
586+
///
587+
/// ```rust
588+
/// # use bdk_chain::example_utils::*;
589+
/// # use std::time::UNIX_EPOCH;
590+
/// # let tx = tx_from_hex(RAW_TX_1);
591+
/// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]);
592+
/// let now = std::time::SystemTime::now()
593+
/// .duration_since(UNIX_EPOCH)
594+
/// .expect("valid duration")
595+
/// .as_secs();
596+
/// let changeset = tx_graph.update_last_seen_unconfirmed(now);
597+
/// assert!(!changeset.last_seen.is_empty());
598+
/// ```
599+
///
600+
/// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must
601+
/// by strictly greater than what is currently stored for a transaction to have an effect.
602+
/// To insert a last seen time for a single txid, see [`insert_seen_at`].
603+
///
604+
/// [`insert_seen_at`]: Self::insert_seen_at
605+
/// [`try_get_chain_position`]: Self::try_get_chain_position
606+
pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet<A> {
607+
let mut changeset = ChangeSet::default();
608+
let unanchored_txs: Vec<Txid> = self
609+
.txs
610+
.iter()
611+
.filter_map(
612+
|(&txid, (_, anchors, _))| {
613+
if anchors.is_empty() {
614+
Some(txid)
615+
} else {
616+
None
617+
}
618+
},
619+
)
620+
.collect();
621+
622+
for txid in unanchored_txs {
623+
changeset.append(self.insert_seen_at(txid, seen_at));
624+
}
625+
changeset
626+
}
627+
565628
/// Extends this graph with another so that `self` becomes the union of the two sets of
566629
/// transactions.
567630
///

crates/chain/tests/test_tx_graph.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,34 @@ fn test_changeset_last_seen_append() {
10581058
}
10591059
}
10601060

1061+
#[test]
1062+
fn update_last_seen_unconfirmed() {
1063+
let mut graph = TxGraph::<()>::default();
1064+
let tx = new_tx(0);
1065+
let txid = tx.txid();
1066+
1067+
// insert a new tx
1068+
// initially we have a last_seen of 0, and no anchors
1069+
let _ = graph.insert_tx(tx);
1070+
let tx = graph.full_txs().next().unwrap();
1071+
assert_eq!(tx.last_seen_unconfirmed, 0);
1072+
assert!(tx.anchors.is_empty());
1073+
1074+
// higher timestamp should update last seen
1075+
let changeset = graph.update_last_seen_unconfirmed(2);
1076+
assert_eq!(changeset.last_seen.get(&txid).unwrap(), &2);
1077+
1078+
// lower timestamp has no effect
1079+
let changeset = graph.update_last_seen_unconfirmed(1);
1080+
assert!(changeset.last_seen.is_empty());
1081+
1082+
// once anchored, last seen is not updated
1083+
let _ = graph.insert_anchor(txid, ());
1084+
let changeset = graph.update_last_seen_unconfirmed(4);
1085+
assert!(changeset.is_empty());
1086+
assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2);
1087+
}
1088+
10611089
#[test]
10621090
fn test_missing_blocks() {
10631091
/// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.

crates/electrum/src/electrum_ext.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,11 @@ impl RelevantTxids {
4040
pub fn into_tx_graph(
4141
self,
4242
client: &Client,
43-
seen_at: Option<u64>,
4443
missing: Vec<Txid>,
4544
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
4645
let new_txs = client.batch_transaction_get(&missing)?;
4746
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
4847
for (txid, anchors) in self.0 {
49-
if let Some(seen_at) = seen_at {
50-
let _ = graph.insert_seen_at(txid, seen_at);
51-
}
5248
for anchor in anchors {
5349
let _ = graph.insert_anchor(txid, anchor);
5450
}
@@ -67,10 +63,9 @@ impl RelevantTxids {
6763
pub fn into_confirmation_time_tx_graph(
6864
self,
6965
client: &Client,
70-
seen_at: Option<u64>,
7166
missing: Vec<Txid>,
7267
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
73-
let graph = self.into_tx_graph(client, seen_at, missing)?;
68+
let graph = self.into_tx_graph(client, missing)?;
7469

7570
let relevant_heights = {
7671
let mut visited_heights = HashSet::new();

crates/electrum/tests/test_electrum.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ fn scan_detects_confirmed_tx() -> Result<()> {
6868
} = client.sync(recv_chain.tip(), [spk_to_track], None, None, 5)?;
6969

7070
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
71-
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
71+
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
7272
let _ = recv_chain
7373
.apply_update(chain_update)
7474
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
@@ -134,7 +134,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
134134
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
135135

136136
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
137-
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
137+
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
138138
let _ = recv_chain
139139
.apply_update(chain_update)
140140
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
@@ -164,8 +164,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
164164
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
165165

166166
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
167-
let graph_update =
168-
relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
167+
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
169168
let _ = recv_chain
170169
.apply_update(chain_update)
171170
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;

example-crates/example_electrum/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,12 @@ fn main() -> anyhow::Result<()> {
299299
relevant_txids.missing_full_txs(graph.graph())
300300
};
301301

302+
let mut graph_update = relevant_txids.into_tx_graph(&client, missing_txids)?;
302303
let now = std::time::UNIX_EPOCH
303304
.elapsed()
304305
.expect("must get time")
305306
.as_secs();
306-
307-
let graph_update = relevant_txids.into_tx_graph(&client, Some(now), missing_txids)?;
307+
let _ = graph_update.update_last_seen_unconfirmed(now);
308308

309309
let db_changeset = {
310310
let mut chain = chain.lock().unwrap();

example-crates/example_esplora/src/main.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,14 @@ fn main() -> anyhow::Result<()> {
189189
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
190190
// represents the last active spk derivation indices of keychains
191191
// (`keychain_indices_update`).
192-
let (graph_update, last_active_indices) = client
192+
let (mut graph_update, last_active_indices) = client
193193
.full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
194194
.context("scanning for transactions")?;
195195

196+
// We want to keep track of the latest time a transaction was seen unconfirmed.
197+
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
198+
let _ = graph_update.update_last_seen_unconfirmed(now);
199+
196200
let mut graph = graph.lock().expect("mutex must not be poisoned");
197201
// Because we did a stop gap based scan we are likely to have some updates to our
198202
// deriviation indices. Usually before a scan you are on a fresh wallet with no
@@ -307,9 +311,13 @@ fn main() -> anyhow::Result<()> {
307311
}
308312
}
309313

310-
let graph_update =
314+
let mut graph_update =
311315
client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
312316

317+
// Update last seen unconfirmed
318+
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
319+
let _ = graph_update.update_last_seen_unconfirmed(now);
320+
313321
graph.lock().unwrap().apply_update(graph_update)
314322
}
315323
};

example-crates/wallet_electrum/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ fn main() -> Result<(), anyhow::Error> {
6666
println!();
6767

6868
let missing = relevant_txids.missing_full_txs(wallet.as_ref());
69-
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, None, missing)?;
69+
let mut graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
70+
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
71+
let _ = graph_update.update_last_seen_unconfirmed(now);
7072

7173
let wallet_update = Update {
7274
last_active_indices: keychain_update,

example-crates/wallet_esplora_async/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,12 @@ async fn main() -> Result<(), anyhow::Error> {
5353
(k, k_spks)
5454
})
5555
.collect();
56-
let (update_graph, last_active_indices) = client
56+
let (mut update_graph, last_active_indices) = client
5757
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)
5858
.await?;
59+
60+
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
61+
let _ = update_graph.update_last_seen_unconfirmed(now);
5962
let missing_heights = update_graph.missing_heights(wallet.local_chain());
6063
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
6164
let update = Update {

example-crates/wallet_esplora_blocking/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ fn main() -> Result<(), anyhow::Error> {
5353
})
5454
.collect();
5555

56-
let (update_graph, last_active_indices) =
56+
let (mut update_graph, last_active_indices) =
5757
client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)?;
58+
59+
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
60+
let _ = update_graph.update_last_seen_unconfirmed(now);
5861
let missing_heights = update_graph.missing_heights(wallet.local_chain());
5962
let chain_update = client.update_local_chain(prev_tip, missing_heights)?;
6063
let update = Update {

0 commit comments

Comments
 (0)