Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a03d4ff
Migrating to workspace structure
Sosthene00 Nov 20, 2025
a113656
Make building with both `async` and `sync` features a compilation error
Sosthene00 Nov 26, 2025
bbc08a5
Remove misleading commentary on get_input_hashes
Sosthene00 Nov 26, 2025
c439abe
fixup: remove parallelization at block level
Sosthene00 Nov 26, 2025
4c60722
blindbit-native: sync needs futures & async-trait
pythcoiner Nov 27, 2025
e42d24e
clippyfy
pythcoiner Nov 27, 2025
6ccf573
Migrating to workspace structure
Sosthene00 Nov 20, 2025
8f18e33
spdk-core: cleanup
pythcoiner Nov 26, 2025
5e15b08
add blindbit-native-non-async backend
pythcoiner Nov 26, 2025
e304e4a
workspace: allow not using spdk-core default featuresfor workspace de…
pythcoiner Nov 26, 2025
e125c98
blindbit-native-non-async: keep low dependencies
pythcoiner Nov 26, 2025
ae3ed5e
blindbit-native-non-async: drop url dependency
pythcoiner Nov 26, 2025
8bb78fc
spdk-core: impl DummyUpdater
pythcoiner Nov 27, 2025
d82309e
spdk-core: implem (sync) account module
pythcoiner Nov 27, 2025
ae61a2d
spdk-core: impl From<SecretKey> for SpendKey
pythcoiner Nov 28, 2025
6326579
spdk-core: allow SpClient::default() only for test
pythcoiner Nov 28, 2025
e89c62f
spdk-core: add SpCLient constructors from mnemonics
pythcoiner Nov 28, 2025
abbc8af
spdk-core: compile check on both async + sync feature ws a brain fart
pythcoiner Nov 28, 2025
d4ab1ad
spdk-core: allow to use both bitcoin versions 0.31.x & 0.32.x using f…
pythcoiner Nov 28, 2025
63d19fa
spdk-core: add .infos() & .network() to ChainBackend trait
pythcoiner Nov 29, 2025
b6c51e1
blindbit-native: silently set minimal value for range.start to 1
pythcoiner Nov 29, 2025
52fb89f
spdk-core: make dust_limit optional
pythcoiner Nov 30, 2025
9328d5b
blindbit-native-non-async: use proper error types, remove anyhow
pythcoiner Jan 31, 2026
9c8f8f2
blindbit-native-non-async: fix ureq gzip headers workaround
pythcoiner Jan 31, 2026
afcdc49
blindbit-native-non-async: strip trailing slash in URL join
pythcoiner Jan 31, 2026
fbf87f3
blindbit-native-non-async: expose info() and re-export InfoResponse
pythcoiner Jan 31, 2026
35746ca
blindbit-native-non-async: fix ThreadPool deadlock
pythcoiner Jan 31, 2026
072cc66
blindbit-native: add integration tests
pythcoiner Nov 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
[package]
name = "spdk"
version = "0.1.0"
edition = "2021"
[workspace]
members = [
"spdk-core",
"backend-blindbit-native",
"backend-blindbit-native-non-async",
"backend_tests/blindbit-native",
]
resolver = "3"

[lib]
name = "spdk"
crate-type = ["lib", "staticlib", "cdylib"]
[workspace.dependencies]
# Internal workspace crates
# NOTE: if default-features == true, it cannot be disabled by crates using spdk-core.workspace = true
spdk-core = { path = "spdk-core", version = "0.1.0" , default-features = false}
bitcoin = { path = "bitcoin" }

[dependencies]
silentpayments = "0.4"
anyhow = "1.0"
# Core dependencies - shared across crates
# silentpayments = "0.4"
silentpayments = { git = "https://github.com/pythcoiner/rust-silentpayments.git", branch = "secp_29"}
thiserror = "2"
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"
futures = "0.3"
log = "0.4"
futures-util = "0.3.31"
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 }
log = "0.4"
hex = { version = "0.4.3", features = ["serde"] }
bdk_coin_select = "0.4.0"
bip39 = { version = "2.2.0" }

