diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..125986b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,12 @@ +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", "target-feature=+crt-static", + "-C", "link-arg=--import-memory", + "-C", "link-arg=--initial-memory=2097152", + "-C", "link-arg=--max-memory=2097152", + "-C", "link-arg=--stack-first", + "-C", "link-arg=--export-table", +] + +# [build] +# target = "wasm32-unknown-unknown" # Commented out to allow workspace to build for native \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d8e963a..90f6375 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,36 @@ -[package] -name = "sp_client" -version = "0.1.0" -edition = "2021" - -[lib] -name = "sp_client" -crate-type = ["lib", "staticlib", "cdylib"] +[workspace] +members = [ + "sp-client", + "backend-blindbit-native", + "backend-blindbit-wasm", +] +resolver = "2" -[dependencies] +[workspace.dependencies] +# Core dependencies - shared across crates silentpayments = "0.4" anyhow = "1.0" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" bitcoin = { version = "0.31.1", features = ["serde", "rand", "base64"] } -rayon = "1.10.0" +bip39 = { version = "2.2.0" } futures = "0.3" log = "0.4" async-trait = "0.1" -reqwest = { version = "0.12.4", features = ["rustls-tls", "gzip", "json"], default-features = false, optional = true } -hex = { version = "0.4.3", features = ["serde"], optional = true } +hex = { version = "0.4.3", features = ["serde"] } bdk_coin_select = "0.4.0" -[features] -blindbit-backend = ["reqwest", "hex"] +# Performance dependencies (optional) +rayon = "1.10.0" + +# Native backend dependencies +reqwest = { version = "0.12.4", features = ["rustls-tls", "gzip", "json"], default-features = false } + +# WASM dependencies (for future use) +wasm-bindgen = "0.2" + +[workspace.package] +name = "sp-client" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/cygnet3/sp-client" diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..ccb171a --- /dev/null +++ b/TESTING.md @@ -0,0 +1,90 @@ +# Compilation Testing Guide + +This workspace is designed to support multiple architectures with clean separation. Here's how to test all compilation scenarios: + +## Quick Test All Scenarios + +```bash +./test-compilation.sh +``` + +## Manual Testing Commands + +### Core Client Tests +```bash +# Default compilation (native) +cargo check -p sp-client + +# With parallel processing feature +cargo check -p sp-client --features parallel + +# No features +cargo check -p sp-client --no-default-features + +# WASM compilation (should work) +cargo check -p sp-client --target wasm32-unknown-unknown +``` + +### Backend Tests +```bash +# Native compilation (should work) +cargo check -p backend-blindbit-native + +# WASM compilation (should FAIL) +cargo check -p backend-blindbit-native --target wasm32-unknown-unknown +``` + +### Workspace Tests +```bash +# All features +cargo check --workspace --all-features + +# No features +cargo check --workspace --no-default-features + +# Default +cargo check --workspace +``` + +## Expected Results + +| Command | Target | Result | +|---------|--------|--------| +| `sp-client` | native | ✅ Pass | +| `sp-client` | wasm32 | ✅ Pass | +| `backend-blindbit-native` | native | ✅ Pass | +| `backend-blindbit-native` | wasm32 | ❌ Fail (expected) | +| `workspace` | native | ✅ Pass | + +## Architecture Goals + +- **Core client (`sp-client`)**: Architecture-agnostic, minimal dependencies +- **Native backend (`backend-blindbit-native`)**: Native-only, full functionality +- **Future WASM backend (`backend-blindbit-wasm`)**: WASM-specific implementation +- **Future backends**: `backend-electrum-native`, `backend-esplora-native`, etc. + +## Feature Flags + +- `parallel`: Enable parallel processing in core client (native performance optimization) +- No backend-specific feature flags needed - use separate crates instead + +## CI/CD Integration + +Add these commands to your CI pipeline: + +```yaml +# Test all compilation scenarios +- name: Test compilation scenarios + run: ./test-compilation.sh + +# Test specific targets +- name: Test WASM + run: cargo check -p sp-client --target wasm32-unknown-unknown + +- name: Test native backend fails on WASM + run: | + if cargo check -p backend-blindbit-native --target wasm32-unknown-unknown; then + echo "ERROR: Backend should not compile for WASM" + exit 1 + fi +``` diff --git a/backend-blindbit-native/Cargo.toml b/backend-blindbit-native/Cargo.toml new file mode 100644 index 0000000..8744750 --- /dev/null +++ b/backend-blindbit-native/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "backend-blindbit-native" +version.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +# Core client dependency +sp-client = { path = "../sp-client" } + +# Async and futures +futures.workspace = true +async-trait.workspace = true +log.workspace = true + +# Native-specific dependencies +rayon.workspace = true +reqwest.workspace = true + +# Re-export some core types for convenience +bitcoin.workspace = true +anyhow.workspace = true +silentpayments.workspace = true +serde.workspace = true +serde_json.workspace = true +hex.workspace = true + +[features] +default = [] diff --git a/src/backend/backend.rs b/backend-blindbit-native/src/backend/backend.rs similarity index 91% rename from src/backend/backend.rs rename to backend-blindbit-native/src/backend/backend.rs index d1892f0..a33ebe6 100644 --- a/src/backend/backend.rs +++ b/backend-blindbit-native/src/backend/backend.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use bitcoin::{absolute::Height, Amount}; use futures::Stream; -use super::structs::{BlockData, SpentIndexData, UtxoData}; +use sp_client::{BlockData, SpentIndexData, UtxoData}; #[async_trait] pub trait ChainBackend { diff --git a/src/backend/blindbit/backend/backend.rs b/backend-blindbit-native/src/backend/blindbit/backend/backend.rs similarity index 100% rename from src/backend/blindbit/backend/backend.rs rename to backend-blindbit-native/src/backend/blindbit/backend/backend.rs diff --git a/src/backend/blindbit/backend/mod.rs b/backend-blindbit-native/src/backend/blindbit/backend/mod.rs similarity index 100% rename from src/backend/blindbit/backend/mod.rs rename to backend-blindbit-native/src/backend/blindbit/backend/mod.rs diff --git a/src/backend/blindbit/client/client.rs b/backend-blindbit-native/src/backend/blindbit/client/client.rs similarity index 100% rename from src/backend/blindbit/client/client.rs rename to backend-blindbit-native/src/backend/blindbit/client/client.rs diff --git a/src/backend/blindbit/client/mod.rs b/backend-blindbit-native/src/backend/blindbit/client/mod.rs similarity index 100% rename from src/backend/blindbit/client/mod.rs rename to backend-blindbit-native/src/backend/blindbit/client/mod.rs diff --git a/src/backend/blindbit/client/structs.rs b/backend-blindbit-native/src/backend/blindbit/client/structs.rs similarity index 100% rename from src/backend/blindbit/client/structs.rs rename to backend-blindbit-native/src/backend/blindbit/client/structs.rs diff --git a/src/backend/blindbit/mod.rs b/backend-blindbit-native/src/backend/blindbit/mod.rs similarity index 100% rename from src/backend/blindbit/mod.rs rename to backend-blindbit-native/src/backend/blindbit/mod.rs diff --git a/backend-blindbit-native/src/backend/mod.rs b/backend-blindbit-native/src/backend/mod.rs new file mode 100644 index 0000000..0e54f10 --- /dev/null +++ b/backend-blindbit-native/src/backend/mod.rs @@ -0,0 +1,6 @@ +mod backend; +mod blindbit; + +pub use backend::ChainBackend; +pub use blindbit::BlindbitBackend; +pub use blindbit::BlindbitClient; diff --git a/backend-blindbit-native/src/lib.rs b/backend-blindbit-native/src/lib.rs new file mode 100644 index 0000000..98a8fc0 --- /dev/null +++ b/backend-blindbit-native/src/lib.rs @@ -0,0 +1,10 @@ +#![allow(clippy::module_inception)] +mod backend; +mod scanner; + +// Re-export backend functionality +pub use backend::{BlindbitBackend, BlindbitClient, ChainBackend}; +pub use scanner::SpScanner; + +// Re-export core client for convenience (includes Updater) +pub use sp_client::*; diff --git a/backend-blindbit-native/src/scanner/mod.rs b/backend-blindbit-native/src/scanner/mod.rs new file mode 100644 index 0000000..9716f73 --- /dev/null +++ b/backend-blindbit-native/src/scanner/mod.rs @@ -0,0 +1,325 @@ +use std::collections::{HashMap, HashSet}; + +use anyhow::{Error, Result}; +use bitcoin::{ + absolute::Height, bip158::BlockFilter, Amount, BlockHash, OutPoint, Txid, XOnlyPublicKey, +}; +use futures::Stream; +use silentpayments::receiving::Label; + +// Updater now comes from sp-client + +use sp_client::{BlockData, FilterData, OwnedOutput, SpClient, Updater, UtxoData}; + +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. +#[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