diff --git a/Cargo.toml b/Cargo.toml index 294c861..18c8bed 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" } @@ -114,15 +115,4 @@ url = "2.5.4" # Test Utils tempfile = "3.17.0" -[patch.crates-io] -signet-bundle = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} -signet-constants = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} -signet-evm = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} -signet-extract = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} -signet-journal = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} -signet-test-utils = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} -signet-tx-cache = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} -signet-types = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} -signet-zenith = { git = "https://github.com/init4tech/signet-sdk.git", branch = "feat/extractable-metadata"} - # init4-bin-base = { path = "../shared" } 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..9cbad0a --- /dev/null +++ b/crates/host-reth/src/alias.rs @@ -0,0 +1,90 @@ +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 { + future::ready(self.check_alias(address)) + } +} + +/// 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 { + // 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) + .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..c7fb3fd --- /dev/null +++ b/crates/host-reth/src/chain.rs @@ -0,0 +1,86 @@ +use alloy::{consensus::Block, consensus::BlockHeader}; +use reth::primitives::{EthPrimitives, RecoveredBlock}; +use reth::providers::Chain; +use signet_blobber::RecoveredBlockShim; +use signet_extract::{BlockAndReceipts, 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. +/// +/// # 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>, +} + +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)| { + // 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 + // `Arc`), which outlives the returned iterator. + let block = + unsafe { std::mem::transmute::<&RethRecovered, &RecoveredBlockShim>(block) }; + BlockAndReceipts { block, receipts } + }) + } + + fn first_number(&self) -> u64 { + self.inner.first().number() + } + + fn tip_number(&self) -> u64 { + self.inner.tip().number() + } + + fn len(&self) -> usize { + self.inner.len() + } +} diff --git a/crates/host-reth/src/config.rs b/crates/host-reth/src/config.rs new file mode 100644 index 0000000..c759819 --- /dev/null +++ b/crates/host-reth/src/config.rs @@ -0,0 +1,89 @@ +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(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() +} + +/// 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, + } +} + +#[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/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..1e96a5a --- /dev/null +++ b/crates/host-reth/src/notifier.rs @@ -0,0 +1,195 @@ +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, 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, error}; + +/// 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 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) +/// - 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() + .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 } => { + 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 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(|| { + debug!(block_number, "header not found for set_head, falling back to genesis"); + 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() } + }); + + self.notifications.set_with_head(reth_exex::ExExHead { block: 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(()) + } +} diff --git a/crates/node-tests/Cargo.toml b/crates/node-tests/Cargo.toml index a6060f0..358984c 100644 --- a/crates/node-tests/Cargo.toml +++ b/crates/node-tests/Cargo.toml @@ -11,11 +11,14 @@ 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"] } 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 +34,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/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 190ac9f..afec7a7 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,26 @@ impl SignetTestContext { let alias_oracle: Arc>> = Arc::new(Mutex::new(HashSet::default())); + 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). + 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_ctx(ctx) + .with_notifier(decomposed.notifier) .with_storage(Arc::clone(&storage)) .with_alias_oracle(Arc::clone(&alias_oracle)) + .with_blob_cacher(blob_cacher) + .with_serve_config(serve_config) + .with_rpc_config(decomposed.rpc_config) + .with_client(reqwest::Client::new()) .build() .await .unwrap(); @@ -301,10 +321,10 @@ impl SignetTestContext { // the expected height is either the last committed block, or the // first reverted block - 1, or 0 if neither exist let expected_height = committed_chain - .and_then(|c| c.blocks.last().map(|b| b.number())) + .and_then(|c| c.blocks().last().map(|b| b.number())) .or_else(|| { reverted_chain - .and_then(|c| c.blocks.first().map(|b| b.number().saturating_sub(1))) + .and_then(|c| c.blocks().first().map(|b| b.number().saturating_sub(1))) }) .unwrap_or(0) .saturating_sub(self.constants().host_deploy_height()); diff --git a/crates/node-tests/src/convert.rs b/crates/node-tests/src/convert.rs index d08dbef..e51879e 100644 --- a/crates/node-tests/src/convert.rs +++ b/crates/node-tests/src/convert.rs @@ -79,8 +79,8 @@ impl ToRethPrimitive for signet_test_utils::chain::Chain { fn to_reth(self) -> Self::RethPrimitive { reth::providers::Chain::new( - self.blocks.to_reth(), - self.execution_outcome.to_reth(), + self.blocks().to_vec().to_reth(), + self.execution_outcome().clone().to_reth(), Default::default(), ) } 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 9788342..040e387 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,18 @@ async fn test_genesis() { let storage = Arc::new(UnifiedStorage::spawn(hot, MemColdBackend::new(), cancel_token.clone())); + let blob_cacher = signet_node_tests::test_blob_cacher(&cfg, decomposed.pool); + + 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_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/src/builder.rs b/crates/node/src/builder.rs index d340f6b..3995070 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -56,27 +56,18 @@ pub struct NotAStorage; /// .with_rpc_config(rpc_config); /// # } /// ``` -pub struct SignetNodeBuilder< - Notifier = (), - Storage = NotAStorage, - Aof = NotAnAof, - Bc = (), - Sc = (), - Rc = (), -> { +pub struct SignetNodeBuilder { config: SignetNodeConfig, alias_oracle: Option, notifier: Option, storage: Option, client: Option, - blob_cacher: Option, - serve_config: Option, - rpc_config: Option, + blob_cacher: Option, + serve_config: Option, + rpc_config: Option, } -impl core::fmt::Debug - for SignetNodeBuilder -{ +impl core::fmt::Debug for SignetNodeBuilder { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("SignetNodeBuilder").finish_non_exhaustive() } @@ -98,12 +89,12 @@ impl SignetNodeBuilder { } } -impl SignetNodeBuilder { +impl SignetNodeBuilder { /// Set the [`UnifiedStorage`] backend for the signet node. pub fn with_storage( self, storage: Arc>, - ) -> SignetNodeBuilder>, Aof, Bc, Sc, Rc> { + ) -> SignetNodeBuilder>, Aof> { SignetNodeBuilder { config: self.config, alias_oracle: self.alias_oracle, @@ -117,10 +108,7 @@ impl SignetNodeBuilder( - self, - notifier: N, - ) -> SignetNodeBuilder { + pub fn with_notifier(self, notifier: N) -> SignetNodeBuilder { SignetNodeBuilder { config: self.config, alias_oracle: self.alias_oracle, @@ -137,7 +125,7 @@ impl SignetNodeBuilder( self, alias_oracle: NewAof, - ) -> SignetNodeBuilder { + ) -> SignetNodeBuilder { SignetNodeBuilder { config: self.config, alias_oracle: Some(alias_oracle), @@ -157,59 +145,25 @@ impl SignetNodeBuilder SignetNodeBuilder { - SignetNodeBuilder { - config: self.config, - alias_oracle: self.alias_oracle, - notifier: self.notifier, - storage: self.storage, - client: self.client, - blob_cacher: Some(blob_cacher), - serve_config: self.serve_config, - rpc_config: self.rpc_config, - } + pub fn with_blob_cacher(mut self, blob_cacher: CacheHandle) -> Self { + self.blob_cacher = Some(blob_cacher); + self } /// Set the RPC transport configuration. - pub fn with_serve_config( - self, - serve_config: ServeConfig, - ) -> SignetNodeBuilder { - SignetNodeBuilder { - config: self.config, - alias_oracle: self.alias_oracle, - notifier: self.notifier, - storage: self.storage, - client: self.client, - blob_cacher: self.blob_cacher, - serve_config: Some(serve_config), - rpc_config: self.rpc_config, - } + pub fn with_serve_config(mut self, serve_config: ServeConfig) -> Self { + self.serve_config = Some(serve_config); + self } /// Set the RPC behaviour configuration. - pub fn with_rpc_config( - self, - rpc_config: StorageRpcConfig, - ) -> SignetNodeBuilder { - SignetNodeBuilder { - config: self.config, - alias_oracle: self.alias_oracle, - notifier: self.notifier, - storage: self.storage, - client: self.client, - blob_cacher: self.blob_cacher, - serve_config: self.serve_config, - rpc_config: Some(rpc_config), - } + pub const fn with_rpc_config(mut self, rpc_config: StorageRpcConfig) -> Self { + self.rpc_config = Some(rpc_config); + self } } -impl - SignetNodeBuilder>, Aof, CacheHandle, ServeConfig, StorageRpcConfig> +impl SignetNodeBuilder>, Aof> where N: HostNotifier, H: HotKv + Clone + Send + Sync + 'static,