From 28f20e81eaad312bb6df12f02dbdad147a49298b Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 11:31:01 -0400 Subject: [PATCH 01/14] cold: regroup module declarations in lib.rs Cosmetic: one blank-line-separated group per module + its re-export. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/lib.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/cold/src/lib.rs b/crates/cold/src/lib.rs index 7a57891..14221cc 100644 --- a/crates/cold/src/lib.rs +++ b/crates/cold/src/lib.rs @@ -135,28 +135,35 @@ #![cfg_attr(docsrs, feature(doc_cfg))] mod cache; + +mod cold_receipt; +pub use cold_receipt::ColdReceipt; + +pub mod connect; +pub use connect::ColdConnect; + mod error; -mod metrics; pub use error::{ColdResult, ColdStorageError}; + mod handle; pub use handle::ColdStorage; + +mod metrics; + mod specifier; -pub use alloy::rpc::types::{Filter, Log as RpcLog}; -pub use signet_storage_types::{Confirmed, Recovered}; -pub use specifier::{ - HeaderSpecifier, ReceiptSpecifier, SignetEventsSpecifier, TransactionSpecifier, - ZenithHeaderSpecifier, -}; -mod cold_receipt; -pub use cold_receipt::ColdReceipt; mod stream; pub use stream::{StreamParams, produce_log_stream_default}; + mod traits; pub use traits::{BlockData, ColdStorageBackend, ColdStorageRead, ColdStorageWrite, LogStream}; -pub mod connect; -pub use connect::ColdConnect; +pub use alloy::rpc::types::{Filter, Log as RpcLog}; +pub use signet_storage_types::{Confirmed, Recovered}; +pub use specifier::{ + HeaderSpecifier, ReceiptSpecifier, SignetEventsSpecifier, TransactionSpecifier, + ZenithHeaderSpecifier, +}; /// Conformance tests for cold storage backends. #[cfg(any(test, feature = "test-utils"))] From ecbd93c7f0536225fd8015f964f63dd2b33730ee Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 11:33:34 -0400 Subject: [PATCH 02/14] cold: declare DynColdStorageBackend trait skeleton Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/dyn_backend.rs | 156 +++++++++++++++++++++++++++++++++ crates/cold/src/lib.rs | 3 + 2 files changed, 159 insertions(+) create mode 100644 crates/cold/src/dyn_backend.rs diff --git a/crates/cold/src/dyn_backend.rs b/crates/cold/src/dyn_backend.rs new file mode 100644 index 0000000..5dcddba --- /dev/null +++ b/crates/cold/src/dyn_backend.rs @@ -0,0 +1,156 @@ +//! Object-safe mirror of [`ColdStorageBackend`]. +//! +//! [`DynColdStorageBackend`] re-declares every method on +//! [`ColdStorageRead`], [`ColdStorageWrite`], and [`ColdStorageBackend`] +//! with an explicit `Pin>` return type so +//! the trait is object-safe. A blanket impl auto-implements it for +//! every `B: ColdStorageBackend`, and `Arc` +//! re-implements the strong traits by delegating to the boxed methods. +//! +//! # Plumbing, Not API +//! +//! This trait exists so [`ColdStorage`]'s default type parameter +//! (`Arc`) is nameable in error messages +//! and downstream signatures. Backends should implement +//! [`ColdStorageBackend`] — the blanket impl handles this trait. +//! +//! [`ColdStorage`]: crate::ColdStorage +//! [`ColdStorageBackend`]: crate::ColdStorageBackend +//! [`ColdStorageRead`]: crate::ColdStorageRead +//! [`ColdStorageWrite`]: crate::ColdStorageWrite + +use crate::{ + BlockData, ColdReceipt, ColdResult, Confirmed, Filter, HeaderSpecifier, ReceiptSpecifier, + RpcLog, SignetEventsSpecifier, StreamParams, TransactionSpecifier, ZenithHeaderSpecifier, +}; +use alloy::primitives::BlockNumber; +use signet_storage_types::{DbSignetEvent, DbZenithHeader, RecoveredTx, SealedHeader}; +use std::{future::Future, pin::Pin, sync::Arc, time::Duration}; + +/// Object-safe mirror of [`ColdStorageBackend`]. Auto-implemented by a +/// blanket impl over every `B: ColdStorageBackend`; do not implement +/// directly. +/// +/// [`ColdStorageBackend`]: crate::ColdStorageBackend +#[allow(clippy::type_complexity)] +pub trait DynColdStorageBackend: Send + Sync + 'static { + /// Get a header by specifier. + fn dyn_get_header<'a>( + &'a self, + spec: HeaderSpecifier, + ) -> Pin>> + Send + 'a>>; + + /// Get multiple headers by specifiers. + fn dyn_get_headers<'a>( + &'a self, + specs: Vec, + ) -> Pin>>> + Send + 'a>>; + + /// Get a transaction by specifier, with block confirmation metadata. + fn dyn_get_transaction<'a>( + &'a self, + spec: TransactionSpecifier, + ) -> Pin>>> + Send + 'a>>; + + /// Get all transactions in a block. + fn dyn_get_transactions_in_block<'a>( + &'a self, + block: BlockNumber, + ) -> Pin>> + Send + 'a>>; + + /// Get the number of transactions in a block. + fn dyn_get_transaction_count<'a>( + &'a self, + block: BlockNumber, + ) -> Pin> + Send + 'a>>; + + /// Get a receipt by specifier. + fn dyn_get_receipt<'a>( + &'a self, + spec: ReceiptSpecifier, + ) -> Pin>> + Send + 'a>>; + + /// Get all receipts in a block. + fn dyn_get_receipts_in_block<'a>( + &'a self, + block: BlockNumber, + ) -> Pin>> + Send + 'a>>; + + /// Get signet events by specifier. + fn dyn_get_signet_events<'a>( + &'a self, + spec: SignetEventsSpecifier, + ) -> Pin>> + Send + 'a>>; + + /// Get a zenith header by specifier. + fn dyn_get_zenith_header<'a>( + &'a self, + spec: ZenithHeaderSpecifier, + ) -> Pin>> + Send + 'a>>; + + /// Get multiple zenith headers by specifier. + fn dyn_get_zenith_headers<'a>( + &'a self, + spec: ZenithHeaderSpecifier, + ) -> Pin>> + Send + 'a>>; + + /// Get the latest block number in storage. + fn dyn_get_latest_block<'a>( + &'a self, + ) -> Pin>> + Send + 'a>>; + + /// Filter logs by block range, address, and topics. + fn dyn_get_logs<'a>( + &'a self, + filter: &'a Filter, + max_logs: usize, + ) -> Pin>> + Send + 'a>>; + + /// Produce a log stream by iterating blocks and sending matching logs. + fn dyn_produce_log_stream<'a>( + &'a self, + filter: &'a Filter, + params: StreamParams, + ) -> Pin + Send + 'a>>; + + /// Append a single block to cold storage. + fn dyn_append_block<'a>( + &'a self, + data: BlockData, + ) -> Pin> + Send + 'a>>; + + /// Append multiple blocks to cold storage. + fn dyn_append_blocks<'a>( + &'a self, + data: Vec, + ) -> Pin> + Send + 'a>>; + + /// Truncate all data above the given block number (exclusive). + fn dyn_truncate_above<'a>( + &'a self, + block: BlockNumber, + ) -> Pin> + Send + 'a>>; + + /// Read and remove all blocks above the given block number. + fn dyn_drain_above<'a>( + &'a self, + block: BlockNumber, + ) -> Pin>>> + Send + 'a>>; + + /// Configured read deadline, if any. + fn dyn_read_timeout(&self) -> Option; + + /// Configured write deadline, if any. + fn dyn_write_timeout(&self) -> Option; +} + +// Sanity check: ensure the trait is object-safe. The line below fails +// to compile if any method violates object-safety. +const _: fn() = || { + fn _assert_object_safe(_: &dyn DynColdStorageBackend) {} +}; + +// Suppress unused-import warnings until later tasks consume Arc. +const _: fn() = || { + let _: Option> = None; +}; diff --git a/crates/cold/src/lib.rs b/crates/cold/src/lib.rs index 14221cc..e6a1054 100644 --- a/crates/cold/src/lib.rs +++ b/crates/cold/src/lib.rs @@ -155,6 +155,9 @@ mod specifier; mod stream; pub use stream::{StreamParams, produce_log_stream_default}; +mod dyn_backend; +pub use dyn_backend::DynColdStorageBackend; + mod traits; pub use traits::{BlockData, ColdStorageBackend, ColdStorageRead, ColdStorageWrite, LogStream}; From a209cf16d03a8bfeac80e97ee21e49934aafd59b Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 11:37:16 -0400 Subject: [PATCH 03/14] cold: blanket impl DynColdStorageBackend for ColdStorageBackend Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/dyn_backend.rs | 145 ++++++++++++++++++++++++++++++--- 1 file changed, 135 insertions(+), 10 deletions(-) diff --git a/crates/cold/src/dyn_backend.rs b/crates/cold/src/dyn_backend.rs index 5dcddba..838b4e2 100644 --- a/crates/cold/src/dyn_backend.rs +++ b/crates/cold/src/dyn_backend.rs @@ -20,12 +20,13 @@ //! [`ColdStorageWrite`]: crate::ColdStorageWrite use crate::{ - BlockData, ColdReceipt, ColdResult, Confirmed, Filter, HeaderSpecifier, ReceiptSpecifier, - RpcLog, SignetEventsSpecifier, StreamParams, TransactionSpecifier, ZenithHeaderSpecifier, + BlockData, ColdReceipt, ColdResult, ColdStorageBackend, ColdStorageRead, ColdStorageWrite, + Confirmed, Filter, HeaderSpecifier, ReceiptSpecifier, RpcLog, SignetEventsSpecifier, + StreamParams, TransactionSpecifier, ZenithHeaderSpecifier, }; use alloy::primitives::BlockNumber; use signet_storage_types::{DbSignetEvent, DbZenithHeader, RecoveredTx, SealedHeader}; -use std::{future::Future, pin::Pin, sync::Arc, time::Duration}; +use std::{future::Future, pin::Pin, time::Duration}; /// Object-safe mirror of [`ColdStorageBackend`]. Auto-implemented by a /// blanket impl over every `B: ColdStorageBackend`; do not implement @@ -144,13 +145,137 @@ pub trait DynColdStorageBackend: Send + Sync + 'static { fn dyn_write_timeout(&self) -> Option; } -// Sanity check: ensure the trait is object-safe. The line below fails -// to compile if any method violates object-safety. -const _: fn() = || { - fn _assert_object_safe(_: &dyn DynColdStorageBackend) {} -}; +impl DynColdStorageBackend for B { + fn dyn_get_header<'a>( + &'a self, + spec: HeaderSpecifier, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_header(self, spec)) + } + + fn dyn_get_headers<'a>( + &'a self, + specs: Vec, + ) -> Pin>>> + Send + 'a>> { + Box::pin(::get_headers(self, specs)) + } + + fn dyn_get_transaction<'a>( + &'a self, + spec: TransactionSpecifier, + ) -> Pin>>> + Send + 'a>> { + Box::pin(::get_transaction(self, spec)) + } + + fn dyn_get_transactions_in_block<'a>( + &'a self, + block: BlockNumber, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_transactions_in_block(self, block)) + } + + fn dyn_get_transaction_count<'a>( + &'a self, + block: BlockNumber, + ) -> Pin> + Send + 'a>> { + Box::pin(::get_transaction_count(self, block)) + } + + fn dyn_get_receipt<'a>( + &'a self, + spec: ReceiptSpecifier, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_receipt(self, spec)) + } + + fn dyn_get_receipts_in_block<'a>( + &'a self, + block: BlockNumber, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_receipts_in_block(self, block)) + } + + fn dyn_get_signet_events<'a>( + &'a self, + spec: SignetEventsSpecifier, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_signet_events(self, spec)) + } + + fn dyn_get_zenith_header<'a>( + &'a self, + spec: ZenithHeaderSpecifier, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_zenith_header(self, spec)) + } -// Suppress unused-import warnings until later tasks consume Arc. + fn dyn_get_zenith_headers<'a>( + &'a self, + spec: ZenithHeaderSpecifier, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_zenith_headers(self, spec)) + } + + fn dyn_get_latest_block<'a>( + &'a self, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_latest_block(self)) + } + + fn dyn_get_logs<'a>( + &'a self, + filter: &'a Filter, + max_logs: usize, + ) -> Pin>> + Send + 'a>> { + Box::pin(::get_logs(self, filter, max_logs)) + } + + fn dyn_produce_log_stream<'a>( + &'a self, + filter: &'a Filter, + params: StreamParams, + ) -> Pin + Send + 'a>> { + Box::pin(::produce_log_stream(self, filter, params)) + } + + fn dyn_append_block<'a>( + &'a self, + data: BlockData, + ) -> Pin> + Send + 'a>> { + Box::pin(::append_block(self, data)) + } + + fn dyn_append_blocks<'a>( + &'a self, + data: Vec, + ) -> Pin> + Send + 'a>> { + Box::pin(::append_blocks(self, data)) + } + + fn dyn_truncate_above<'a>( + &'a self, + block: BlockNumber, + ) -> Pin> + Send + 'a>> { + Box::pin(::truncate_above(self, block)) + } + + fn dyn_drain_above<'a>( + &'a self, + block: BlockNumber, + ) -> Pin>>> + Send + 'a>> { + Box::pin(::drain_above(self, block)) + } + + fn dyn_read_timeout(&self) -> Option { + ::read_timeout(self) + } + + fn dyn_write_timeout(&self) -> Option { + ::write_timeout(self) + } +} + +// Compile-time check that the trait is object-safe. const _: fn() = || { - let _: Option> = None; + fn _assert_object_safe(_: &dyn DynColdStorageBackend) {} }; From ff3650d6e5d79aff07749b5d2108db9337ec8b03 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 11:41:12 -0400 Subject: [PATCH 04/14] cold: impl ColdStorageBackend for Arc Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/dyn_backend.rs | 145 ++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/crates/cold/src/dyn_backend.rs b/crates/cold/src/dyn_backend.rs index 838b4e2..ab56851 100644 --- a/crates/cold/src/dyn_backend.rs +++ b/crates/cold/src/dyn_backend.rs @@ -14,6 +14,15 @@ //! and downstream signatures. Backends should implement //! [`ColdStorageBackend`] — the blanket impl handles this trait. //! +//! # Filter Cloning on the Erased Path +//! +//! The [`ColdStorageRead`] impl for `Arc` +//! clones the [`Filter`](crate::Filter) inside `get_logs` and +//! `produce_log_stream`. The dyn methods unify `&self` and `&Filter` +//! into a single lifetime, which cannot be expressed by the +//! independent-lifetime trait signatures without an owned bridge. The +//! concrete `ColdStorage` path is unaffected. +//! //! [`ColdStorage`]: crate::ColdStorage //! [`ColdStorageBackend`]: crate::ColdStorageBackend //! [`ColdStorageRead`]: crate::ColdStorageRead @@ -26,7 +35,7 @@ use crate::{ }; use alloy::primitives::BlockNumber; use signet_storage_types::{DbSignetEvent, DbZenithHeader, RecoveredTx, SealedHeader}; -use std::{future::Future, pin::Pin, time::Duration}; +use std::{future::Future, pin::Pin, sync::Arc, time::Duration}; /// Object-safe mirror of [`ColdStorageBackend`]. Auto-implemented by a /// blanket impl over every `B: ColdStorageBackend`; do not implement @@ -279,3 +288,137 @@ impl DynColdStorageBackend for B { const _: fn() = || { fn _assert_object_safe(_: &dyn DynColdStorageBackend) {} }; + +impl ColdStorageRead for Arc { + fn get_header( + &self, + spec: HeaderSpecifier, + ) -> impl Future>> + Send { + (**self).dyn_get_header(spec) + } + + fn get_headers( + &self, + specs: Vec, + ) -> impl Future>>> + Send { + (**self).dyn_get_headers(specs) + } + + fn get_transaction( + &self, + spec: TransactionSpecifier, + ) -> impl Future>>> + Send { + (**self).dyn_get_transaction(spec) + } + + fn get_transactions_in_block( + &self, + block: BlockNumber, + ) -> impl Future>> + Send { + (**self).dyn_get_transactions_in_block(block) + } + + fn get_transaction_count( + &self, + block: BlockNumber, + ) -> impl Future> + Send { + (**self).dyn_get_transaction_count(block) + } + + fn get_receipt( + &self, + spec: ReceiptSpecifier, + ) -> impl Future>> + Send { + (**self).dyn_get_receipt(spec) + } + + fn get_receipts_in_block( + &self, + block: BlockNumber, + ) -> impl Future>> + Send { + (**self).dyn_get_receipts_in_block(block) + } + + fn get_signet_events( + &self, + spec: SignetEventsSpecifier, + ) -> impl Future>> + Send { + (**self).dyn_get_signet_events(spec) + } + + fn get_zenith_header( + &self, + spec: ZenithHeaderSpecifier, + ) -> impl Future>> + Send { + (**self).dyn_get_zenith_header(spec) + } + + fn get_zenith_headers( + &self, + spec: ZenithHeaderSpecifier, + ) -> impl Future>> + Send { + (**self).dyn_get_zenith_headers(spec) + } + + fn get_latest_block(&self) -> impl Future>> + Send { + (**self).dyn_get_latest_block() + } + + fn get_logs( + &self, + filter: &Filter, + max_logs: usize, + ) -> impl Future>> + Send { + let this = self.clone(); + let filter = filter.clone(); + async move { this.dyn_get_logs(&filter, max_logs).await } + } + + fn produce_log_stream( + &self, + filter: &Filter, + params: StreamParams, + ) -> impl Future + Send { + let this = self.clone(); + let filter = filter.clone(); + async move { this.dyn_produce_log_stream(&filter, params).await } + } +} + +impl ColdStorageWrite for Arc { + fn append_block(&self, data: BlockData) -> impl Future> + Send { + (**self).dyn_append_block(data) + } + + fn append_blocks(&self, data: Vec) -> impl Future> + Send { + (**self).dyn_append_blocks(data) + } + + fn truncate_above(&self, block: BlockNumber) -> impl Future> + Send { + (**self).dyn_truncate_above(block) + } +} + +impl ColdStorageBackend for Arc { + fn read_timeout(&self) -> Option { + (**self).dyn_read_timeout() + } + + fn write_timeout(&self) -> Option { + (**self).dyn_write_timeout() + } + + fn drain_above( + &self, + block: BlockNumber, + ) -> impl Future>>> + Send { + (**self).dyn_drain_above(block) + } +} + +// Compile-time check that `Arc` satisfies the +// bound `ColdStorage` will require. +const _: fn() = || { + const fn _assert_bound() {} + _assert_bound::>(); +}; From 52fcc72bb25d9333634adafea76c4ad1c089f52b Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 11:49:57 -0400 Subject: [PATCH 05/14] cold: default ColdStorage to erased backend; add new_erased Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/handle.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/cold/src/handle.rs b/crates/cold/src/handle.rs index 6547b05..b7dd53e 100644 --- a/crates/cold/src/handle.rs +++ b/crates/cold/src/handle.rs @@ -111,7 +111,7 @@ pub(crate) struct Inner { /// `ColdStorage` is cheap to [`Clone`] — it is just an `Arc` around the /// shared inner state. All operations dispatch through semaphore-gated /// [`TaskTracker`]-spawned tasks. -pub struct ColdStorage { +pub struct ColdStorage> { inner: Arc>, } @@ -692,3 +692,19 @@ impl ColdStorage { .await } } + +impl ColdStorage> { + /// Construct a type-erased cold storage handle. + /// + /// Wraps `backend` in `Arc` so the + /// resulting handle has no `B` type parameter to propagate + /// through downstream signatures. Equivalent to + /// `ColdStorage::new(Arc::new(backend) as Arc, cancel)`. + /// + /// Choose this constructor when you want runtime swappability of + /// the backend; use [`new`](Self::new) directly for fully + /// monomorphized call sites. + pub fn new_erased(backend: B, cancel: CancellationToken) -> Self { + Self::new(Arc::new(backend) as Arc, cancel) + } +} From 99cc92407738f0d3f651c9d912f33c613f0f1daa Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 11:53:29 -0400 Subject: [PATCH 06/14] cold: add erased conformance entry point Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/conformance.rs | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/cold/src/conformance.rs b/crates/cold/src/conformance.rs index 72f13d2..2ee3b30 100644 --- a/crates/cold/src/conformance.rs +++ b/crates/cold/src/conformance.rs @@ -5,8 +5,8 @@ //! a custom backend, call the test functions with your backend instance. use crate::{ - BlockData, ColdResult, ColdStorage, ColdStorageBackend, ColdStorageError, Filter, - HeaderSpecifier, ReceiptSpecifier, RpcLog, TransactionSpecifier, + BlockData, ColdResult, ColdStorage, ColdStorageBackend, ColdStorageError, + DynColdStorageBackend, Filter, HeaderSpecifier, ReceiptSpecifier, RpcLog, TransactionSpecifier, }; use alloy::{ consensus::{ @@ -17,7 +17,7 @@ use alloy::{ }, }; use signet_storage_types::{Receipt, RecoveredTx, TransactionSigned}; -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use tokio_stream::StreamExt; use tokio_util::sync::CancellationToken; @@ -43,6 +43,32 @@ pub async fn conformance(backend: B) -> ColdResult<()> { Ok(()) } +/// Run the conformance suite against `backend` after erasing it +/// through [`DynColdStorageBackend`]. +/// +/// Exercises the same contract as [`conformance`] but routes every +/// call through `Arc`, validating that +/// the erased dispatch path upholds the trait contract. +pub async fn conformance_erased(backend: B) -> ColdResult<()> { + let erased: Arc = Arc::new(backend); + let cancel = CancellationToken::new(); + let handle = ColdStorage::new(erased, cancel.clone()); + test_empty_storage(&handle).await?; + test_append_and_read_header(&handle).await?; + test_header_hash_lookup(&handle).await?; + test_transaction_lookups(&handle).await?; + test_receipt_lookups(&handle).await?; + test_truncation(&handle).await?; + test_batch_append(&handle).await?; + test_confirmation_metadata(&handle).await?; + test_cold_receipt_metadata(&handle).await?; + test_get_logs(&handle).await?; + test_stream_logs(&handle).await?; + test_drain_above(&handle).await?; + cancel.cancel(); + Ok(()) +} + /// Create test block data for conformance tests. /// /// Creates a minimal valid block with the given block number. From 2d8f4ec62ab5530c565421fdddf2724100587648 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 11:59:13 -0400 Subject: [PATCH 07/14] cold: test erased conformance + drain_above override preservation Also fix infinite recursion in the Arc ColdStorageRead impl: get_logs and produce_log_stream were cycling back through the blanket DynColdStorageBackend impl. Fixed by calling DynColdStorageBackend::dyn_* directly on the inner trait object via Arc::as_ref(), bypassing the blanket dispatch loop. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/dyn_backend.rs | 26 ++++- crates/cold/src/mem.rs | 169 ++++++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 3 deletions(-) diff --git a/crates/cold/src/dyn_backend.rs b/crates/cold/src/dyn_backend.rs index ab56851..c36c06e 100644 --- a/crates/cold/src/dyn_backend.rs +++ b/crates/cold/src/dyn_backend.rs @@ -23,6 +23,20 @@ //! independent-lifetime trait signatures without an owned bridge. The //! concrete `ColdStorage` path is unaffected. //! +//! # Maintainer Note: Recursion Hazard for Borrowed Arguments +//! +//! Any method on the `Arc` impls that +//! cannot use the direct `(**self).dyn_(...)` form (because a +//! borrowed argument forces it through a `self.clone()` + `async move` +//! bridge) MUST dispatch via qualified path on the inner trait object, +//! e.g. `DynColdStorageBackend::dyn_(this.as_ref(), ...)`. +//! +//! Writing `this.dyn_(...)` on a cloned `Arc` resolves +//! to the blanket impl (`Arc: ColdStorageBackend` ⇒ +//! `Arc: DynColdStorageBackend`), which calls back into the +//! strong-trait impl and recurses infinitely. See `get_logs` and +//! `produce_log_stream` for the canonical pattern. +//! //! [`ColdStorage`]: crate::ColdStorage //! [`ColdStorageBackend`]: crate::ColdStorageBackend //! [`ColdStorageRead`]: crate::ColdStorageRead @@ -371,7 +385,10 @@ impl ColdStorageRead for Arc { ) -> impl Future>> + Send { let this = self.clone(); let filter = filter.clone(); - async move { this.dyn_get_logs(&filter, max_logs).await } + // Call dyn_get_logs via the inner trait object directly (not through + // the Arc's blanket DynColdStorageBackend impl), which would re-enter + // ColdStorageRead::get_logs on Arc and recurse infinitely. + async move { DynColdStorageBackend::dyn_get_logs(this.as_ref(), &filter, max_logs).await } } fn produce_log_stream( @@ -381,7 +398,12 @@ impl ColdStorageRead for Arc { ) -> impl Future + Send { let this = self.clone(); let filter = filter.clone(); - async move { this.dyn_produce_log_stream(&filter, params).await } + // Same recursion hazard as `get_logs` above — call through + // `as_ref()` + qualified path so dispatch lands on the inner + // trait object's vtable, not the Arc's blanket impl. + async move { + DynColdStorageBackend::dyn_produce_log_stream(this.as_ref(), &filter, params).await + } } } diff --git a/crates/cold/src/mem.rs b/crates/cold/src/mem.rs index 37d0a99..88f1a7b 100644 --- a/crates/cold/src/mem.rs +++ b/crates/cold/src/mem.rs @@ -387,7 +387,7 @@ impl ColdStorageBackend for MemColdBackend { mod test { use super::*; - use crate::conformance::conformance; + use crate::conformance::{conformance, conformance_erased}; #[tokio::test] async fn mem_backend_conformance() { @@ -395,6 +395,173 @@ mod test { conformance(backend).await.unwrap(); } + #[tokio::test] + async fn erased_conformance() { + conformance_erased(MemColdBackend::new()).await.unwrap(); + } + + #[tokio::test] + async fn dyn_drain_above_invokes_backend_override() { + use crate::{ + BlockData, ColdReceipt, ColdResult, ColdStorageBackend, ColdStorageRead, + ColdStorageWrite, DynColdStorageBackend, Filter, HeaderSpecifier, ReceiptSpecifier, + RpcLog, SignetEventsSpecifier, StreamParams, TransactionSpecifier, + ZenithHeaderSpecifier, + }; + use alloy::primitives::BlockNumber; + use signet_storage_types::{DbSignetEvent, DbZenithHeader, RecoveredTx, SealedHeader}; + use std::{ + future::Future, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + }; + + #[derive(Clone)] + struct DrainTracker { + inner: MemColdBackend, + called: Arc, + } + + impl ColdStorageRead for DrainTracker { + fn get_header( + &self, + spec: HeaderSpecifier, + ) -> impl Future>> + Send { + self.inner.get_header(spec) + } + + fn get_headers( + &self, + specs: Vec, + ) -> impl Future>>> + Send { + self.inner.get_headers(specs) + } + + fn get_transaction( + &self, + spec: TransactionSpecifier, + ) -> impl Future>>> + Send + { + self.inner.get_transaction(spec) + } + + fn get_transactions_in_block( + &self, + block: BlockNumber, + ) -> impl Future>> + Send { + self.inner.get_transactions_in_block(block) + } + + fn get_transaction_count( + &self, + block: BlockNumber, + ) -> impl Future> + Send { + self.inner.get_transaction_count(block) + } + + fn get_receipt( + &self, + spec: ReceiptSpecifier, + ) -> impl Future>> + Send { + self.inner.get_receipt(spec) + } + + fn get_receipts_in_block( + &self, + block: BlockNumber, + ) -> impl Future>> + Send { + self.inner.get_receipts_in_block(block) + } + + fn get_signet_events( + &self, + spec: SignetEventsSpecifier, + ) -> impl Future>> + Send { + self.inner.get_signet_events(spec) + } + + fn get_zenith_header( + &self, + spec: ZenithHeaderSpecifier, + ) -> impl Future>> + Send { + self.inner.get_zenith_header(spec) + } + + fn get_zenith_headers( + &self, + spec: ZenithHeaderSpecifier, + ) -> impl Future>> + Send { + self.inner.get_zenith_headers(spec) + } + + fn get_latest_block( + &self, + ) -> impl Future>> + Send { + self.inner.get_latest_block() + } + + fn get_logs( + &self, + filter: &Filter, + max_logs: usize, + ) -> impl Future>> + Send { + self.inner.get_logs(filter, max_logs) + } + + fn produce_log_stream( + &self, + filter: &Filter, + params: StreamParams, + ) -> impl Future + Send { + self.inner.produce_log_stream(filter, params) + } + } + + impl ColdStorageWrite for DrainTracker { + fn append_block(&self, data: BlockData) -> impl Future> + Send { + self.inner.append_block(data) + } + + fn append_blocks( + &self, + data: Vec, + ) -> impl Future> + Send { + self.inner.append_blocks(data) + } + + fn truncate_above( + &self, + block: BlockNumber, + ) -> impl Future> + Send { + self.inner.truncate_above(block) + } + } + + impl ColdStorageBackend for DrainTracker { + fn drain_above( + &self, + block: BlockNumber, + ) -> impl Future>>> + Send { + let called = self.called.clone(); + let fut = self.inner.drain_above(block); + async move { + called.store(true, Ordering::SeqCst); + fut.await + } + } + } + + let called = Arc::new(AtomicBool::new(false)); + let backend = DrainTracker { inner: MemColdBackend::new(), called: called.clone() }; + let erased: Arc = Arc::new(backend); + + let _ = erased.dyn_drain_above(0).await.unwrap(); + + assert!(called.load(Ordering::SeqCst), "overridden drain_above must run through erasure"); + } + #[tokio::test] async fn write_trait_takes_self_shared_ref() { // Compile-fence: proves ColdStorageWrite can be called on &self. From 8274dce47bbb129e4ce60fea3be07f83b43a6e06 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 13 May 2026 13:14:05 -0400 Subject: [PATCH 08/14] cold: drop redundant explicit link target on Filter rustdoc Workspace doc build runs with -D warnings which enables rustdoc::redundant_explicit_links. The label already auto-resolves to crate::Filter via the prelude, so the explicit target is rejected. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/dyn_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cold/src/dyn_backend.rs b/crates/cold/src/dyn_backend.rs index c36c06e..25f6d1f 100644 --- a/crates/cold/src/dyn_backend.rs +++ b/crates/cold/src/dyn_backend.rs @@ -17,7 +17,7 @@ //! # Filter Cloning on the Erased Path //! //! The [`ColdStorageRead`] impl for `Arc` -//! clones the [`Filter`](crate::Filter) inside `get_logs` and +//! clones the [`Filter`] inside `get_logs` and //! `produce_log_stream`. The dyn methods unify `&self` and `&Filter` //! into a single lifetime, which cannot be expressed by the //! independent-lifetime trait signatures without an owned bridge. The From 741e5f9d413f0e5b7ad9a6835922ba010722038a Mon Sep 17 00:00:00 2001 From: James Date: Thu, 14 May 2026 02:05:49 -0400 Subject: [PATCH 09/14] chore: bump version to 0.8.1 Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 51baa85..2f8c351 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.8.0" +version = "0.8.1" edition = "2024" rust-version = "1.92" authors = ["init4"] @@ -35,13 +35,13 @@ incremental = false [workspace.dependencies] # internal -signet-hot = { version = "0.8.0", path = "./crates/hot" } -signet-hot-mdbx = { version = "0.8.0", path = "./crates/hot-mdbx" } -signet-cold = { version = "0.8.0", path = "./crates/cold" } -signet-cold-mdbx = { version = "0.8.0", path = "./crates/cold-mdbx" } -signet-cold-sql = { version = "0.8.0", path = "./crates/cold-sql" } -signet-storage = { version = "0.8.0", path = "./crates/storage" } -signet-storage-types = { version = "0.8.0", path = "./crates/types" } +signet-hot = { version = "0.8.1", path = "./crates/hot" } +signet-hot-mdbx = { version = "0.8.1", path = "./crates/hot-mdbx" } +signet-cold = { version = "0.8.1", path = "./crates/cold" } +signet-cold-mdbx = { version = "0.8.1", path = "./crates/cold-mdbx" } +signet-cold-sql = { version = "0.8.1", path = "./crates/cold-sql" } +signet-storage = { version = "0.8.1", path = "./crates/storage" } +signet-storage-types = { version = "0.8.1", path = "./crates/types" } # External, in-house signet-libmdbx = { version = "0.8.0" } From abd96d966b362aed02826e5e1717fb0aa4c06968 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 14 May 2026 02:12:08 -0400 Subject: [PATCH 10/14] storage: default UnifiedStorage to erased backend; add spawn_erased Mirrors ColdStorage's erased default so callers can hold UnifiedStorage without propagating a backend generic. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/storage/src/unified.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/storage/src/unified.rs b/crates/storage/src/unified.rs index e8c1b8d..dfa8207 100644 --- a/crates/storage/src/unified.rs +++ b/crates/storage/src/unified.rs @@ -73,7 +73,10 @@ pub struct DrainedBlock { /// storage.unwind_above(reorg_block).await?; /// ``` #[derive(Debug)] -pub struct UnifiedStorage { +pub struct UnifiedStorage< + H: HotKv, + B: ColdStorageBackend = std::sync::Arc, +> { hot: H, cold: ColdStorage, } @@ -93,7 +96,25 @@ impl UnifiedStorage { let cold = ColdStorage::new(cold_backend, cancel_token); Self::new(hot, cold) } +} +impl UnifiedStorage> { + /// Spawn a unified storage with a type-erased cold backend. + /// + /// Erases the concrete cold backend behind + /// [`signet_cold::DynColdStorageBackend`], so callers can hold a + /// `UnifiedStorage` without propagating a backend generic. + pub fn spawn_erased( + hot: H, + cold_backend: B, + cancel_token: CancellationToken, + ) -> Self { + let cold = ColdStorage::new_erased(cold_backend, cancel_token); + Self::new(hot, cold) + } +} + +impl UnifiedStorage { /// Get a reference to the hot storage backend. pub const fn hot(&self) -> &H { &self.hot From 13061f7a8d8c8fdd416cfe94b1161bed29ffb4da Mon Sep 17 00:00:00 2001 From: James Date: Thu, 14 May 2026 02:15:23 -0400 Subject: [PATCH 11/14] cold: introduce StorageFuture + ErasedBackend type aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `StorageFuture<'a, T>` alias for `Pin>` and use it across the `DynColdStorageBackend` trait and blanket impl; drop the `#[allow(clippy::type_complexity)]` workaround. - Add `ErasedBackend = Arc` and use it as the default `B` on `ColdStorage`, in `new_erased`, and in the strong-trait impls — `handle.rs` no longer reaches for `crate::DynColdStorageBackend` via qualified paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/dyn_backend.rs | 160 +++++++++++++++------------------ crates/cold/src/handle.rs | 21 ++--- crates/cold/src/lib.rs | 2 +- crates/storage/src/unified.rs | 7 +- 4 files changed, 88 insertions(+), 102 deletions(-) diff --git a/crates/cold/src/dyn_backend.rs b/crates/cold/src/dyn_backend.rs index 25f6d1f..fb14e9c 100644 --- a/crates/cold/src/dyn_backend.rs +++ b/crates/cold/src/dyn_backend.rs @@ -2,39 +2,39 @@ //! //! [`DynColdStorageBackend`] re-declares every method on //! [`ColdStorageRead`], [`ColdStorageWrite`], and [`ColdStorageBackend`] -//! with an explicit `Pin>` return type so -//! the trait is object-safe. A blanket impl auto-implements it for -//! every `B: ColdStorageBackend`, and `Arc` -//! re-implements the strong traits by delegating to the boxed methods. +//! with an explicit [`StorageFuture`] return type so the trait is +//! object-safe. A blanket impl auto-implements it for every +//! `B: ColdStorageBackend`, and [`ErasedBackend`] re-implements the +//! strong traits by delegating to the boxed methods. //! //! # Plumbing, Not API //! //! This trait exists so [`ColdStorage`]'s default type parameter -//! (`Arc`) is nameable in error messages -//! and downstream signatures. Backends should implement -//! [`ColdStorageBackend`] — the blanket impl handles this trait. +//! ([`ErasedBackend`]) is nameable in error messages and downstream +//! signatures. Backends should implement [`ColdStorageBackend`] — the +//! blanket impl handles this trait. //! //! # Filter Cloning on the Erased Path //! -//! The [`ColdStorageRead`] impl for `Arc` -//! clones the [`Filter`] inside `get_logs` and -//! `produce_log_stream`. The dyn methods unify `&self` and `&Filter` -//! into a single lifetime, which cannot be expressed by the -//! independent-lifetime trait signatures without an owned bridge. The -//! concrete `ColdStorage` path is unaffected. +//! The [`ColdStorageRead`] impl for [`ErasedBackend`] clones the +//! [`Filter`] inside `get_logs` and `produce_log_stream`. The dyn +//! methods unify `&self` and `&Filter` into a single lifetime, which +//! cannot be expressed by the independent-lifetime trait signatures +//! without an owned bridge. The concrete `ColdStorage` path is +//! unaffected. //! //! # Maintainer Note: Recursion Hazard for Borrowed Arguments //! -//! Any method on the `Arc` impls that -//! cannot use the direct `(**self).dyn_(...)` form (because a -//! borrowed argument forces it through a `self.clone()` + `async move` -//! bridge) MUST dispatch via qualified path on the inner trait object, -//! e.g. `DynColdStorageBackend::dyn_(this.as_ref(), ...)`. +//! Any method on the [`ErasedBackend`] impls that cannot use the +//! direct `(**self).dyn_(...)` form (because a borrowed argument +//! forces it through a `self.clone()` + `async move` bridge) MUST +//! dispatch via qualified path on the inner trait object, e.g. +//! `DynColdStorageBackend::dyn_(this.as_ref(), ...)`. //! -//! Writing `this.dyn_(...)` on a cloned `Arc` resolves -//! to the blanket impl (`Arc: ColdStorageBackend` ⇒ -//! `Arc: DynColdStorageBackend`), which calls back into the -//! strong-trait impl and recurses infinitely. See `get_logs` and +//! Writing `this.dyn_(...)` on a cloned [`ErasedBackend`] +//! resolves to the blanket impl (`ErasedBackend: ColdStorageBackend` +//! ⇒ `ErasedBackend: DynColdStorageBackend`), which calls back into +//! the strong-trait impl and recurses infinitely. See `get_logs` and //! `produce_log_stream` for the canonical pattern. //! //! [`ColdStorage`]: crate::ColdStorage @@ -51,115 +51,114 @@ use alloy::primitives::BlockNumber; use signet_storage_types::{DbSignetEvent, DbZenithHeader, RecoveredTx, SealedHeader}; use std::{future::Future, pin::Pin, sync::Arc, time::Duration}; +/// Boxed, pinned, `Send`-able future returned from object-safe +/// [`DynColdStorageBackend`] methods. +pub type StorageFuture<'a, T> = Pin + Send + 'a>>; + +/// Type-erased cold storage backend, shareable across tasks. +/// +/// This is the default `B` for [`ColdStorage`](crate::ColdStorage): a +/// handle written as plain `ColdStorage` uses this backend. Construct +/// one with [`ColdStorage::new_erased`](crate::ColdStorage::new_erased). +pub type ErasedBackend = Arc; + /// Object-safe mirror of [`ColdStorageBackend`]. Auto-implemented by a /// blanket impl over every `B: ColdStorageBackend`; do not implement /// directly. /// /// [`ColdStorageBackend`]: crate::ColdStorageBackend -#[allow(clippy::type_complexity)] pub trait DynColdStorageBackend: Send + Sync + 'static { /// Get a header by specifier. fn dyn_get_header<'a>( &'a self, spec: HeaderSpecifier, - ) -> Pin>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>; /// Get multiple headers by specifiers. fn dyn_get_headers<'a>( &'a self, specs: Vec, - ) -> Pin>>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>>; /// Get a transaction by specifier, with block confirmation metadata. fn dyn_get_transaction<'a>( &'a self, spec: TransactionSpecifier, - ) -> Pin>>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>>; /// Get all transactions in a block. fn dyn_get_transactions_in_block<'a>( &'a self, block: BlockNumber, - ) -> Pin>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>; /// Get the number of transactions in a block. fn dyn_get_transaction_count<'a>( &'a self, block: BlockNumber, - ) -> Pin> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>; /// Get a receipt by specifier. fn dyn_get_receipt<'a>( &'a self, spec: ReceiptSpecifier, - ) -> Pin>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>; /// Get all receipts in a block. fn dyn_get_receipts_in_block<'a>( &'a self, block: BlockNumber, - ) -> Pin>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>; /// Get signet events by specifier. fn dyn_get_signet_events<'a>( &'a self, spec: SignetEventsSpecifier, - ) -> Pin>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>; /// Get a zenith header by specifier. fn dyn_get_zenith_header<'a>( &'a self, spec: ZenithHeaderSpecifier, - ) -> Pin>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>; /// Get multiple zenith headers by specifier. fn dyn_get_zenith_headers<'a>( &'a self, spec: ZenithHeaderSpecifier, - ) -> Pin>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>; /// Get the latest block number in storage. - fn dyn_get_latest_block<'a>( - &'a self, - ) -> Pin>> + Send + 'a>>; + fn dyn_get_latest_block<'a>(&'a self) -> StorageFuture<'a, ColdResult>>; /// Filter logs by block range, address, and topics. fn dyn_get_logs<'a>( &'a self, filter: &'a Filter, max_logs: usize, - ) -> Pin>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>; /// Produce a log stream by iterating blocks and sending matching logs. fn dyn_produce_log_stream<'a>( &'a self, filter: &'a Filter, params: StreamParams, - ) -> Pin + Send + 'a>>; + ) -> StorageFuture<'a, ()>; /// Append a single block to cold storage. - fn dyn_append_block<'a>( - &'a self, - data: BlockData, - ) -> Pin> + Send + 'a>>; + fn dyn_append_block<'a>(&'a self, data: BlockData) -> StorageFuture<'a, ColdResult<()>>; /// Append multiple blocks to cold storage. - fn dyn_append_blocks<'a>( - &'a self, - data: Vec, - ) -> Pin> + Send + 'a>>; + fn dyn_append_blocks<'a>(&'a self, data: Vec) -> StorageFuture<'a, ColdResult<()>>; /// Truncate all data above the given block number (exclusive). - fn dyn_truncate_above<'a>( - &'a self, - block: BlockNumber, - ) -> Pin> + Send + 'a>>; + fn dyn_truncate_above<'a>(&'a self, block: BlockNumber) -> StorageFuture<'a, ColdResult<()>>; /// Read and remove all blocks above the given block number. fn dyn_drain_above<'a>( &'a self, block: BlockNumber, - ) -> Pin>>> + Send + 'a>>; + ) -> StorageFuture<'a, ColdResult>>>; /// Configured read deadline, if any. fn dyn_read_timeout(&self) -> Option; @@ -172,76 +171,74 @@ impl DynColdStorageBackend for B { fn dyn_get_header<'a>( &'a self, spec: HeaderSpecifier, - ) -> Pin>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_header(self, spec)) } fn dyn_get_headers<'a>( &'a self, specs: Vec, - ) -> Pin>>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>>> { Box::pin(::get_headers(self, specs)) } fn dyn_get_transaction<'a>( &'a self, spec: TransactionSpecifier, - ) -> Pin>>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>>> { Box::pin(::get_transaction(self, spec)) } fn dyn_get_transactions_in_block<'a>( &'a self, block: BlockNumber, - ) -> Pin>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_transactions_in_block(self, block)) } fn dyn_get_transaction_count<'a>( &'a self, block: BlockNumber, - ) -> Pin> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult> { Box::pin(::get_transaction_count(self, block)) } fn dyn_get_receipt<'a>( &'a self, spec: ReceiptSpecifier, - ) -> Pin>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_receipt(self, spec)) } fn dyn_get_receipts_in_block<'a>( &'a self, block: BlockNumber, - ) -> Pin>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_receipts_in_block(self, block)) } fn dyn_get_signet_events<'a>( &'a self, spec: SignetEventsSpecifier, - ) -> Pin>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_signet_events(self, spec)) } fn dyn_get_zenith_header<'a>( &'a self, spec: ZenithHeaderSpecifier, - ) -> Pin>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_zenith_header(self, spec)) } fn dyn_get_zenith_headers<'a>( &'a self, spec: ZenithHeaderSpecifier, - ) -> Pin>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_zenith_headers(self, spec)) } - fn dyn_get_latest_block<'a>( - &'a self, - ) -> Pin>> + Send + 'a>> { + fn dyn_get_latest_block<'a>(&'a self) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_latest_block(self)) } @@ -249,7 +246,7 @@ impl DynColdStorageBackend for B { &'a self, filter: &'a Filter, max_logs: usize, - ) -> Pin>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>> { Box::pin(::get_logs(self, filter, max_logs)) } @@ -257,35 +254,26 @@ impl DynColdStorageBackend for B { &'a self, filter: &'a Filter, params: StreamParams, - ) -> Pin + Send + 'a>> { + ) -> StorageFuture<'a, ()> { Box::pin(::produce_log_stream(self, filter, params)) } - fn dyn_append_block<'a>( - &'a self, - data: BlockData, - ) -> Pin> + Send + 'a>> { + fn dyn_append_block<'a>(&'a self, data: BlockData) -> StorageFuture<'a, ColdResult<()>> { Box::pin(::append_block(self, data)) } - fn dyn_append_blocks<'a>( - &'a self, - data: Vec, - ) -> Pin> + Send + 'a>> { + fn dyn_append_blocks<'a>(&'a self, data: Vec) -> StorageFuture<'a, ColdResult<()>> { Box::pin(::append_blocks(self, data)) } - fn dyn_truncate_above<'a>( - &'a self, - block: BlockNumber, - ) -> Pin> + Send + 'a>> { + fn dyn_truncate_above<'a>(&'a self, block: BlockNumber) -> StorageFuture<'a, ColdResult<()>> { Box::pin(::truncate_above(self, block)) } fn dyn_drain_above<'a>( &'a self, block: BlockNumber, - ) -> Pin>>> + Send + 'a>> { + ) -> StorageFuture<'a, ColdResult>>> { Box::pin(::drain_above(self, block)) } @@ -303,7 +291,7 @@ const _: fn() = || { fn _assert_object_safe(_: &dyn DynColdStorageBackend) {} }; -impl ColdStorageRead for Arc { +impl ColdStorageRead for ErasedBackend { fn get_header( &self, spec: HeaderSpecifier, @@ -387,7 +375,7 @@ impl ColdStorageRead for Arc { let filter = filter.clone(); // Call dyn_get_logs via the inner trait object directly (not through // the Arc's blanket DynColdStorageBackend impl), which would re-enter - // ColdStorageRead::get_logs on Arc and recurse infinitely. + // ColdStorageRead::get_logs on ErasedBackend and recurse infinitely. async move { DynColdStorageBackend::dyn_get_logs(this.as_ref(), &filter, max_logs).await } } @@ -407,7 +395,7 @@ impl ColdStorageRead for Arc { } } -impl ColdStorageWrite for Arc { +impl ColdStorageWrite for ErasedBackend { fn append_block(&self, data: BlockData) -> impl Future> + Send { (**self).dyn_append_block(data) } @@ -421,7 +409,7 @@ impl ColdStorageWrite for Arc { } } -impl ColdStorageBackend for Arc { +impl ColdStorageBackend for ErasedBackend { fn read_timeout(&self) -> Option { (**self).dyn_read_timeout() } @@ -438,9 +426,9 @@ impl ColdStorageBackend for Arc { } } -// Compile-time check that `Arc` satisfies the -// bound `ColdStorage` will require. +// Compile-time check that `ErasedBackend` satisfies the bound +// `ColdStorage` will require. const _: fn() = || { const fn _assert_bound() {} - _assert_bound::>(); + _assert_bound::(); }; diff --git a/crates/cold/src/handle.rs b/crates/cold/src/handle.rs index b7dd53e..46092d4 100644 --- a/crates/cold/src/handle.rs +++ b/crates/cold/src/handle.rs @@ -14,9 +14,10 @@ //! participate in the drain barrier. use crate::{ - BlockData, ColdReceipt, ColdResult, ColdStorageBackend, ColdStorageError, Confirmed, Filter, - HeaderSpecifier, LogStream, ReceiptSpecifier, RpcLog, SignetEventsSpecifier, StreamParams, - TransactionSpecifier, ZenithHeaderSpecifier, cache::ColdCache, metrics, + BlockData, ColdReceipt, ColdResult, ColdStorageBackend, ColdStorageError, Confirmed, + ErasedBackend, Filter, HeaderSpecifier, LogStream, ReceiptSpecifier, RpcLog, + SignetEventsSpecifier, StreamParams, TransactionSpecifier, ZenithHeaderSpecifier, + cache::ColdCache, metrics, }; use alloy::primitives::{B256, BlockNumber}; use parking_lot::Mutex; @@ -111,7 +112,7 @@ pub(crate) struct Inner { /// `ColdStorage` is cheap to [`Clone`] — it is just an `Arc` around the /// shared inner state. All operations dispatch through semaphore-gated /// [`TaskTracker`]-spawned tasks. -pub struct ColdStorage> { +pub struct ColdStorage { inner: Arc>, } @@ -693,18 +694,18 @@ impl ColdStorage { } } -impl ColdStorage> { +impl ColdStorage { /// Construct a type-erased cold storage handle. /// - /// Wraps `backend` in `Arc` so the - /// resulting handle has no `B` type parameter to propagate - /// through downstream signatures. Equivalent to - /// `ColdStorage::new(Arc::new(backend) as Arc, cancel)`. + /// Wraps `backend` in [`ErasedBackend`] so the resulting handle + /// has no `B` type parameter to propagate through downstream + /// signatures. Equivalent to + /// `ColdStorage::new(Arc::new(backend) as ErasedBackend, cancel)`. /// /// Choose this constructor when you want runtime swappability of /// the backend; use [`new`](Self::new) directly for fully /// monomorphized call sites. pub fn new_erased(backend: B, cancel: CancellationToken) -> Self { - Self::new(Arc::new(backend) as Arc, cancel) + Self::new(Arc::new(backend) as ErasedBackend, cancel) } } diff --git a/crates/cold/src/lib.rs b/crates/cold/src/lib.rs index e6a1054..2a8b3c9 100644 --- a/crates/cold/src/lib.rs +++ b/crates/cold/src/lib.rs @@ -156,7 +156,7 @@ mod stream; pub use stream::{StreamParams, produce_log_stream_default}; mod dyn_backend; -pub use dyn_backend::DynColdStorageBackend; +pub use dyn_backend::{DynColdStorageBackend, ErasedBackend, StorageFuture}; mod traits; pub use traits::{BlockData, ColdStorageBackend, ColdStorageRead, ColdStorageWrite, LogStream}; diff --git a/crates/storage/src/unified.rs b/crates/storage/src/unified.rs index dfa8207..afac604 100644 --- a/crates/storage/src/unified.rs +++ b/crates/storage/src/unified.rs @@ -73,10 +73,7 @@ pub struct DrainedBlock { /// storage.unwind_above(reorg_block).await?; /// ``` #[derive(Debug)] -pub struct UnifiedStorage< - H: HotKv, - B: ColdStorageBackend = std::sync::Arc, -> { +pub struct UnifiedStorage { hot: H, cold: ColdStorage, } @@ -98,7 +95,7 @@ impl UnifiedStorage { } } -impl UnifiedStorage> { +impl UnifiedStorage { /// Spawn a unified storage with a type-erased cold backend. /// /// Erases the concrete cold backend behind From 4f8b3b79cdf9b601d2b7400b9074f1ac9a821acc Mon Sep 17 00:00:00 2001 From: James Date: Thu, 14 May 2026 02:19:31 -0400 Subject: [PATCH 12/14] storage: re-export ErasedBackend; use alias in spawn_erased docs Propagate the signet_cold::ErasedBackend alias to the signet-storage public surface so callers can write signet_storage::ErasedBackend, and swap the residual DynColdStorageBackend doc link in spawn_erased for the alias. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/storage/src/lib.rs | 1 + crates/storage/src/unified.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 75b6d5d..c389e22 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -83,6 +83,7 @@ pub use signet_cold_sql::SqlConnector; // Re-export key types for convenience pub use signet_cold::{ ColdStorage, ColdStorageBackend, ColdStorageError, ColdStorageRead, ColdStorageWrite, + DynColdStorageBackend, ErasedBackend, }; pub use signet_cold_mdbx::MdbxColdBackend; pub use signet_hot::{ diff --git a/crates/storage/src/unified.rs b/crates/storage/src/unified.rs index afac604..e1ae023 100644 --- a/crates/storage/src/unified.rs +++ b/crates/storage/src/unified.rs @@ -99,7 +99,7 @@ impl UnifiedStorage { /// Spawn a unified storage with a type-erased cold backend. /// /// Erases the concrete cold backend behind - /// [`signet_cold::DynColdStorageBackend`], so callers can hold a + /// [`signet_cold::ErasedBackend`], so callers can hold a /// `UnifiedStorage` without propagating a backend generic. pub fn spawn_erased( hot: H, From 36225899e7bdc85ed57d573d876a7135d049bb8d Mon Sep 17 00:00:00 2001 From: James Date: Thu, 14 May 2026 02:49:05 -0400 Subject: [PATCH 13/14] storage: hand-write Debug for UnifiedStorage Drops the implicit `B: Debug` bound from `#[derive(Debug)]` so that downstream `#[derive(Debug)]` on types holding `UnifiedStorage` (default `B = ErasedBackend`) compile without requiring `dyn DynColdStorageBackend: Debug`. Mirrors the existing manual impl on `ColdStorage`. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/storage/src/unified.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/storage/src/unified.rs b/crates/storage/src/unified.rs index e1ae023..f525d12 100644 --- a/crates/storage/src/unified.rs +++ b/crates/storage/src/unified.rs @@ -72,12 +72,17 @@ pub struct DrainedBlock { /// // Handle reorgs /// storage.unwind_above(reorg_block).await?; /// ``` -#[derive(Debug)] pub struct UnifiedStorage { hot: H, cold: ColdStorage, } +impl std::fmt::Debug for UnifiedStorage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UnifiedStorage").finish_non_exhaustive() + } +} + impl UnifiedStorage { /// Create a new unified storage instance. pub const fn new(hot: H, cold: ColdStorage) -> Self { From 354ae29886b06e1f3414e0972127e5c2a152fb64 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 14 May 2026 02:49:14 -0400 Subject: [PATCH 14/14] cold: convert ErasedBackend from type alias to newtype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old alias `pub type ErasedBackend = Arc` exposed the trait-object lifetime to inference. When an `ErasedBackend` was captured into a future passed to a spawner, rustc invented a fresh `'0` for the dyn object and required `for<'0> Arc: ColdStorageRead`, which the `'static`-bounded impl could not satisfy. A newtype keeps the dyn lifetime out of the surface type so HRTB resolution is trivial. Public API: - `ErasedBackend::new(b)` — erase a concrete backend. - `ErasedBackend::from_arc(arc)` — wrap an existing `Arc`. - `ErasedBackend::as_dyn()` / `into_arc()` — interop accessors. - `Clone` + hand-written `Debug`. `ColdStorage::new_erased` and `conformance_erased` rewired through `ErasedBackend::new`. Module docs explain the resolution problem and the recursion-hazard note now references `self.0` instead of `**self`. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cold/src/conformance.rs | 14 ++-- crates/cold/src/dyn_backend.rs | 113 +++++++++++++++++++++++++-------- crates/cold/src/handle.rs | 4 +- 3 files changed, 94 insertions(+), 37 deletions(-) diff --git a/crates/cold/src/conformance.rs b/crates/cold/src/conformance.rs index 2ee3b30..8102430 100644 --- a/crates/cold/src/conformance.rs +++ b/crates/cold/src/conformance.rs @@ -5,8 +5,8 @@ //! a custom backend, call the test functions with your backend instance. use crate::{ - BlockData, ColdResult, ColdStorage, ColdStorageBackend, ColdStorageError, - DynColdStorageBackend, Filter, HeaderSpecifier, ReceiptSpecifier, RpcLog, TransactionSpecifier, + BlockData, ColdResult, ColdStorage, ColdStorageBackend, ColdStorageError, ErasedBackend, + Filter, HeaderSpecifier, ReceiptSpecifier, RpcLog, TransactionSpecifier, }; use alloy::{ consensus::{ @@ -17,7 +17,7 @@ use alloy::{ }, }; use signet_storage_types::{Receipt, RecoveredTx, TransactionSigned}; -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use tokio_stream::StreamExt; use tokio_util::sync::CancellationToken; @@ -44,13 +44,13 @@ pub async fn conformance(backend: B) -> ColdResult<()> { } /// Run the conformance suite against `backend` after erasing it -/// through [`DynColdStorageBackend`]. +/// through [`crate::DynColdStorageBackend`]. /// /// Exercises the same contract as [`conformance`] but routes every -/// call through `Arc`, validating that -/// the erased dispatch path upholds the trait contract. +/// call through [`ErasedBackend`], validating that the erased +/// dispatch path upholds the trait contract. pub async fn conformance_erased(backend: B) -> ColdResult<()> { - let erased: Arc = Arc::new(backend); + let erased = ErasedBackend::new(backend); let cancel = CancellationToken::new(); let handle = ColdStorage::new(erased, cancel.clone()); test_empty_storage(&handle).await?; diff --git a/crates/cold/src/dyn_backend.rs b/crates/cold/src/dyn_backend.rs index fb14e9c..6c1bec1 100644 --- a/crates/cold/src/dyn_backend.rs +++ b/crates/cold/src/dyn_backend.rs @@ -14,6 +14,17 @@ //! signatures. Backends should implement [`ColdStorageBackend`] — the //! blanket impl handles this trait. //! +//! # Why a Newtype, Not a Type Alias +//! +//! [`ErasedBackend`] is a newtype wrapping `Arc` +//! rather than a plain alias. A type alias exposes the dyn trait-object +//! lifetime to trait resolution; when an `ErasedBackend` is captured +//! into a spawned future, rustc invents a fresh `'0` lifetime for the +//! dyn object and asks `for<'0> Arc: +//! ColdStorageRead`. The `'static`-bounded impl does not satisfy this +//! HRTB and downstream `Send` checks fail. A concrete newtype has no +//! dyn lifetime in its surface type, so resolution is trivial. +//! //! # Filter Cloning on the Erased Path //! //! The [`ColdStorageRead`] impl for [`ErasedBackend`] clones the @@ -26,10 +37,10 @@ //! # Maintainer Note: Recursion Hazard for Borrowed Arguments //! //! Any method on the [`ErasedBackend`] impls that cannot use the -//! direct `(**self).dyn_(...)` form (because a borrowed argument +//! direct `self.0.dyn_(...)` form (because a borrowed argument //! forces it through a `self.clone()` + `async move` bridge) MUST //! dispatch via qualified path on the inner trait object, e.g. -//! `DynColdStorageBackend::dyn_(this.as_ref(), ...)`. +//! `DynColdStorageBackend::dyn_(this.0.as_ref(), ...)`. //! //! Writing `this.dyn_(...)` on a cloned [`ErasedBackend`] //! resolves to the blanket impl (`ErasedBackend: ColdStorageBackend` @@ -59,8 +70,53 @@ pub type StorageFuture<'a, T> = Pin + Send + 'a>>; /// /// This is the default `B` for [`ColdStorage`](crate::ColdStorage): a /// handle written as plain `ColdStorage` uses this backend. Construct -/// one with [`ColdStorage::new_erased`](crate::ColdStorage::new_erased). -pub type ErasedBackend = Arc; +/// one with [`ErasedBackend::new`] or +/// [`ColdStorage::new_erased`](crate::ColdStorage::new_erased). +/// +/// # Why a Newtype +/// +/// Wrapping the `Arc` in a struct keeps the trait-object +/// lifetime out of the public type signature. See the module-level +/// docs for the HRTB resolution problem this avoids. +pub struct ErasedBackend(Arc); + +impl ErasedBackend { + /// Erase a concrete backend behind `Arc`. + pub fn new(backend: B) -> Self { + Self(Arc::new(backend)) + } + + /// Wrap an existing trait object. + /// + /// Prefer [`ErasedBackend::new`] for concrete backends. Use this + /// only when you already hold an `Arc`, + /// e.g. when bridging from another type-erased channel. + pub const fn from_arc(arc: Arc) -> Self { + Self(arc) + } + + /// Borrow the inner trait object. + pub fn as_dyn(&self) -> &(dyn DynColdStorageBackend + 'static) { + &*self.0 + } + + /// Consume the newtype and return the inner `Arc`. + pub fn into_arc(self) -> Arc { + self.0 + } +} + +impl Clone for ErasedBackend { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +impl std::fmt::Debug for ErasedBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("ErasedBackend").finish() + } +} /// Object-safe mirror of [`ColdStorageBackend`]. Auto-implemented by a /// blanket impl over every `B: ColdStorageBackend`; do not implement @@ -296,74 +352,74 @@ impl ColdStorageRead for ErasedBackend { &self, spec: HeaderSpecifier, ) -> impl Future>> + Send { - (**self).dyn_get_header(spec) + self.0.dyn_get_header(spec) } fn get_headers( &self, specs: Vec, ) -> impl Future>>> + Send { - (**self).dyn_get_headers(specs) + self.0.dyn_get_headers(specs) } fn get_transaction( &self, spec: TransactionSpecifier, ) -> impl Future>>> + Send { - (**self).dyn_get_transaction(spec) + self.0.dyn_get_transaction(spec) } fn get_transactions_in_block( &self, block: BlockNumber, ) -> impl Future>> + Send { - (**self).dyn_get_transactions_in_block(block) + self.0.dyn_get_transactions_in_block(block) } fn get_transaction_count( &self, block: BlockNumber, ) -> impl Future> + Send { - (**self).dyn_get_transaction_count(block) + self.0.dyn_get_transaction_count(block) } fn get_receipt( &self, spec: ReceiptSpecifier, ) -> impl Future>> + Send { - (**self).dyn_get_receipt(spec) + self.0.dyn_get_receipt(spec) } fn get_receipts_in_block( &self, block: BlockNumber, ) -> impl Future>> + Send { - (**self).dyn_get_receipts_in_block(block) + self.0.dyn_get_receipts_in_block(block) } fn get_signet_events( &self, spec: SignetEventsSpecifier, ) -> impl Future>> + Send { - (**self).dyn_get_signet_events(spec) + self.0.dyn_get_signet_events(spec) } fn get_zenith_header( &self, spec: ZenithHeaderSpecifier, ) -> impl Future>> + Send { - (**self).dyn_get_zenith_header(spec) + self.0.dyn_get_zenith_header(spec) } fn get_zenith_headers( &self, spec: ZenithHeaderSpecifier, ) -> impl Future>> + Send { - (**self).dyn_get_zenith_headers(spec) + self.0.dyn_get_zenith_headers(spec) } fn get_latest_block(&self) -> impl Future>> + Send { - (**self).dyn_get_latest_block() + self.0.dyn_get_latest_block() } fn get_logs( @@ -374,9 +430,10 @@ impl ColdStorageRead for ErasedBackend { let this = self.clone(); let filter = filter.clone(); // Call dyn_get_logs via the inner trait object directly (not through - // the Arc's blanket DynColdStorageBackend impl), which would re-enter - // ColdStorageRead::get_logs on ErasedBackend and recurse infinitely. - async move { DynColdStorageBackend::dyn_get_logs(this.as_ref(), &filter, max_logs).await } + // the newtype's blanket DynColdStorageBackend impl), which would + // re-enter ColdStorageRead::get_logs on ErasedBackend and recurse + // infinitely. + async move { DynColdStorageBackend::dyn_get_logs(this.0.as_ref(), &filter, max_logs).await } } fn produce_log_stream( @@ -386,43 +443,43 @@ impl ColdStorageRead for ErasedBackend { ) -> impl Future + Send { let this = self.clone(); let filter = filter.clone(); - // Same recursion hazard as `get_logs` above — call through - // `as_ref()` + qualified path so dispatch lands on the inner - // trait object's vtable, not the Arc's blanket impl. + // Same recursion hazard as `get_logs` above — call through the + // inner Arc + qualified path so dispatch lands on the trait + // object's vtable, not the newtype's blanket impl. async move { - DynColdStorageBackend::dyn_produce_log_stream(this.as_ref(), &filter, params).await + DynColdStorageBackend::dyn_produce_log_stream(this.0.as_ref(), &filter, params).await } } } impl ColdStorageWrite for ErasedBackend { fn append_block(&self, data: BlockData) -> impl Future> + Send { - (**self).dyn_append_block(data) + self.0.dyn_append_block(data) } fn append_blocks(&self, data: Vec) -> impl Future> + Send { - (**self).dyn_append_blocks(data) + self.0.dyn_append_blocks(data) } fn truncate_above(&self, block: BlockNumber) -> impl Future> + Send { - (**self).dyn_truncate_above(block) + self.0.dyn_truncate_above(block) } } impl ColdStorageBackend for ErasedBackend { fn read_timeout(&self) -> Option { - (**self).dyn_read_timeout() + self.0.dyn_read_timeout() } fn write_timeout(&self) -> Option { - (**self).dyn_write_timeout() + self.0.dyn_write_timeout() } fn drain_above( &self, block: BlockNumber, ) -> impl Future>>> + Send { - (**self).dyn_drain_above(block) + self.0.dyn_drain_above(block) } } diff --git a/crates/cold/src/handle.rs b/crates/cold/src/handle.rs index 46092d4..97d0c2a 100644 --- a/crates/cold/src/handle.rs +++ b/crates/cold/src/handle.rs @@ -700,12 +700,12 @@ impl ColdStorage { /// Wraps `backend` in [`ErasedBackend`] so the resulting handle /// has no `B` type parameter to propagate through downstream /// signatures. Equivalent to - /// `ColdStorage::new(Arc::new(backend) as ErasedBackend, cancel)`. + /// `ColdStorage::new(ErasedBackend::new(backend), cancel)`. /// /// Choose this constructor when you want runtime swappability of /// the backend; use [`new`](Self::new) directly for fully /// monomorphized call sites. pub fn new_erased(backend: B, cancel: CancellationToken) -> Self { - Self::new(Arc::new(backend) as ErasedBackend, cancel) + Self::new(ErasedBackend::new(backend), cancel) } }