From b4c68c777ac8b6d66d80c9d6ceeee8024a6ca7bb Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 20 Jun 2025 10:50:47 +0200 Subject: [PATCH 01/18] Register spent transaction with txid or blockid raw bytes not hex string --- src/client/structs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/structs.rs b/src/client/structs.rs index dccc587..d3718cb 100644 --- a/src/client/structs.rs +++ b/src/client/structs.rs @@ -12,8 +12,8 @@ use bitcoin::{ use serde::{Deserialize, Serialize}; use silentpayments::{receiving::Label, SilentPaymentAddress}; -type SpendingTxId = String; -type MinedInBlock = String; +type SpendingTxId = [u8; 32]; +type MinedInBlock = [u8; 32]; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum OutputSpendStatus { From 1c4a7e294882756864b54b408a615f9f6c6ae195 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 21 Jun 2025 17:11:10 +0200 Subject: [PATCH 02/18] Reexport futures --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index c055981..2e27d2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ mod updater; pub use bdk_coin_select::FeeRate; pub use bitcoin; pub use silentpayments; +pub use futures; pub use backend::*; pub use client::*; From dcf97ac2aabe1e8d9078888040c22f0a7f72aee2 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 18 Aug 2025 11:36:19 +0200 Subject: [PATCH 03/18] Abstract SpScanner into a trait --- src/scanner/mod.rs | 336 +++++++++++++++++++++++++++++++++++- src/scanner/scanner.rs | 380 ----------------------------------------- 2 files changed, 334 insertions(+), 382 deletions(-) delete mode 100644 src/scanner/scanner.rs diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 2d6bea2..2ddc51d 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -1,3 +1,335 @@ -mod scanner; +use std::collections::{HashMap, HashSet}; -pub use scanner::SpScanner; +use anyhow::{Error, Result}; +use bitcoin::{ + absolute::Height, bip158::BlockFilter, hashes::{sha256, Hash}, + Amount, BlockHash, OutPoint, Txid, XOnlyPublicKey +}; +use futures::Stream; +use silentpayments::receiving::Label; + +use crate::{ + backend::{BlockData, FilterData, UtxoData}, + client::{OwnedOutput, SpClient}, + updater::Updater, +}; + +use crate::backend::ChainBackend; + +/// Trait for scanning silent payment blocks +/// +/// This trait abstracts the core scanning functionality, allowing consumers +/// to implement it with their own constraints and requirements. +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +pub trait SpScanner { + /// Scan a range of blocks for silent payment outputs and inputs + /// + /// # Arguments + /// * `start` - Starting block height (inclusive) + /// * `end` - Ending block height (inclusive) + /// * `dust_limit` - Minimum amount to consider (dust outputs are ignored) + /// * `with_cutthrough` - Whether to use cutthrough optimization + async fn scan_blocks( + &mut self, + start: Height, + end: Height, + dust_limit: Amount, + with_cutthrough: bool, + ) -> Result<()>; + + /// Process a single block's data + /// + /// # Arguments + /// * `blockdata` - Block data containing tweaks and filters + /// + /// # Returns + /// * `(found_outputs, found_inputs)` - Tuple of found outputs and spent inputs + async fn process_block( + &mut self, + blockdata: BlockData, + ) -> Result<(HashMap, HashSet)>; + + /// Process block outputs to find owned silent payment outputs + /// + /// # Arguments + /// * `blkheight` - Block height + /// * `tweaks` - List of tweak public keys + /// * `new_utxo_filter` - Filter data for new UTXOs + /// + /// # Returns + /// * Map of outpoints to owned outputs + async fn process_block_outputs( + &self, + blkheight: Height, + tweaks: Vec, + new_utxo_filter: FilterData, + ) -> Result>; + + /// Process block inputs to find spent outputs + /// + /// # Arguments + /// * `blkheight` - Block height + /// * `spent_filter` - Filter data for spent outputs + /// + /// # Returns + /// * Set of spent outpoints + async fn process_block_inputs( + &self, + blkheight: Height, + spent_filter: FilterData, + ) -> Result>; + + /// Get the block data stream for a range of blocks + /// + /// # Arguments + /// * `range` - Range of block heights + /// * `dust_limit` - Minimum amount to consider + /// * `with_cutthrough` - Whether to use cutthrough optimization + /// + /// # Returns + /// * Stream of block data results + fn get_block_data_stream( + &self, + range: std::ops::RangeInclusive, + dust_limit: Amount, + with_cutthrough: bool, + ) -> std::pin::Pin> + Send>>; + + /// Check if scanning should be interrupted + /// + /// # Returns + /// * `true` if scanning should stop, `false` otherwise + fn should_interrupt(&self) -> bool; + + /// Save current state to persistent storage + fn save_state(&mut self) -> Result<()>; + + /// Record found outputs for a block + /// + /// # Arguments + /// * `height` - Block height + /// * `block_hash` - Block hash + /// * `outputs` - Found outputs + fn record_outputs( + &mut self, + height: Height, + block_hash: BlockHash, + outputs: HashMap, + ) -> Result<()>; + + /// Record spent inputs for a block + /// + /// # Arguments + /// * `height` - Block height + /// * `block_hash` - Block hash + /// * `inputs` - Spent inputs + fn record_inputs( + &mut self, + height: Height, + block_hash: BlockHash, + inputs: HashSet, + ) -> Result<()>; + + /// Record scan progress + /// + /// # Arguments + /// * `start` - Start height + /// * `current` - Current height + /// * `end` - End height + fn record_progress(&mut self, start: Height, current: Height, end: Height) -> Result<()>; + + /// Get the silent payment client + fn client(&self) -> &SpClient; + + /// Get the chain backend + fn backend(&self) -> &dyn ChainBackend; + + /// Get the updater + fn updater(&mut self) -> &mut dyn Updater; + + // Helper methods with default implementations + + /// Process multiple blocks from a stream + /// + /// This is a default implementation that can be overridden if needed + async fn process_blocks( + &mut self, + start: Height, + end: Height, + block_data_stream: impl Stream> + Unpin + Send, + ) -> Result<()> { + use futures::StreamExt; + use std::time::{Duration, Instant}; + + let mut update_time = Instant::now(); + let mut stream = block_data_stream; + + while let Some(blockdata) = stream.next().await { + let blockdata = blockdata?; + let blkheight = blockdata.blkheight; + let blkhash = blockdata.blkhash; + + // stop scanning and return if interrupted + if self.should_interrupt() { + self.save_state()?; + return Ok(()); + } + + let mut save_to_storage = false; + + // always save on last block or after 30 seconds since last save + if blkheight == end || update_time.elapsed() > Duration::from_secs(30) { + save_to_storage = true; + } + + let (found_outputs, found_inputs) = self.process_block(blockdata).await?; + + if !found_outputs.is_empty() { + save_to_storage = true; + self.record_outputs(blkheight, blkhash, found_outputs)?; + } + + if !found_inputs.is_empty() { + save_to_storage = true; + self.record_inputs(blkheight, blkhash, found_inputs)?; + } + + // tell the updater we scanned this block + self.record_progress(start, blkheight, end)?; + + if save_to_storage { + self.save_state()?; + update_time = Instant::now(); + } + } + + Ok(()) + } + + /// Scan UTXOs for a given block and secrets map + /// + /// This is a default implementation that can be overridden if needed + async fn scan_utxos( + &self, + blkheight: Height, + secrets_map: HashMap<[u8; 34], bitcoin::secp256k1::PublicKey>, + ) -> Result, UtxoData, bitcoin::secp256k1::Scalar)>> { + let utxos = self.backend().utxos(blkheight).await?; + + let mut res: Vec<(Option