[features]
blindbit-backend = ["reqwest", "hex"]
[workspace.package]
repository = "https://github.com/cygnet3/spdk"
42 changes: 42 additions & 0 deletions backend-blindbit-native-non-async/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "backend-blindbit-native-non-async"
version = "0.1.0"
edition = "2021"
repository.workspace = true

[dependencies]
# Core client dependency
spdk-core.workspace = true
spdk-core.default-features = false
spdk-core.features = ["sync"]

# TLS support via rustls is required for HTTPS connections to blindbit
ureq = { git = "https://github.com/pythcoiner/ureq.git", branch = "gzip", default-features = false, features = [ "gzip"]}

# Logging
log.workspace = true

# Re-export some core types for convenience
bitcoin.workspace = true
thiserror.workspace = true
silentpayments.workspace = true
serde.workspace = true
serde_json.workspace = true
hex.workspace = true

# Parallelization
rayon.workspace = true
rayon.optional = true

[dev-dependencies]
blindbitd = { git = "https://github.com/pythcoiner/blindbitd.git" }
reqwest = { version = "0.12", features = ["blocking", "gzip"] }
ureq2 = { package = "ureq", version = "2.12", features = ["gzip"] }
flate2 = "1"
corepc-node = { version = "0.10.0", features = ["download", "29_0"] }
rand = "0.8"

[features]
default = []
rayon = ["dep:rayon", "spdk-core/parallel"]

213 changes: 213 additions & 0 deletions backend-blindbit-native-non-async/src/backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use std::{
ops::RangeInclusive,
sync::{mpsc, Arc},
};

use bitcoin::{absolute::Height, Amount};

#[cfg(feature = "rayon")]
use rayon::{
iter::{IntoParallelIterator, ParallelIterator},
ThreadPoolBuilder,
};

use crate::client::{BlindbitClient, HttpClient};
use spdk_core::{BlockData, ChainBackend, SpentIndexData, UtxoData};

const CONCURRENT_FILTER_REQUESTS: usize = 200;

pub struct BlindbitBackend<H: HttpClient> {
client: BlindbitClient<H>,
}

impl<H: HttpClient + Clone + 'static> BlindbitBackend<H> {
/// Create a new async Blindbit backend with a custom HTTP client.
///
/// # Arguments
/// * `blindbit_url` - Base URL of the Blindbit server
/// * `http_client` - HTTP client implementation
pub fn new(blindbit_url: String, http_client: H) -> crate::error::Result<Self> {
Ok(Self {
client: BlindbitClient::new(blindbit_url, http_client)?,
})
}

/// Get block data for a range of blocks as an Iterator
///
/// This fetches blocks concurrently for better performance.
///
/// # Arguments
/// * `range` - Range of block heights to fetch
/// * `dust_limit` - Minimum amount to consider (dust outputs are ignored)
/// * `with_cutthrough` - Whether to use cutthrough optimization
///
/// # Returns
/// A Iterator of BlockData results
pub fn get_block_data_for_range(
&self,
range: RangeInclusive<u32>,
dust_limit: Option<Amount>,
with_cutthrough: bool,
) -> spdk_core::BlockDataIterator {
#[cfg(feature = "rayon")]
let iter = self.get_block_data_for_range_rayon(range, dust_limit, with_cutthrough);

#[cfg(not(feature = "rayon"))]
let iter = self.get_block_data_for_range_thread_pool(range, dust_limit, with_cutthrough);

iter
}

