From a6b7ea941ceb235ff4cd7da2329e6636d77cf079 Mon Sep 17 00:00:00 2001 From: Sonkeng Maldini Date: Thu, 11 Jun 2026 12:59:17 +0100 Subject: [PATCH] feat(blindbit): manage available server features --- backend-blindbit-v1/src/api_structs.rs | 69 ++++++++++++++++++++++++ backend-blindbit-v1/src/backend.rs | 75 ++++++++++++++++---------- 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/backend-blindbit-v1/src/api_structs.rs b/backend-blindbit-v1/src/api_structs.rs index ce1b6c6a..67ff8be4 100644 --- a/backend-blindbit-v1/src/api_structs.rs +++ b/backend-blindbit-v1/src/api_structs.rs @@ -100,3 +100,72 @@ where Network::from_core_arg(&buf).map_err(serde::de::Error::custom) } + +impl InfoResponse { + /// Validates whether the server supports the requested parameters. + /// + /// Returns: + /// - `Ok(use_tweaks_endpoint)` - true means use tweaks(), false means use tweak_index() + /// - `Err(String)` if the request cannot be satisfied + pub fn validate_and_resolve_endpoint( + &self, + dust_limit: Amount, + with_cutthrough: bool, + ) -> Result { + let has_dust_limit = dust_limit > Amount::ZERO; + + match (with_cutthrough, has_dust_limit) { + // Request: cutthrough with dust filtering + (true, true) => { + if self.tweaks_cut_through_with_dust_filter { + Ok(true) + } else if self.tweaks_full_with_dust_filter { + Ok(false) + } else { + Err( + "Server does not support dust filtering; cannot satisfy request with dust_limit" + .to_string(), + ) + } + } + + // Request: cutthrough without dust filtering + (true, false) => { + // Can use tweaks endpoint without dust limit + if self.tweaks_only + || self.tweaks_full_basic + || self.tweaks_cut_through_with_dust_filter + { + Ok(true) + } else { + Err("Server does not support tweaks endpoint".to_string()) + } + } + + // Request: full index with dust filtering + (false, true) => { + if self.tweaks_full_with_dust_filter { + Ok(false) + } else if self.tweaks_cut_through_with_dust_filter { + Ok(true) + } else { + Err( + "Server does not support dust filtering; cannot satisfy request with dust_limit" + .to_string(), + ) + } + } + + // Request: full index without dust filtering + (false, false) => { + if self.tweaks_full_basic { + Ok(false) + } else if self.tweaks_cut_through_with_dust_filter || self.tweaks_only { + Ok(true) + } else { + Err("Server does not support any tweak endpoints".to_string()) + } + } + } + } +} diff --git a/backend-blindbit-v1/src/backend.rs b/backend-blindbit-v1/src/backend.rs index 61a8ea03..8286d725 100644 --- a/backend-blindbit-v1/src/backend.rs +++ b/backend-blindbit-v1/src/backend.rs @@ -3,9 +3,8 @@ use std::{ops::RangeInclusive, pin::Pin}; use anyhow::Result; use async_trait::async_trait; use bitcoin::{Amount, absolute::Height}; -use futures::{Stream, StreamExt, stream}; +use futures::{Stream, StreamExt, future::Either, stream}; -use itertools::Either; use spdk_core::chain::{BlockData, ChainBackend, SpentIndexData, UtxoData}; use crate::BlindbitClient; @@ -37,38 +36,60 @@ impl ChainBackend for BlindbitBackend { with_cutthrough: bool, ) -> Pin> + Send>> { let client = self.client.clone(); + let client_for_info = self.client.clone(); // convert range to u32 since Height does not implement Step let range = range.start().to_consensus_u32()..=range.end().to_consensus_u32(); let range = match reverse { - false => Either::Left(range), - true => Either::Right(range.rev()), + false => itertools::Either::Left(range), + true => itertools::Either::Right(range.rev()), }; - let res = stream::iter(range) - .map(move |n| { - let client = client.clone(); - - async move { - let blkheight = Height::from_consensus(n)?; - let tweaks = match with_cutthrough { - true => client.tweaks(blkheight, dust_limit).await?, - false => client.tweak_index(blkheight, dust_limit).await?, - }; - let new_utxo_filter = client.filter_new_utxos(blkheight).await?; - let spent_filter = client.filter_spent(blkheight).await?; - let blkhash = new_utxo_filter.block_hash; - Ok(BlockData { - blkheight, - blkhash, - tweaks, - new_utxo_filter: new_utxo_filter.into(), - spent_filter: spent_filter.into(), - }) - } - }) - .buffered(CONCURRENT_FILTER_REQUESTS); + let res = stream::once(async move { + client_for_info + .info() + .await? + .validate_and_resolve_endpoint(dust_limit, with_cutthrough) + .map_err(|e| anyhow::anyhow!(e)) + }) + .flat_map(move |use_tweaks| { + let client = client.clone(); + let heights = range.clone(); + + // Resolve the Result once here rather than re-checking it per block. + let use_tweaks: bool = match use_tweaks { + Ok(v) => v, + Err(e) => return Either::Left(stream::once(async move { Err(e) })), + }; + + let inner = stream::iter(heights) + .map(move |n| { + let client = client.clone(); + + async move { + let blkheight = Height::from_consensus(n)?; + let tweaks = match use_tweaks { + true => client.tweaks(blkheight, dust_limit).await?, + false => client.tweak_index(blkheight, dust_limit).await?, + }; + let new_utxo_filter = client.filter_new_utxos(blkheight).await?; + let spent_filter = client.filter_spent(blkheight).await?; + let blkhash = new_utxo_filter.block_hash; + + Ok(BlockData { + blkheight, + blkhash, + tweaks, + new_utxo_filter: new_utxo_filter.into(), + spent_filter: spent_filter.into(), + }) + } + }) + .buffered(CONCURRENT_FILTER_REQUESTS); + + Either::Right(inner) + }); Box::pin(res) }