Skip to content

Commit

Permalink
fix(esplora)!: Change sync, full_scan to take a timestamp parameter
Browse files Browse the repository at this point in the history
This is used for setting the time a transaction was last
seen in mempool. When doing a sync or full scan, the caller
must specify the current time, for example as a UNIX
timestamp.
  • Loading branch information
ValuedMammal committed Mar 23, 2024
1 parent a837cd3 commit 3162957
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 16 deletions.
23 changes: 22 additions & 1 deletion crates/esplora/src/async_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ pub trait EsploraAsyncExt {
///
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
/// parallel. `time` is the current time, typically a UNIX timestamp, used only when setting
/// the time a transaction was last seen unconfirmed.
async fn full_scan<K: Ord + Clone + Send>(
&self,
keychain_spks: BTreeMap<
Expand All @@ -60,6 +61,7 @@ pub trait EsploraAsyncExt {
>,
stop_gap: usize,
parallel_requests: usize,
time: u64,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;

/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
Expand All @@ -69,6 +71,7 @@ pub trait EsploraAsyncExt {
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to include in the update
/// * `time`: UNIX timestamp used to set the time a transaction was last seen unconfirmed
///
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
/// may include scripts that have been used, use [`full_scan`] with the keychain.
Expand All @@ -80,6 +83,7 @@ pub trait EsploraAsyncExt {
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize,
time: u64,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
}

Expand Down Expand Up @@ -157,6 +161,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
>,
stop_gap: usize,
parallel_requests: usize,
time: u64,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
let parallel_requests = Ord::max(parallel_requests, 1);
Expand Down Expand Up @@ -204,6 +209,9 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
if let Some(anchor) = anchor_from_status(&tx.status) {
let _ = graph.insert_anchor(tx.txid, anchor);
}
if !tx.status.confirmed {
let _ = graph.insert_seen_at(tx.txid, time);
}

let previous_outputs = tx.vin.iter().filter_map(|vin| {
let prevout = vin.prevout.as_ref()?;
Expand Down Expand Up @@ -250,6 +258,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize,
time: u64,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let mut graph = self
.full_scan(
Expand All @@ -263,6 +272,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
.into(),
usize::MAX,
parallel_requests,
time,
)
.await
.map(|(g, _)| g)?;
Expand All @@ -287,10 +297,14 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(txid, anchor);
}
if !status.confirmed {
let _ = graph.insert_seen_at(txid, time);
}
}
}

for op in outpoints.into_iter() {
// get tx for this outpoint
if graph.get_tx(op.txid).is_none() {
if let Some(tx) = self.get_tx(&op.txid).await? {
let _ = graph.insert_tx(tx);
Expand All @@ -299,8 +313,12 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(op.txid, anchor);
}
if !status.confirmed {
let _ = graph.insert_seen_at(op.txid, time);
}
}

// get spending status of this outpoint
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _).await? {
if let Some(txid) = op_status.txid {
if graph.get_tx(txid).is_none() {
Expand All @@ -311,6 +329,9 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(txid, anchor);
}
if !status.confirmed {
let _ = graph.insert_seen_at(txid, time);
}
}
}
}
Expand Down
23 changes: 22 additions & 1 deletion crates/esplora/src/blocking_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ pub trait EsploraExt {
///
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
/// parallel. `time` is the current time, typically a UNIX timestamp, used only when setting
/// the time a transaction was last seen unconfirmed.
fn full_scan<K: Ord + Clone>(
&self,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
stop_gap: usize,
parallel_requests: usize,
time: u64,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;

/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
Expand All @@ -64,6 +66,7 @@ pub trait EsploraExt {
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
/// want to include in the update
/// * `time`: UNIX timestamp used to set the time a transaction was last seen unconfirmed
///
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
/// may include scripts that have been used, use [`full_scan`] with the keychain.
Expand All @@ -75,6 +78,7 @@ pub trait EsploraExt {
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
parallel_requests: usize,
time: u64,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
}

Expand Down Expand Up @@ -144,6 +148,7 @@ impl EsploraExt for esplora_client::BlockingClient {
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
stop_gap: usize,
parallel_requests: usize,
time: u64,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
let parallel_requests = Ord::max(parallel_requests, 1);
Expand Down Expand Up @@ -194,6 +199,9 @@ impl EsploraExt for esplora_client::BlockingClient {
if let Some(anchor) = anchor_from_status(&tx.status) {
let _ = graph.insert_anchor(tx.txid, anchor);
}
if !tx.status.confirmed {
let _ = graph.insert_seen_at(tx.txid, time);
}

let previous_outputs = tx.vin.iter().filter_map(|vin| {
let prevout = vin.prevout.as_ref()?;
Expand Down Expand Up @@ -240,6 +248,7 @@ impl EsploraExt for esplora_client::BlockingClient {
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
parallel_requests: usize,
time: u64,
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
let mut graph = self
.full_scan(
Expand All @@ -253,6 +262,7 @@ impl EsploraExt for esplora_client::BlockingClient {
.into(),
usize::MAX,
parallel_requests,
time,
)
.map(|(g, _)| g)?;

Expand Down Expand Up @@ -284,10 +294,14 @@ impl EsploraExt for esplora_client::BlockingClient {
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(txid, anchor);
}
if !status.confirmed {
let _ = graph.insert_seen_at(txid, time);
}
}
}

for op in outpoints {
// get tx for this outpoint
if graph.get_tx(op.txid).is_none() {
if let Some(tx) = self.get_tx(&op.txid)? {
let _ = graph.insert_tx(tx);
Expand All @@ -296,8 +310,12 @@ impl EsploraExt for esplora_client::BlockingClient {
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(op.txid, anchor);
}
if !status.confirmed {
let _ = graph.insert_seen_at(op.txid, time);
}
}

// get spending status of this outpoint
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _)? {
if let Some(txid) = op_status.txid {
if graph.get_tx(txid).is_none() {
Expand All @@ -308,6 +326,9 @@ impl EsploraExt for esplora_client::BlockingClient {
if let Some(anchor) = anchor_from_status(&status) {
let _ = graph.insert_anchor(txid, anchor);
}
if !status.confirmed {
let _ = graph.insert_seen_at(txid, time);
}
}
}
}
Expand Down
9 changes: 5 additions & 4 deletions crates/esplora/tests/async_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
vec![].into_iter(),
vec![].into_iter(),
1,
0,
)
.await?;

Expand Down Expand Up @@ -188,10 +189,10 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {

// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1, 0).await?;
assert!(graph_update.full_txs().next().is_none());
assert!(active_indices.is_empty());
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1, 0).await?;
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
assert_eq!(active_indices[&0], 3);

Expand All @@ -213,12 +214,12 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {

// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
// The last active indice won't be updated in the first case but will in the second one.
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1, 0).await?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(active_indices[&0], 3);
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1, 0).await?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
Expand Down
9 changes: 5 additions & 4 deletions crates/esplora/tests/blocking_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
vec![].into_iter(),
vec![].into_iter(),
1,
0,
)?;

// Check to see if we have the floating txouts available from our two created transactions'
Expand Down Expand Up @@ -216,10 +217,10 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {

// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1, 0)?;
assert!(graph_update.full_txs().next().is_none());
assert!(active_indices.is_empty());
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1, 0)?;
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
assert_eq!(active_indices[&0], 3);

Expand All @@ -241,12 +242,12 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {

// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
// The last active indice won't be updated in the first case but will in the second one.
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1, 0)?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(active_indices[&0], 3);
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1, 0)?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
Expand Down
16 changes: 14 additions & 2 deletions example-crates/example_esplora/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{
collections::{BTreeMap, BTreeSet},
io::{self, Write},
sync::Mutex,
time,
};

use bdk_chain::{
Expand Down Expand Up @@ -189,8 +190,16 @@ 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 now = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)?
.as_secs();
let (graph_update, last_active_indices) = client
.full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
.full_scan(
keychain_spks,
*stop_gap,
scan_options.parallel_requests,
now,
)
.context("scanning for transactions")?;

let mut graph = graph.lock().expect("mutex must not be poisoned");
Expand Down Expand Up @@ -307,8 +316,11 @@ fn main() -> anyhow::Result<()> {
}
}

let now = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)?
.as_secs();
let graph_update =
client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
client.sync(spks, txids, outpoints, scan_options.parallel_requests, now)?;

graph.lock().unwrap().apply_update(graph_update)
}
Expand Down
7 changes: 5 additions & 2 deletions example-crates/wallet_esplora_async/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{io::Write, str::FromStr};
use std::{io::Write, str::FromStr, time};

use bdk::{
bitcoin::{Address, Network},
Expand Down Expand Up @@ -53,8 +53,11 @@ async fn main() -> Result<(), anyhow::Error> {
(k, k_spks)
})
.collect();
let now = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)?
.as_secs();
let (update_graph, last_active_indices) = client
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS, now)
.await?;
let missing_heights = update_graph.missing_heights(wallet.local_chain());
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
Expand Down
7 changes: 5 additions & 2 deletions example-crates/wallet_esplora_blocking/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const SEND_AMOUNT: u64 = 1000;
const STOP_GAP: usize = 5;
const PARALLEL_REQUESTS: usize = 1;

use std::{io::Write, str::FromStr};
use std::{io::Write, str::FromStr, time};

use bdk::{
bitcoin::{Address, Network},
Expand Down Expand Up @@ -53,8 +53,11 @@ fn main() -> Result<(), anyhow::Error> {
})
.collect();

let now = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)?
.as_secs();
let (update_graph, last_active_indices) =
client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)?;
client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS, 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 {
Expand Down

0 comments on commit 3162957

Please sign in to comment.