#[cfg(not(feature = "rayon"))]
pub fn get_block_data_for_range_thread_pool(
&self,
range: RangeInclusive<u32>,
dust_limit: Option<Amount>,
with_cutthrough: bool,
) -> spdk_core::BlockDataIterator {
use crate::thread_pool::ThreadPool;

let client = Arc::new(self.client.clone());

let (sender, receiver) = mpsc::channel();

let pool = ThreadPool::new(CONCURRENT_FILTER_REQUESTS);

for height in range {
let client = client.clone();
let sender = sender.clone();

pool.execute(move || {
get_block_data_for_height(height, dust_limit, with_cutthrough, sender, client);
});
}
Box::new(receiver.into_iter())
}
#[cfg(feature = "rayon")]
pub fn get_block_data_for_range_rayon(
&self,
range: RangeInclusive<u32>,
dust_limit: Option<Amount>,
with_cutthrough: bool,
) -> spdk_core::BlockDataIterator {
let client = Arc::new(self.client.clone());

let (sender, receiver) = mpsc::channel();

let pool = ThreadPoolBuilder::new()
.num_threads(CONCURRENT_FILTER_REQUESTS)
.build()
.unwrap();

pool.install(|| {
range.into_par_iter().for_each(move |height| {
let client = client.clone();
let sender = sender.clone();

get_block_data_for_height(height, dust_limit, with_cutthrough, sender, client);
})
});
Box::new(receiver.into_iter())
}

/// Get spent index data for a block height
pub fn spent_index(&self, block_height: Height) -> crate::error::Result<SpentIndexData> {
Ok(self.client.spent_index(block_height)?.into())
}

/// Get UTXO data for a block height
pub fn utxos(&self, block_height: Height) -> crate::error::Result<Vec<UtxoData>> {
Ok(self
.client
.utxos(block_height)?
.into_iter()
.map(Into::into)
.collect())
}

/// Get the current block height from the server
pub fn block_height(&self) -> crate::error::Result<Height> {
self.client.block_height()
}

/// Get server info (network, supported modes, etc.)
pub fn info(&self) -> crate::error::Result<crate::InfoResponse> {
Ok(self.client.info()?)
}
}

fn get_block_data_for_height<H>(
height: u32,
dust_limit: Option<Amount>,
with_cutthrough: bool,
sender: mpsc::Sender<spdk_core::error::Result<BlockData>>,
client: Arc<BlindbitClient<H>>,
) where
H: HttpClient,
{
let blkheight = match Height::from_consensus(height) {
Ok(bh) => bh,
Err(e) => {
sender.send(Err(spdk_core::Error::from(e))).expect("closed");
return;
}
};
let tweaks = match with_cutthrough {
true => client.tweaks(blkheight, dust_limit),
false => client.tweak_index(blkheight, dust_limit),
};
let tweaks = match tweaks {
Ok(t) => t,
Err(e) => {
sender.send(Err(spdk_core::Error::from(e))).expect("closed");
return;
}
};
let new_utxo_filter = match client.filter_new_utxos(blkheight) {
Ok(f) => f,
Err(e) => {
sender.send(Err(spdk_core::Error::from(e))).expect("closed");
return;
}
};
let spent_filter = match client.filter_spent(blkheight) {
Ok(f) => f,
Err(e) => {
sender.send(Err(spdk_core::Error::from(e))).expect("closed");
return;
}
};
let blkhash = new_utxo_filter.block_hash;
sender
.send(Ok(BlockData {
blkheight,
blkhash,
tweaks,
new_utxo_filter: new_utxo_filter.into(),
spent_filter: spent_filter.into(),
}))
.expect("closed")
}

impl<H: HttpClient + Clone + 'static> ChainBackend for BlindbitBackend<H> {
fn get_block_data_for_range(
&self,
range: RangeInclusive<u32>,
dust_limit: Option<Amount>,
with_cutthrough: bool,
) -> spdk_core::BlockDataIterator {
self.get_block_data_for_range(range, dust_limit, with_cutthrough)
}

fn spent_index(&self, block_height: Height) -> spdk_core::error::Result<SpentIndexData> {
Ok(self.spent_index(block_height)?)
}

fn utxos(&self, block_height: Height) -> spdk_core::error::Result<Vec<UtxoData>> {
Ok(self.utxos(block_height)?)
}

fn block_height(&self) -> spdk_core::error::Result<Height> {
Ok(self.block_height()?)
}
}
Loading
Loading