From 7b80e1e152976a8dc866a6a857c7dff6fc41888f Mon Sep 17 00:00:00 2001 From: James Date: Fri, 13 Mar 2026 12:46:28 -0400 Subject: [PATCH 1/7] feat: add signet-host-reth crate with RethHostNotifier Introduces the signet-host-reth crate which isolates all reth ExEx dependencies behind the HostNotifier trait. This includes: - RethChain: owning wrapper around reth's Chain implementing Extractable with O(1) metadata accessors via inlined transmute logic - RethHostNotifier: ExEx-backed implementation of HostNotifier that handles hash resolution internally - RethAliasOracle/Factory: moved from signet-node for reuse - RPC config helpers: rpc_config_from_args and serve_config_from_args - decompose_exex_context: splits ExExContext into notifier + config - RethHostError: proper error type satisfying core::error::Error Also re-exports RecoveredBlockShim from signet-blobber. Co-Authored-By: Claude Opus 4.6 --- Cargo.toml | 1 + crates/host-reth/Cargo.toml | 30 ++++++ crates/host-reth/README.md | 3 + crates/host-reth/src/alias.rs | 79 +++++++++++++++ crates/host-reth/src/chain.rs | 57 +++++++++++ crates/host-reth/src/config.rs | 34 +++++++ crates/host-reth/src/error.rs | 24 +++++ crates/host-reth/src/lib.rs | 26 +++++ crates/host-reth/src/notifier.rs | 167 +++++++++++++++++++++++++++++++ 9 files changed, 421 insertions(+) create mode 100644 crates/host-reth/Cargo.toml create mode 100644 crates/host-reth/README.md create mode 100644 crates/host-reth/src/alias.rs create mode 100644 crates/host-reth/src/chain.rs create mode 100644 crates/host-reth/src/config.rs create mode 100644 crates/host-reth/src/error.rs create mode 100644 crates/host-reth/src/lib.rs create mode 100644 crates/host-reth/src/notifier.rs diff --git a/Cargo.toml b/Cargo.toml index 294c861..9266e64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ incremental = false signet-blobber = { version = "0.16.0-rc.7", path = "crates/blobber" } signet-block-processor = { version = "0.16.0-rc.7", path = "crates/block-processor" } signet-genesis = { version = "0.16.0-rc.7", path = "crates/genesis" } +signet-host-reth = { version = "0.16.0-rc.7", path = "crates/host-reth" } signet-node = { version = "0.16.0-rc.7", path = "crates/node" } signet-node-config = { version = "0.16.0-rc.7", path = "crates/node-config" } signet-node-tests = { version = "0.16.0-rc.7", path = "crates/node-tests" } diff --git a/crates/host-reth/Cargo.toml b/crates/host-reth/Cargo.toml new file mode 100644 index 0000000..b9efe47 --- /dev/null +++ b/crates/host-reth/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "signet-host-reth" +description = "Reth ExEx implementation of the `HostNotifier` trait for signet-node." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +signet-node-types.workspace = true +signet-blobber.workspace = true +signet-extract.workspace = true +signet-rpc.workspace = true +signet-block-processor.workspace = true +signet-types.workspace = true + +alloy.workspace = true +reth.workspace = true +reth-exex.workspace = true +reth-node-api.workspace = true +reth-stages-types.workspace = true + +eyre.workspace = true +futures-util.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true diff --git a/crates/host-reth/README.md b/crates/host-reth/README.md new file mode 100644 index 0000000..c381b88 --- /dev/null +++ b/crates/host-reth/README.md @@ -0,0 +1,3 @@ +# signet-host-reth + +Reth ExEx implementation of the `HostNotifier` trait for signet-node. diff --git a/crates/host-reth/src/alias.rs b/crates/host-reth/src/alias.rs new file mode 100644 index 0000000..8a06344 --- /dev/null +++ b/crates/host-reth/src/alias.rs @@ -0,0 +1,79 @@ +use alloy::{consensus::constants::KECCAK_EMPTY, primitives::Address}; +use core::fmt; +use eyre::OptionExt; +use reth::providers::{StateProviderBox, StateProviderFactory}; +use signet_block_processor::{AliasOracle, AliasOracleFactory}; + +/// An [`AliasOracle`] backed by a reth [`StateProviderBox`]. +/// +/// Checks whether an address has non-delegation bytecode, indicating it +/// should be aliased during transaction processing. +pub struct RethAliasOracle(StateProviderBox); + +impl fmt::Debug for RethAliasOracle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RethAliasOracle").finish_non_exhaustive() + } +} + +impl AliasOracle for RethAliasOracle { + fn should_alias(&self, address: Address) -> eyre::Result { + // No account at this address. + let Some(acct) = self.0.basic_account(&address)? else { return Ok(false) }; + // Get the bytecode hash for this account. + let bch = match acct.bytecode_hash { + Some(hash) => hash, + // No bytecode hash; not a contract. + None => return Ok(false), + }; + // No code at this address. + if bch == KECCAK_EMPTY { + return Ok(false); + } + // Fetch the code associated with this bytecode hash. + let code = self + .0 + .bytecode_by_hash(&bch)? + .ok_or_eyre("code not found. This indicates a corrupted database")?; + + // If not a 7702 delegation contract, alias it. + Ok(!code.is_eip7702()) + } +} + +/// An [`AliasOracleFactory`] backed by a `Box`. +/// +/// Creates [`RethAliasOracle`] instances from the latest host chain state. +pub struct RethAliasOracleFactory(Box); + +impl fmt::Debug for RethAliasOracleFactory { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RethAliasOracleFactory").finish_non_exhaustive() + } +} + +impl RethAliasOracleFactory { + /// Create a new [`RethAliasOracleFactory`] from a boxed state provider + /// factory. + pub fn new(provider: Box) -> Self { + Self(provider) + } +} + +impl AliasOracleFactory for RethAliasOracleFactory { + type Oracle = RethAliasOracle; + + fn create(&self) -> eyre::Result { + // NB: This becomes a problem if anyone ever birthday attacks a + // contract/EOA pair (c.f. EIP-3607). In practice this is unlikely to + // happen for the foreseeable future, and if it does we can revisit + // this decision. + // We considered taking the host height as an argument to this method, + // but this would require all nodes to be archive nodes in order to + // sync, which is less than ideal + self.0 + .state_by_block_number_or_tag(alloy::eips::BlockNumberOrTag::Latest) + .map(RethAliasOracle) + .map_err(Into::into) + } +} diff --git a/crates/host-reth/src/chain.rs b/crates/host-reth/src/chain.rs new file mode 100644 index 0000000..c368c5a --- /dev/null +++ b/crates/host-reth/src/chain.rs @@ -0,0 +1,57 @@ +use alloy::{consensus::Block, consensus::BlockHeader}; +use reth::primitives::{EthPrimitives, RecoveredBlock}; +use reth::providers::Chain; +use signet_blobber::RecoveredBlockShim; +use signet_extract::Extractable; +use signet_types::primitives::TransactionSigned; +use std::sync::Arc; + +/// Reth's recovered block type, aliased for readability. +type RethRecovered = RecoveredBlock>; + +/// An owning wrapper around reth's [`Chain`] that implements [`Extractable`] +/// with O(1) metadata accessors. +#[derive(Debug)] +pub struct RethChain { + inner: Arc>, +} + +impl RethChain { + /// Wrap a reth chain. + pub const fn new(chain: Arc>) -> Self { + Self { inner: chain } + } +} + +impl Extractable for RethChain { + type Block = RecoveredBlockShim; + type Receipt = reth::primitives::Receipt; + + fn blocks_and_receipts(&self) -> impl Iterator)> { + self.inner.blocks_and_receipts().map(|(block, receipts)| { + // SAFETY: `RecoveredBlockShim` is `#[repr(transparent)]` over + // `RethRecovered`, so these types have identical memory layouts. + // The lifetime of the reference is tied to `self.inner` (the + // `Arc`), which outlives the returned iterator. + let block = + unsafe { std::mem::transmute::<&RethRecovered, &RecoveredBlockShim>(block) }; + (block, receipts) + }) + } + + fn first_number(&self) -> Option { + Some(self.inner.first().number()) + } + + fn tip_number(&self) -> Option { + Some(self.inner.tip().number()) + } + + fn len(&self) -> usize { + self.inner.len() + } + + fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} diff --git a/crates/host-reth/src/config.rs b/crates/host-reth/src/config.rs new file mode 100644 index 0000000..f5334f7 --- /dev/null +++ b/crates/host-reth/src/config.rs @@ -0,0 +1,34 @@ +use reth::args::RpcServerArgs; +use signet_rpc::{ServeConfig, StorageRpcConfig}; +use std::net::SocketAddr; + +/// Extract [`StorageRpcConfig`] values from reth's host RPC settings. +/// +/// Fields with no reth equivalent retain their defaults. +pub fn rpc_config_from_args(args: &RpcServerArgs) -> StorageRpcConfig { + let gpo = &args.gas_price_oracle; + StorageRpcConfig::builder() + .rpc_gas_cap(args.rpc_gas_cap) + .max_tracing_requests(args.rpc_max_tracing_requests) + .gas_oracle_block_count(gpo.blocks as u64) + .gas_oracle_percentile(gpo.percentile as f64) + .ignore_price(Some(gpo.ignore_price as u128)) + .max_price(Some(gpo.max_price as u128)) + .build() +} + +/// Convert reth [`RpcServerArgs`] into a reth-free [`ServeConfig`]. +pub fn serve_config_from_args(args: &RpcServerArgs) -> ServeConfig { + let http = + if args.http { vec![SocketAddr::from((args.http_addr, args.http_port))] } else { vec![] }; + let ws = if args.ws { vec![SocketAddr::from((args.ws_addr, args.ws_port))] } else { vec![] }; + let ipc = if !args.ipcdisable { Some(args.ipcpath.clone()) } else { None }; + + ServeConfig { + http, + http_cors: args.http_corsdomain.clone(), + ws, + ws_cors: args.ws_allowed_origins.clone(), + ipc, + } +} diff --git a/crates/host-reth/src/error.rs b/crates/host-reth/src/error.rs new file mode 100644 index 0000000..1391db2 --- /dev/null +++ b/crates/host-reth/src/error.rs @@ -0,0 +1,24 @@ +use reth_exex::ExExEvent; + +/// Errors from the [`RethHostNotifier`](crate::RethHostNotifier). +#[derive(Debug, thiserror::Error)] +pub enum RethHostError { + /// A notification stream error forwarded from reth. + #[error("notification stream error: {0}")] + Notification(#[source] Box), + /// The provider failed to look up a header or block tag. + #[error("provider error: {0}")] + Provider(#[from] reth::providers::ProviderError), + /// Failed to send an ExEx event back to the host. + #[error("failed to send ExEx event")] + EventSend(#[from] tokio::sync::mpsc::error::SendError), + /// A required header was missing from the provider. + #[error("missing header for block {0}")] + MissingHeader(u64), +} + +impl From for RethHostError { + fn from(e: eyre::Report) -> Self { + Self::Notification(e.into()) + } +} diff --git a/crates/host-reth/src/lib.rs b/crates/host-reth/src/lib.rs new file mode 100644 index 0000000..3ff1e46 --- /dev/null +++ b/crates/host-reth/src/lib.rs @@ -0,0 +1,26 @@ +#![doc = include_str!("../README.md")] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod alias; +pub use alias::{RethAliasOracle, RethAliasOracleFactory}; +mod error; +pub use error::RethHostError; + +mod chain; +pub use chain::RethChain; + +mod config; +pub use config::{rpc_config_from_args, serve_config_from_args}; + +mod notifier; +pub use notifier::{DecomposedContext, RethHostNotifier, decompose_exex_context}; diff --git a/crates/host-reth/src/notifier.rs b/crates/host-reth/src/notifier.rs new file mode 100644 index 0000000..6d0cab0 --- /dev/null +++ b/crates/host-reth/src/notifier.rs @@ -0,0 +1,167 @@ +use crate::{ + RethChain, + config::{rpc_config_from_args, serve_config_from_args}, + error::RethHostError, +}; +use alloy::eips::BlockNumHash; +use futures_util::StreamExt; +use reth::{ + chainspec::EthChainSpec, + primitives::EthPrimitives, + providers::{BlockIdReader, BlockReader, HeaderProvider}, +}; +use reth_exex::{ExExContext, ExExEvent, ExExNotifications, ExExNotificationsStream}; +use reth_node_api::{FullNodeComponents, NodeTypes}; +use reth_stages_types::ExecutionStageThresholds; +use signet_node_types::{HostNotification, HostNotificationKind, HostNotifier}; +use signet_rpc::{ServeConfig, StorageRpcConfig}; +use std::sync::Arc; +use tracing::debug; + +/// Reth ExEx implementation of [`HostNotifier`]. +/// +/// Wraps reth's notification stream, provider, and event sender. All hash +/// resolution happens internally — consumers only work with block numbers. +pub struct RethHostNotifier { + notifications: ExExNotifications, + provider: Host::Provider, + events: tokio::sync::mpsc::UnboundedSender, +} + +impl core::fmt::Debug for RethHostNotifier { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("RethHostNotifier").finish_non_exhaustive() + } +} + +/// The output of [`decompose_exex_context`]. +pub struct DecomposedContext { + /// The host notifier adapter. + pub notifier: RethHostNotifier, + /// Plain RPC serve config. + pub serve_config: ServeConfig, + /// Plain RPC storage config. + pub rpc_config: StorageRpcConfig, + /// The transaction pool, for blob cacher construction. + pub pool: Host::Pool, + /// The chain name, for tracing. + pub chain_name: String, +} + +impl core::fmt::Debug for DecomposedContext { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("DecomposedContext") + .field("chain_name", &self.chain_name) + .finish_non_exhaustive() + } +} + +/// Decompose a reth [`ExExContext`] into a [`RethHostNotifier`] and +/// associated configuration values. +/// +/// This splits the ExEx context into: +/// - A [`RethHostNotifier`] (implements [`HostNotifier`]) +/// - A [`ServeConfig`] (plain RPC server config) +/// - A [`StorageRpcConfig`] (gas oracle settings) +/// - The transaction pool handle +/// - A chain name for tracing +pub fn decompose_exex_context(ctx: ExExContext) -> DecomposedContext +where + Host: FullNodeComponents, + Host::Types: NodeTypes, +{ + let chain_name = ctx.config.chain.chain().to_string(); + let serve_config = serve_config_from_args(&ctx.config.rpc); + let rpc_config = rpc_config_from_args(&ctx.config.rpc); + let pool = ctx.pool().clone(); + let provider = ctx.provider().clone(); + + let notifier = + RethHostNotifier { notifications: ctx.notifications, provider, events: ctx.events }; + + DecomposedContext { notifier, serve_config, rpc_config, pool, chain_name } +} + +impl HostNotifier for RethHostNotifier +where + Host: FullNodeComponents, + Host::Types: NodeTypes, +{ + type Chain = RethChain; + type Error = RethHostError; + + async fn next_notification( + &mut self, + ) -> Option, Self::Error>> { + let notification = self.notifications.next().await?; + let notification = match notification { + Ok(n) => n, + Err(e) => return Some(Err(e.into())), + }; + + // Read safe/finalized from the provider at notification time. + let safe_block_number = self.provider.safe_block_number().ok().flatten(); + let finalized_block_number = self.provider.finalized_block_number().ok().flatten(); + + let kind = match notification { + reth_exex::ExExNotification::ChainCommitted { new } => { + HostNotificationKind::ChainCommitted { new: Arc::new(RethChain::new(new)) } + } + reth_exex::ExExNotification::ChainReverted { old } => { + HostNotificationKind::ChainReverted { old: Arc::new(RethChain::new(old)) } + } + reth_exex::ExExNotification::ChainReorged { old, new } => { + HostNotificationKind::ChainReorged { + old: Arc::new(RethChain::new(old)), + new: Arc::new(RethChain::new(new)), + } + } + }; + + Some(Ok(HostNotification { kind, safe_block_number, finalized_block_number })) + } + + fn set_head(&mut self, block_number: u64) { + let block = self + .provider + .block_by_number(block_number) + .expect("failed to look up block for set_head"); + + let head = match block { + Some(b) => b.num_hash_slow(), + None => { + debug!(block_number, "block not found for set_head, falling back to genesis"); + let genesis = self + .provider + .block_by_number(0) + .expect("failed to look up genesis block") + .expect("genesis block missing"); + genesis.num_hash_slow() + } + }; + + let exex_head = reth_exex::ExExHead { block: head }; + self.notifications.set_with_head(exex_head); + } + + fn set_backfill_thresholds(&mut self, max_blocks: Option) { + if let Some(max_blocks) = max_blocks { + self.notifications.set_backfill_thresholds(ExecutionStageThresholds { + max_blocks: Some(max_blocks), + ..Default::default() + }); + debug!(max_blocks, "configured backfill thresholds"); + } + } + + fn send_finished_height(&self, block_number: u64) -> Result<(), Self::Error> { + let header = self + .provider + .sealed_header(block_number)? + .ok_or(RethHostError::MissingHeader(block_number))?; + + let hash = header.hash(); + self.events.send(ExExEvent::FinishedHeight(BlockNumHash { number: block_number, hash }))?; + Ok(()) + } +} From 79805e760600568f3462777920e8519c22c17bec Mon Sep 17 00:00:00 2001 From: James Date: Fri, 13 Mar 2026 13:39:08 -0400 Subject: [PATCH 2/7] refactor(host-reth): use sealed_header instead of num_hash_slow in set_head Eliminates num_hash_slow() calls by using the provider's sealed_header method, which returns pre-cached hashes instead of recomputing from RLP. Co-Authored-By: Claude Opus 4.6 --- crates/host-reth/src/notifier.rs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/crates/host-reth/src/notifier.rs b/crates/host-reth/src/notifier.rs index 6d0cab0..ed2fa3c 100644 --- a/crates/host-reth/src/notifier.rs +++ b/crates/host-reth/src/notifier.rs @@ -8,7 +8,7 @@ use futures_util::StreamExt; use reth::{ chainspec::EthChainSpec, primitives::EthPrimitives, - providers::{BlockIdReader, BlockReader, HeaderProvider}, + providers::{BlockIdReader, HeaderProvider}, }; use reth_exex::{ExExContext, ExExEvent, ExExNotifications, ExExNotificationsStream}; use reth_node_api::{FullNodeComponents, NodeTypes}; @@ -122,26 +122,22 @@ where } fn set_head(&mut self, block_number: u64) { - let block = self + let head = self .provider - .block_by_number(block_number) - .expect("failed to look up block for set_head"); - - let head = match block { - Some(b) => b.num_hash_slow(), - None => { - debug!(block_number, "block not found for set_head, falling back to genesis"); + .sealed_header(block_number) + .expect("failed to look up header for set_head") + .map(|h| BlockNumHash { number: block_number, hash: h.hash() }) + .unwrap_or_else(|| { + debug!(block_number, "header not found for set_head, falling back to genesis"); let genesis = self .provider - .block_by_number(0) - .expect("failed to look up genesis block") - .expect("genesis block missing"); - genesis.num_hash_slow() - } - }; + .sealed_header(0) + .expect("failed to look up genesis header") + .expect("genesis header missing"); + BlockNumHash { number: 0, hash: genesis.hash() } + }); - let exex_head = reth_exex::ExExHead { block: head }; - self.notifications.set_with_head(exex_head); + self.notifications.set_with_head(reth_exex::ExExHead { block: head }); } fn set_backfill_thresholds(&mut self, max_blocks: Option) { From 156fe47b79ac3ab8ebce524327cf9e18ba815e12 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 13 Mar 2026 13:09:28 -0400 Subject: [PATCH 3/7] test: update signet-node-tests for HostNotifier API Use decompose_exex_context to construct RethHostNotifier and pass it through the new builder API. Co-Authored-By: Claude Opus 4.6 --- crates/node-tests/Cargo.toml | 3 +++ crates/node-tests/src/context.rs | 21 ++++++++++++++++++++- crates/node-tests/tests/db.rs | 25 +++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/crates/node-tests/Cargo.toml b/crates/node-tests/Cargo.toml index a6060f0..bc3ca25 100644 --- a/crates/node-tests/Cargo.toml +++ b/crates/node-tests/Cargo.toml @@ -12,10 +12,12 @@ repository.workspace = true signet-node.workspace = true signet-node-config = { workspace = true, features = ["test_utils"] } +signet-blobber.workspace = true signet-cold = { workspace = true, features = ["in-memory"] } signet-constants.workspace = true signet-evm.workspace = true signet-genesis.workspace = true +signet-host-reth.workspace = true signet-hot = { workspace = true, features = ["in-memory"] } signet-storage.workspace = true signet-storage-types.workspace = true @@ -31,6 +33,7 @@ reth-exex-test-utils.workspace = true reth-node-api.workspace = true eyre.workspace = true +reqwest.workspace = true tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/crates/node-tests/src/context.rs b/crates/node-tests/src/context.rs index 190ac9f..256df34 100644 --- a/crates/node-tests/src/context.rs +++ b/crates/node-tests/src/context.rs @@ -18,6 +18,7 @@ use reth::transaction_pool::{TransactionOrigin, TransactionPool, test_utils::Moc use reth_exex_test_utils::{Adapter, TestExExHandle}; use reth_node_api::FullNodeComponents; use signet_cold::{ColdStorageReadHandle, mem::MemColdBackend}; +use signet_host_reth::decompose_exex_context; use signet_hot::{ db::{HotDbRead, UnsafeDbWrite}, mem::MemKv, @@ -104,6 +105,9 @@ impl SignetTestContext { let (ctx, handle) = reth_exex_test_utils::test_exex_context().await.unwrap(); let components = ctx.components.clone(); + // Decompose the ExEx context into notifier + configs + let decomposed = decompose_exex_context(ctx); + // set up Signet Node storage let constants = cfg.constants().unwrap(); @@ -148,10 +152,25 @@ impl SignetTestContext { let alias_oracle: Arc>> = Arc::new(Mutex::new(HashSet::default())); + // Build the blob cacher from the decomposed pool + let blob_cacher = signet_blobber::BlobFetcher::builder() + .with_config(cfg.block_extractor()) + .unwrap() + .with_pool(decomposed.pool) + .with_client(reqwest::Client::new()) + .build_cache() + .unwrap() + .spawn::(); + let (node, mut node_status) = SignetNodeBuilder::new(cfg.clone()) - .with_ctx(ctx) + .with_notifier(decomposed.notifier) .with_storage(Arc::clone(&storage)) .with_alias_oracle(Arc::clone(&alias_oracle)) + .with_chain_name(decomposed.chain_name) + .with_blob_cacher(blob_cacher) + .with_serve_config(decomposed.serve_config) + .with_rpc_config(decomposed.rpc_config) + .with_client(reqwest::Client::new()) .build() .await .unwrap(); diff --git a/crates/node-tests/tests/db.rs b/crates/node-tests/tests/db.rs index 9788342..df1dc17 100644 --- a/crates/node-tests/tests/db.rs +++ b/crates/node-tests/tests/db.rs @@ -1,5 +1,7 @@ +use alloy::primitives::map::HashSet; use serial_test::serial; use signet_cold::mem::MemColdBackend; +use signet_host_reth::decompose_exex_context; use signet_hot::{ db::{HotDbRead, UnsafeDbWrite}, mem::MemKv, @@ -7,7 +9,7 @@ use signet_hot::{ use signet_node::SignetNodeBuilder; use signet_node_config::test_utils::test_config; use signet_storage::{CancellationToken, HistoryRead, HistoryWrite, HotKv, UnifiedStorage}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; #[serial] #[tokio::test] @@ -19,6 +21,8 @@ async fn test_genesis() { let chain_spec: Arc<_> = cfg.chain_spec().clone(); assert_eq!(chain_spec.genesis().config.chain_id, consts.unwrap().ru_chain_id()); + let decomposed = decompose_exex_context(ctx); + let cancel_token = CancellationToken::new(); let hot = MemKv::new(); { @@ -30,9 +34,26 @@ async fn test_genesis() { let storage = Arc::new(UnifiedStorage::spawn(hot, MemColdBackend::new(), cancel_token.clone())); + let blob_cacher = signet_blobber::BlobFetcher::builder() + .with_config(cfg.block_extractor()) + .unwrap() + .with_pool(decomposed.pool) + .with_client(reqwest::Client::new()) + .build_cache() + .unwrap() + .spawn::(); + + let alias_oracle: Arc>> = Arc::new(Mutex::new(HashSet::default())); + let (_, _) = SignetNodeBuilder::new(cfg.clone()) - .with_ctx(ctx) + .with_notifier(decomposed.notifier) .with_storage(Arc::clone(&storage)) + .with_alias_oracle(Arc::clone(&alias_oracle)) + .with_chain_name(decomposed.chain_name) + .with_blob_cacher(blob_cacher) + .with_serve_config(decomposed.serve_config) + .with_rpc_config(decomposed.rpc_config) + .with_client(reqwest::Client::new()) .build() .await .unwrap(); From 1d092c427663da55bbc17ae70c3238b1c557ecde Mon Sep 17 00:00:00 2001 From: James Date: Fri, 13 Mar 2026 13:13:21 -0400 Subject: [PATCH 4/7] fix(node-tests): build ServeConfig from Signet test config The decomposed reth context has IPC/HTTP disabled by default. Tests need the Signet-configured IPC path for RPC communication. Co-Authored-By: Claude Opus 4.6 --- crates/node-tests/Cargo.toml | 1 + crates/node-tests/src/context.rs | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/node-tests/Cargo.toml b/crates/node-tests/Cargo.toml index bc3ca25..358984c 100644 --- a/crates/node-tests/Cargo.toml +++ b/crates/node-tests/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true [dependencies] signet-node.workspace = true signet-node-config = { workspace = true, features = ["test_utils"] } +signet-rpc.workspace = true signet-blobber.workspace = true signet-cold = { workspace = true, features = ["in-memory"] } diff --git a/crates/node-tests/src/context.rs b/crates/node-tests/src/context.rs index 256df34..907ab0c 100644 --- a/crates/node-tests/src/context.rs +++ b/crates/node-tests/src/context.rs @@ -162,13 +162,23 @@ impl SignetTestContext { .unwrap() .spawn::(); + // Build serve config from the Signet test config rather than the + // reth defaults (which have IPC/HTTP disabled). + let serve_config = signet_rpc::ServeConfig { + http: vec![], + http_cors: None, + ws: vec![], + ws_cors: None, + ipc: cfg.ipc_endpoint().map(ToOwned::to_owned), + }; + let (node, mut node_status) = SignetNodeBuilder::new(cfg.clone()) .with_notifier(decomposed.notifier) .with_storage(Arc::clone(&storage)) .with_alias_oracle(Arc::clone(&alias_oracle)) .with_chain_name(decomposed.chain_name) .with_blob_cacher(blob_cacher) - .with_serve_config(decomposed.serve_config) + .with_serve_config(serve_config) .with_rpc_config(decomposed.rpc_config) .with_client(reqwest::Client::new()) .build() From b409876fd1002dab215cd3cf4311e43a42c49c5c Mon Sep 17 00:00:00 2001 From: James Date: Fri, 13 Mar 2026 15:53:43 -0400 Subject: [PATCH 5/7] fix: adapt host-reth to async AliasOracle and BlockAndReceipts API - Update RethAliasOracle::should_alias to async RPITIT signature - Update RethChain::blocks_and_receipts to return BlockAndReceipts - Remove orphan alias.rs from signet-node (now in host-reth) Co-Authored-By: Claude Opus 4.6 --- crates/host-reth/src/alias.rs | 14 +++-- crates/host-reth/src/chain.rs | 8 ++- crates/node/src/alias.rs | 114 ---------------------------------- 3 files changed, 15 insertions(+), 121 deletions(-) delete mode 100644 crates/node/src/alias.rs diff --git a/crates/host-reth/src/alias.rs b/crates/host-reth/src/alias.rs index 8a06344..acbce8e 100644 --- a/crates/host-reth/src/alias.rs +++ b/crates/host-reth/src/alias.rs @@ -1,5 +1,5 @@ use alloy::{consensus::constants::KECCAK_EMPTY, primitives::Address}; -use core::fmt; +use core::{fmt, future::{self, Future}}; use eyre::OptionExt; use reth::providers::{StateProviderBox, StateProviderFactory}; use signet_block_processor::{AliasOracle, AliasOracleFactory}; @@ -16,9 +16,9 @@ impl fmt::Debug for RethAliasOracle { } } -impl AliasOracle for RethAliasOracle { - fn should_alias(&self, address: Address) -> eyre::Result { - // No account at this address. +impl RethAliasOracle { + /// Synchronously check whether the given address should be aliased. + fn check_alias(&self, address: Address) -> eyre::Result { let Some(acct) = self.0.basic_account(&address)? else { return Ok(false) }; // Get the bytecode hash for this account. let bch = match acct.bytecode_hash { @@ -41,6 +41,12 @@ impl AliasOracle for RethAliasOracle { } } +impl AliasOracle for RethAliasOracle { + fn should_alias(&self, address: Address) -> impl Future> + Send { + future::ready(self.check_alias(address)) + } +} + /// An [`AliasOracleFactory`] backed by a `Box`. /// /// Creates [`RethAliasOracle`] instances from the latest host chain state. diff --git a/crates/host-reth/src/chain.rs b/crates/host-reth/src/chain.rs index c368c5a..cfaf34c 100644 --- a/crates/host-reth/src/chain.rs +++ b/crates/host-reth/src/chain.rs @@ -2,7 +2,7 @@ use alloy::{consensus::Block, consensus::BlockHeader}; use reth::primitives::{EthPrimitives, RecoveredBlock}; use reth::providers::Chain; use signet_blobber::RecoveredBlockShim; -use signet_extract::Extractable; +use signet_extract::{BlockAndReceipts, Extractable}; use signet_types::primitives::TransactionSigned; use std::sync::Arc; @@ -27,7 +27,9 @@ impl Extractable for RethChain { type Block = RecoveredBlockShim; type Receipt = reth::primitives::Receipt; - fn blocks_and_receipts(&self) -> impl Iterator)> { + fn blocks_and_receipts( + &self, + ) -> impl Iterator> { self.inner.blocks_and_receipts().map(|(block, receipts)| { // SAFETY: `RecoveredBlockShim` is `#[repr(transparent)]` over // `RethRecovered`, so these types have identical memory layouts. @@ -35,7 +37,7 @@ impl Extractable for RethChain { // `Arc`), which outlives the returned iterator. let block = unsafe { std::mem::transmute::<&RethRecovered, &RecoveredBlockShim>(block) }; - (block, receipts) + BlockAndReceipts { block, receipts } }) } diff --git a/crates/node/src/alias.rs b/crates/node/src/alias.rs deleted file mode 100644 index c682c87..0000000 --- a/crates/node/src/alias.rs +++ /dev/null @@ -1,114 +0,0 @@ -use alloy::{consensus::constants::KECCAK_EMPTY, primitives::Address}; -use core::{ - fmt, - future::{self, Future}, -}; -use eyre::OptionExt; -use reth::providers::{StateProviderBox, StateProviderFactory}; -use signet_block_processor::{AliasOracle, AliasOracleFactory}; - -/// An [`AliasOracle`] backed by a reth [`StateProviderBox`]. -/// -/// Checks whether an address has non-delegation bytecode, indicating it -/// should be aliased during transaction processing. -pub struct RethAliasOracle(StateProviderBox); - -impl fmt::Debug for RethAliasOracle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RethAliasOracle").finish_non_exhaustive() - } -} - -impl RethAliasOracle { - /// Synchronously check whether the given address should be aliased. - fn check_alias(&self, address: Address) -> eyre::Result { - let Some(acct) = self.0.basic_account(&address)? else { return Ok(false) }; - // Get the bytecode hash for this account. - let bch = match acct.bytecode_hash { - Some(hash) => hash, - // No bytecode hash; not a contract. - None => return Ok(false), - }; - // No code at this address. - if bch == KECCAK_EMPTY { - return Ok(false); - } - // Fetch the code associated with this bytecode hash. - let code = self - .0 - .bytecode_by_hash(&bch)? - .ok_or_eyre("code not found. This indicates a corrupted database")?; - - // If not a 7702 delegation contract, alias it. - Ok(!code.is_eip7702()) - } -} - -impl AliasOracle for RethAliasOracle { - fn should_alias(&self, address: Address) -> impl Future> + Send { - let result = self.check_alias(address); - future::ready(result) - } -} - -/// An [`AliasOracleFactory`] backed by a `Box`. -/// -/// Creates [`RethAliasOracle`] instances from the latest host chain state. -pub struct RethAliasOracleFactory(Box); - -impl fmt::Debug for RethAliasOracleFactory { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RethAliasOracleFactory").finish_non_exhaustive() - } -} - -impl RethAliasOracleFactory { - /// Create a new [`RethAliasOracleFactory`] from a boxed state provider - /// factory. - pub fn new(provider: Box) -> Self { - Self(provider) - } -} - -impl AliasOracleFactory for RethAliasOracleFactory { - type Oracle = RethAliasOracle; - - fn create(&self) -> eyre::Result { - // ## Why `Latest` instead of a pinned host height - // - // We use `Latest` rather than pinning to a specific host block - // height because pinning would require every node to be an archive - // node in order to sync historical state, which is impractical. - // - // ## Why `Latest` is safe - // - // An EOA cannot become a non-delegation contract without a birthday - // attack (c.f. EIP-3607). CREATE and CREATE2 addresses are - // deterministic and cannot target an existing EOA. EIP-7702 - // delegations are explicitly excluded by the `is_eip7702()` check - // in `should_alias`, so delegated EOAs are never aliased. This - // means the alias status of an address is stable across blocks - // under normal conditions, making `Latest` equivalent to any - // pinned height. - // - // ## The only risk: birthday attacks - // - // A birthday attack could produce a CREATE/CREATE2 collision with - // an existing EOA, causing `should_alias` to return a false - // positive. This is computationally infeasible for the foreseeable - // future (~2^80 work), and if it ever becomes practical we can - // revisit this decision. - // - // ## Over-aliasing vs under-aliasing - // - // Even in the birthday attack scenario, the result is - // over-aliasing (a false positive), which is benign: a transaction - // sender gets an aliased address when it shouldn't. The dangerous - // failure mode — under-aliasing — cannot occur here because - // contract bytecode is never removed once deployed. - self.0 - .state_by_block_number_or_tag(alloy::eips::BlockNumberOrTag::Latest) - .map(RethAliasOracle) - .map_err(Into::into) - } -} From e46fd40e01a55183f2e9bd4c495ebbe9f9431c22 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 13 Mar 2026 16:44:35 -0400 Subject: [PATCH 6/7] fix: remove with_chain_name calls and format imports after rebase The updated base branch removed chain_name from the SignetNodeBuilder API. Remove the now-invalid calls in node-tests and apply nightly fmt. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/host-reth/src/alias.rs | 5 ++++- crates/node-tests/src/context.rs | 1 - crates/node-tests/tests/db.rs | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/host-reth/src/alias.rs b/crates/host-reth/src/alias.rs index acbce8e..4cc7933 100644 --- a/crates/host-reth/src/alias.rs +++ b/crates/host-reth/src/alias.rs @@ -1,5 +1,8 @@ use alloy::{consensus::constants::KECCAK_EMPTY, primitives::Address}; -use core::{fmt, future::{self, Future}}; +use core::{ + fmt, + future::{self, Future}, +}; use eyre::OptionExt; use reth::providers::{StateProviderBox, StateProviderFactory}; use signet_block_processor::{AliasOracle, AliasOracleFactory}; diff --git a/crates/node-tests/src/context.rs b/crates/node-tests/src/context.rs index 907ab0c..c04efda 100644 --- a/crates/node-tests/src/context.rs +++ b/crates/node-tests/src/context.rs @@ -176,7 +176,6 @@ impl SignetTestContext { .with_notifier(decomposed.notifier) .with_storage(Arc::clone(&storage)) .with_alias_oracle(Arc::clone(&alias_oracle)) - .with_chain_name(decomposed.chain_name) .with_blob_cacher(blob_cacher) .with_serve_config(serve_config) .with_rpc_config(decomposed.rpc_config) diff --git a/crates/node-tests/tests/db.rs b/crates/node-tests/tests/db.rs index df1dc17..9a23970 100644 --- a/crates/node-tests/tests/db.rs +++ b/crates/node-tests/tests/db.rs @@ -49,7 +49,6 @@ async fn test_genesis() { .with_notifier(decomposed.notifier) .with_storage(Arc::clone(&storage)) .with_alias_oracle(Arc::clone(&alias_oracle)) - .with_chain_name(decomposed.chain_name) .with_blob_cacher(blob_cacher) .with_serve_config(decomposed.serve_config) .with_rpc_config(decomposed.rpc_config) From d8ec66c520f130f6fd4d498b712f5c597cd2f769 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 15 Mar 2026 09:55:48 -0400 Subject: [PATCH 7/7] fix(host-reth): address code review findings from PR #106 - Add compile-time size/align assertions for transmute safety (chain.rs) - Add tracing::error before set_head panics for operator observability - Log swallowed provider errors when reading safe/finalized block numbers - Replace `as` casts with `From` in config conversion for type safety - Restore condensed safety rationale for Latest alias oracle - Add unit tests for config conversion helpers (5 tests) - Add rustdoc usage examples to RethChain and decompose_exex_context - Extract test_blob_cacher helper to reduce duplication in node-tests eyre convention violation tracked in ENG-2041. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/host-reth/src/alias.rs | 16 +++--- crates/host-reth/src/chain.rs | 31 ++++++++++++ crates/host-reth/src/config.rs | 63 ++++++++++++++++++++++-- crates/host-reth/src/notifier.rs | 38 ++++++++++++-- crates/node-tests/src/blob_test_utils.rs | 23 +++++++++ crates/node-tests/src/context.rs | 10 +--- crates/node-tests/src/lib.rs | 3 ++ crates/node-tests/tests/db.rs | 9 +--- 8 files changed, 162 insertions(+), 31 deletions(-) create mode 100644 crates/node-tests/src/blob_test_utils.rs diff --git a/crates/host-reth/src/alias.rs b/crates/host-reth/src/alias.rs index 4cc7933..9cbad0a 100644 --- a/crates/host-reth/src/alias.rs +++ b/crates/host-reth/src/alias.rs @@ -73,13 +73,15 @@ impl AliasOracleFactory for RethAliasOracleFactory { type Oracle = RethAliasOracle; fn create(&self) -> eyre::Result { - // NB: This becomes a problem if anyone ever birthday attacks a - // contract/EOA pair (c.f. EIP-3607). In practice this is unlikely to - // happen for the foreseeable future, and if it does we can revisit - // this decision. - // We considered taking the host height as an argument to this method, - // but this would require all nodes to be archive nodes in order to - // sync, which is less than ideal + // We use `Latest` rather than a pinned host height because pinning + // would require every node to be an archive node, which is impractical. + // + // This is safe because alias status is stable across blocks: an EOA + // cannot become a non-delegation contract without a birthday attack + // (c.f. EIP-3607), and EIP-7702 delegations are excluded by + // `is_eip7702()`. Even in the (computationally infeasible ~2^80) + // birthday attack scenario, the result is a benign false-positive + // (over-aliasing), never a dangerous false-negative. self.0 .state_by_block_number_or_tag(alloy::eips::BlockNumberOrTag::Latest) .map(RethAliasOracle) diff --git a/crates/host-reth/src/chain.rs b/crates/host-reth/src/chain.rs index cfaf34c..3f6e5c7 100644 --- a/crates/host-reth/src/chain.rs +++ b/crates/host-reth/src/chain.rs @@ -11,6 +11,24 @@ type RethRecovered = RecoveredBlock>; /// An owning wrapper around reth's [`Chain`] that implements [`Extractable`] /// with O(1) metadata accessors. +/// +/// # Usage +/// +/// `RethChain` is typically obtained from [`HostNotification`] events, not +/// constructed directly. To extract blocks and receipts: +/// +/// ```ignore +/// # // Requires reth ExEx runtime — shown for API illustration only. +/// use signet_extract::Extractable; +/// +/// fn process(chain: &RethChain) { +/// for bar in chain.blocks_and_receipts() { +/// println!("block receipts: {}", bar.receipts.len()); +/// } +/// } +/// ``` +/// +/// [`HostNotification`]: signet_node_types::HostNotification #[derive(Debug)] pub struct RethChain { inner: Arc>, @@ -31,6 +49,19 @@ impl Extractable for RethChain { &self, ) -> impl Iterator> { self.inner.blocks_and_receipts().map(|(block, receipts)| { + // Compile-time check: RecoveredBlockShim must have the same + // layout as RethRecovered (guaranteed by #[repr(transparent)] + // on RecoveredBlockShim in signet-blobber/src/shim.rs). + const { + assert!( + size_of::() == size_of::(), + "RecoveredBlockShim layout diverged from RethRecovered" + ); + assert!( + align_of::() == align_of::(), + "RecoveredBlockShim alignment diverged from RethRecovered" + ); + } // SAFETY: `RecoveredBlockShim` is `#[repr(transparent)]` over // `RethRecovered`, so these types have identical memory layouts. // The lifetime of the reference is tied to `self.inner` (the diff --git a/crates/host-reth/src/config.rs b/crates/host-reth/src/config.rs index f5334f7..c759819 100644 --- a/crates/host-reth/src/config.rs +++ b/crates/host-reth/src/config.rs @@ -10,10 +10,10 @@ pub fn rpc_config_from_args(args: &RpcServerArgs) -> StorageRpcConfig { StorageRpcConfig::builder() .rpc_gas_cap(args.rpc_gas_cap) .max_tracing_requests(args.rpc_max_tracing_requests) - .gas_oracle_block_count(gpo.blocks as u64) - .gas_oracle_percentile(gpo.percentile as f64) - .ignore_price(Some(gpo.ignore_price as u128)) - .max_price(Some(gpo.max_price as u128)) + .gas_oracle_block_count(u64::from(gpo.blocks)) + .gas_oracle_percentile(f64::from(gpo.percentile)) + .ignore_price(Some(u128::from(gpo.ignore_price))) + .max_price(Some(u128::from(gpo.max_price))) .build() } @@ -32,3 +32,58 @@ pub fn serve_config_from_args(args: &RpcServerArgs) -> ServeConfig { ipc, } } + +#[cfg(test)] +mod tests { + use crate::config::{rpc_config_from_args, serve_config_from_args}; + use reth::args::RpcServerArgs; + + #[test] + fn rpc_config_from_default_args() { + let args = RpcServerArgs::default(); + let gpo = &args.gas_price_oracle; + let config = rpc_config_from_args(&args); + + assert_eq!(config.rpc_gas_cap, args.rpc_gas_cap); + assert_eq!(config.max_tracing_requests, args.rpc_max_tracing_requests); + assert_eq!(config.gas_oracle_block_count, u64::from(gpo.blocks)); + assert_eq!(config.gas_oracle_percentile, f64::from(gpo.percentile)); + assert_eq!(config.ignore_price, Some(u128::from(gpo.ignore_price))); + assert_eq!(config.max_price, Some(u128::from(gpo.max_price))); + } + + #[test] + fn serve_config_http_disabled_by_default() { + let args = RpcServerArgs::default(); + let config = serve_config_from_args(&args); + + assert!(config.http.is_empty()); + assert!(config.ws.is_empty()); + } + + #[test] + fn serve_config_http_enabled() { + let args = RpcServerArgs { http: true, ..Default::default() }; + let config = serve_config_from_args(&args); + + assert_eq!(config.http.len(), 1); + assert_eq!(config.http[0].port(), args.http_port); + } + + #[test] + fn serve_config_ws_enabled() { + let args = RpcServerArgs { ws: true, ..Default::default() }; + let config = serve_config_from_args(&args); + + assert_eq!(config.ws.len(), 1); + assert_eq!(config.ws[0].port(), args.ws_port); + } + + #[test] + fn serve_config_ipc_enabled_by_default() { + let args = RpcServerArgs::default(); + let config = serve_config_from_args(&args); + + assert!(config.ipc.is_some()); + } +} diff --git a/crates/host-reth/src/notifier.rs b/crates/host-reth/src/notifier.rs index ed2fa3c..1e96a5a 100644 --- a/crates/host-reth/src/notifier.rs +++ b/crates/host-reth/src/notifier.rs @@ -16,7 +16,7 @@ use reth_stages_types::ExecutionStageThresholds; use signet_node_types::{HostNotification, HostNotificationKind, HostNotifier}; use signet_rpc::{ServeConfig, StorageRpcConfig}; use std::sync::Arc; -use tracing::debug; +use tracing::{debug, error}; /// Reth ExEx implementation of [`HostNotifier`]. /// @@ -59,6 +59,22 @@ impl core::fmt::Debug for DecomposedContext { /// Decompose a reth [`ExExContext`] into a [`RethHostNotifier`] and /// associated configuration values. /// +/// This is the primary entry point for integrating with reth's ExEx +/// framework. Typical usage in an ExEx `init` function: +/// +/// ```ignore +/// # // Requires ExExContext — shown for API illustration only. +/// use signet_host_reth::decompose_exex_context; +/// +/// async fn init(ctx: ExExContext) -> eyre::Result<()> { +/// let decomposed = decompose_exex_context(ctx); +/// // decomposed.notifier implements HostNotifier +/// // decomposed.serve_config / rpc_config are reth-free +/// // decomposed.pool is the transaction pool handle +/// Ok(()) +/// } +/// ``` +/// /// This splits the ExEx context into: /// - A [`RethHostNotifier`] (implements [`HostNotifier`]) /// - A [`ServeConfig`] (plain RPC server config) @@ -100,8 +116,22 @@ where }; // Read safe/finalized from the provider at notification time. - let safe_block_number = self.provider.safe_block_number().ok().flatten(); - let finalized_block_number = self.provider.finalized_block_number().ok().flatten(); + let safe_block_number = self + .provider + .safe_block_number() + .inspect_err(|e| { + debug!(%e, "failed to read safe block number from provider"); + }) + .ok() + .flatten(); + let finalized_block_number = self + .provider + .finalized_block_number() + .inspect_err(|e| { + debug!(%e, "failed to read finalized block number from provider"); + }) + .ok() + .flatten(); let kind = match notification { reth_exex::ExExNotification::ChainCommitted { new } => { @@ -125,6 +155,7 @@ where let head = self .provider .sealed_header(block_number) + .inspect_err(|e| error!(block_number, %e, "failed to look up header for set_head")) .expect("failed to look up header for set_head") .map(|h| BlockNumHash { number: block_number, hash: h.hash() }) .unwrap_or_else(|| { @@ -132,6 +163,7 @@ where let genesis = self .provider .sealed_header(0) + .inspect_err(|e| error!(%e, "failed to look up genesis header")) .expect("failed to look up genesis header") .expect("genesis header missing"); BlockNumHash { number: 0, hash: genesis.hash() } diff --git a/crates/node-tests/src/blob_test_utils.rs b/crates/node-tests/src/blob_test_utils.rs new file mode 100644 index 0000000..7b435ad --- /dev/null +++ b/crates/node-tests/src/blob_test_utils.rs @@ -0,0 +1,23 @@ +use reth::transaction_pool::TransactionPool; +use signet_blobber::CacheHandle; +use signet_node_config::SignetNodeConfig; + +/// Build a blob [`CacheHandle`] for test use from a config and transaction +/// pool. +/// +/// Uses a fresh `reqwest::Client` and spawns with [`SimpleCoder`]. +/// +/// [`SimpleCoder`]: alloy::consensus::SimpleCoder +pub fn test_blob_cacher(cfg: &SignetNodeConfig, pool: Pool) -> CacheHandle +where + Pool: TransactionPool + 'static, +{ + signet_blobber::BlobFetcher::builder() + .with_config(cfg.block_extractor()) + .unwrap() + .with_pool(pool) + .with_client(reqwest::Client::new()) + .build_cache() + .unwrap() + .spawn::() +} diff --git a/crates/node-tests/src/context.rs b/crates/node-tests/src/context.rs index c04efda..e54f85d 100644 --- a/crates/node-tests/src/context.rs +++ b/crates/node-tests/src/context.rs @@ -152,15 +152,7 @@ impl SignetTestContext { let alias_oracle: Arc>> = Arc::new(Mutex::new(HashSet::default())); - // Build the blob cacher from the decomposed pool - let blob_cacher = signet_blobber::BlobFetcher::builder() - .with_config(cfg.block_extractor()) - .unwrap() - .with_pool(decomposed.pool) - .with_client(reqwest::Client::new()) - .build_cache() - .unwrap() - .spawn::(); + let blob_cacher = crate::test_blob_cacher(&cfg, decomposed.pool); // Build serve config from the Signet test config rather than the // reth defaults (which have IPC/HTTP disabled). diff --git a/crates/node-tests/src/lib.rs b/crates/node-tests/src/lib.rs index 10a13f8..8275716 100644 --- a/crates/node-tests/src/lib.rs +++ b/crates/node-tests/src/lib.rs @@ -14,6 +14,9 @@ /// Test constants. pub mod constants; +/// Blob cacher test utilities. +mod blob_test_utils; +pub use blob_test_utils::test_blob_cacher; /// Test context. mod context; pub use context::{BalanceChecks, NonceChecks, SignetTestContext}; diff --git a/crates/node-tests/tests/db.rs b/crates/node-tests/tests/db.rs index 9a23970..040e387 100644 --- a/crates/node-tests/tests/db.rs +++ b/crates/node-tests/tests/db.rs @@ -34,14 +34,7 @@ async fn test_genesis() { let storage = Arc::new(UnifiedStorage::spawn(hot, MemColdBackend::new(), cancel_token.clone())); - let blob_cacher = signet_blobber::BlobFetcher::builder() - .with_config(cfg.block_extractor()) - .unwrap() - .with_pool(decomposed.pool) - .with_client(reqwest::Client::new()) - .build_cache() - .unwrap() - .spawn::(); + let blob_cacher = signet_node_tests::test_blob_cacher(&cfg, decomposed.pool); let alias_oracle: Arc>> = Arc::new(Mutex::new(HashSet::default()));