From 087ee7eb771b09904dcf17ac51989580eac69ec6 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 18 Mar 2026 09:42:42 -0400 Subject: [PATCH 1/4] feat: add signet-host-reth crate with RethHostNotifier Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 12 +- crates/host-reth/Cargo.toml | 30 ++++ crates/host-reth/README.md | 3 + crates/host-reth/src/alias.rs | 90 +++++++++++ crates/host-reth/src/chain.rs | 86 ++++++++++ crates/host-reth/src/config.rs | 89 +++++++++++ crates/host-reth/src/error.rs | 24 +++ crates/host-reth/src/lib.rs | 26 +++ crates/host-reth/src/notifier.rs | 195 +++++++++++++++++++++++ crates/node-tests/Cargo.toml | 4 + crates/node-tests/src/blob_test_utils.rs | 23 +++ crates/node-tests/src/context.rs | 26 ++- crates/node-tests/src/convert.rs | 4 +- crates/node-tests/src/lib.rs | 3 + crates/node-tests/tests/db.rs | 17 +- crates/node/src/builder.rs | 84 +++------- 16 files changed, 633 insertions(+), 83 deletions(-) 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 create mode 100644 crates/node-tests/src/blob_test_utils.rs 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, From 2bb8d4c37e72f3a6ea10ce679492e4eb019fd150 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Mar 2026 08:24:19 -0400 Subject: [PATCH 2/4] fix: address review feedback from Fraser999 - Delete unused ExtractableChainShim from blobber (RethChain replaces it) - Replace From with named RethHostError::notification() - Handle set_backfill_thresholds(None) by restoring defaults - Add blank line between module groups in host-reth lib.rs Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/blobber/src/lib.rs | 2 +- crates/blobber/src/shim.rs | 41 +------------------------------- crates/host-reth/src/error.rs | 5 ++-- crates/host-reth/src/lib.rs | 1 + crates/host-reth/src/notifier.rs | 20 +++++++++------- 5 files changed, 18 insertions(+), 51 deletions(-) diff --git a/crates/blobber/src/lib.rs b/crates/blobber/src/lib.rs index 511d4ff..4036380 100644 --- a/crates/blobber/src/lib.rs +++ b/crates/blobber/src/lib.rs @@ -24,7 +24,7 @@ mod error; pub use error::{BlobberError, BlobberResult}; mod shim; -pub use shim::{ExtractableChainShim, RecoveredBlockShim}; +pub use shim::RecoveredBlockShim; #[cfg(test)] mod test { diff --git a/crates/blobber/src/shim.rs b/crates/blobber/src/shim.rs index 084ce9f..d47f728 100644 --- a/crates/blobber/src/shim.rs +++ b/crates/blobber/src/shim.rs @@ -1,51 +1,12 @@ //! Shim and utilities for signet-sdk to reth conversions. use alloy::consensus::Block; -use reth::providers::Chain; -use signet_extract::{BlockAndReceipts, Extractable, HasTxns}; +use signet_extract::HasTxns; use signet_types::primitives::TransactionSigned; /// A type alias for Reth's recovered block with a signed transaction. type RethRecovered = reth::primitives::RecoveredBlock>; -/// A shim around Reth's [`Chain`]. -#[derive(Debug)] -#[repr(transparent)] -pub struct ExtractableChainShim<'a> { - /// The underlying Reth chain. - chain: &'a Chain, -} - -impl<'a> ExtractableChainShim<'a> { - /// Create a new shim around the given Reth chain. - pub const fn new(chain: &'a Chain) -> Self { - Self { chain } - } - - /// Get a reference to the underlying Reth chain. - pub const fn chain(&self) -> &'a Chain { - self.chain - } -} - -impl<'a> Extractable for ExtractableChainShim<'a> { - type Block = RecoveredBlockShim; - type Receipt = reth::primitives::Receipt; - - fn blocks_and_receipts( - &self, - ) -> impl Iterator> { - self.chain.blocks_and_receipts().map(|(block, receipts)| { - // SAFETY: `RecoveredBlockShim` is `#[repr(transparent)]` over a - // single `RethRecovered` field, guaranteeing identical memory - // layout. This makes the reference transmute sound. - let block = - unsafe { std::mem::transmute::<&'a RethRecovered, &RecoveredBlockShim>(block) }; - BlockAndReceipts { block, receipts } - }) - } -} - /// A shim for Reth's [`reth::primitives::RecoveredBlock`]. #[derive(Debug)] #[repr(transparent)] diff --git a/crates/host-reth/src/error.rs b/crates/host-reth/src/error.rs index 1391db2..5889992 100644 --- a/crates/host-reth/src/error.rs +++ b/crates/host-reth/src/error.rs @@ -17,8 +17,9 @@ pub enum RethHostError { MissingHeader(u64), } -impl From for RethHostError { - fn from(e: eyre::Report) -> Self { +impl RethHostError { + /// Wrap a notification stream error. + pub fn notification(e: impl Into>) -> Self { Self::Notification(e.into()) } } diff --git a/crates/host-reth/src/lib.rs b/crates/host-reth/src/lib.rs index 3ff1e46..fc16c06 100644 --- a/crates/host-reth/src/lib.rs +++ b/crates/host-reth/src/lib.rs @@ -13,6 +13,7 @@ mod alias; pub use alias::{RethAliasOracle, RethAliasOracleFactory}; + mod error; pub use error::RethHostError; diff --git a/crates/host-reth/src/notifier.rs b/crates/host-reth/src/notifier.rs index 1e96a5a..147b1e0 100644 --- a/crates/host-reth/src/notifier.rs +++ b/crates/host-reth/src/notifier.rs @@ -112,7 +112,7 @@ where let notification = self.notifications.next().await?; let notification = match notification { Ok(n) => n, - Err(e) => return Some(Err(e.into())), + Err(e) => return Some(Err(RethHostError::notification(e))), }; // Read safe/finalized from the provider at notification time. @@ -173,13 +173,17 @@ where } 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"); - } + let thresholds = match max_blocks { + Some(max_blocks) => { + debug!(max_blocks, "configured backfill thresholds"); + ExecutionStageThresholds { max_blocks: Some(max_blocks), ..Default::default() } + } + None => { + debug!("reset backfill thresholds to defaults"); + ExecutionStageThresholds::default() + } + }; + self.notifications.set_backfill_thresholds(thresholds); } fn send_finished_height(&self, block_number: u64) -> Result<(), Self::Error> { From 612831dac5d3f34b9399702c36360a2c0d3faa69 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Mar 2026 10:21:37 -0400 Subject: [PATCH 3/4] feat: replace full chain segments with RevertRange in revert notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverted chain segments in HostNotificationKind (ChainReverted and ChainReorged) now carry a lightweight RevertRange (first, tip block numbers) instead of Arc. No consumer reads block bodies or receipts from reverted segments — only block number range is needed. Adds node-side validation that the revert range tip covers the stored rollup tip, ensuring notification consistency with stored state. This unblocks signet-host-rpc where old/reverted blocks may not be available from the RPC node. Closes ENG-2069 Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/host-reth/src/notifier.rs | 13 ++- crates/node-types/src/lib.rs | 2 +- crates/node-types/src/notification.rs | 116 ++++++++++++++++++++++---- crates/node/src/metrics.rs | 9 +- crates/node/src/node.rs | 35 ++++++-- 5 files changed, 137 insertions(+), 38 deletions(-) diff --git a/crates/host-reth/src/notifier.rs b/crates/host-reth/src/notifier.rs index 147b1e0..7c4be7c 100644 --- a/crates/host-reth/src/notifier.rs +++ b/crates/host-reth/src/notifier.rs @@ -3,7 +3,7 @@ use crate::{ config::{rpc_config_from_args, serve_config_from_args}, error::RethHostError, }; -use alloy::eips::BlockNumHash; +use alloy::{consensus::BlockHeader, eips::BlockNumHash}; use futures_util::StreamExt; use reth::{ chainspec::EthChainSpec, @@ -13,7 +13,7 @@ use reth::{ 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_node_types::{HostNotification, HostNotificationKind, HostNotifier, RevertRange}; use signet_rpc::{ServeConfig, StorageRpcConfig}; use std::sync::Arc; use tracing::{debug, error}; @@ -138,13 +138,12 @@ where HostNotificationKind::ChainCommitted { new: Arc::new(RethChain::new(new)) } } reth_exex::ExExNotification::ChainReverted { old } => { - HostNotificationKind::ChainReverted { old: Arc::new(RethChain::new(old)) } + let old = RevertRange::new(old.first().number(), old.tip().number()); + HostNotificationKind::ChainReverted { old } } reth_exex::ExExNotification::ChainReorged { old, new } => { - HostNotificationKind::ChainReorged { - old: Arc::new(RethChain::new(old)), - new: Arc::new(RethChain::new(new)), - } + let old = RevertRange::new(old.first().number(), old.tip().number()); + HostNotificationKind::ChainReorged { old, new: Arc::new(RethChain::new(new)) } } }; diff --git a/crates/node-types/src/lib.rs b/crates/node-types/src/lib.rs index 6572742..040e3b9 100644 --- a/crates/node-types/src/lib.rs +++ b/crates/node-types/src/lib.rs @@ -12,7 +12,7 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] mod notification; -pub use notification::{HostNotification, HostNotificationKind}; +pub use notification::{HostNotification, HostNotificationKind, RevertRange}; mod notifier; pub use notifier::HostNotifier; diff --git a/crates/node-types/src/notification.rs b/crates/node-types/src/notification.rs index abfb63f..0130d04 100644 --- a/crates/node-types/src/notification.rs +++ b/crates/node-types/src/notification.rs @@ -1,5 +1,57 @@ use std::sync::Arc; +/// The range of host blocks that were reverted by the host chain. +/// +/// # Panics +/// +/// [`RevertRange::new`] panics if `first > tip` or `first == 0`. +/// +/// # Examples +/// +/// ``` +/// # use signet_node_types::RevertRange; +/// let range = RevertRange::new(10, 15); +/// assert_eq!(range.first(), 10); +/// assert_eq!(range.tip(), 15); +/// assert_eq!(range.len(), 6); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RevertRange { + /// Block number of the first reverted block. + first: u64, + /// Block number of the last reverted block (tip). + tip: u64, +} + +impl RevertRange { + /// Create a new revert range. Panics if `first > tip` or `first == 0`. + pub fn new(first: u64, tip: u64) -> Self { + assert!(first > 0, "RevertRange: first block number must be > 0"); + assert!(first <= tip, "RevertRange: first ({first}) must be <= tip ({tip})"); + Self { first, tip } + } + + /// The first (lowest) reverted block number. + pub const fn first(&self) -> u64 { + self.first + } + + /// The tip (highest) reverted block number. + pub const fn tip(&self) -> u64 { + self.tip + } + + /// The number of blocks in this range. Always >= 1. + pub const fn len(&self) -> u64 { + self.tip - self.first + 1 + } + + /// Returns `false`. A valid `RevertRange` is never empty. + pub const fn is_empty(&self) -> bool { + false + } +} + /// A notification from the host chain, bundling a chain event with /// point-in-time block tag data. The safe/finalized block numbers are /// intentionally snapshotted at notification creation time rather than @@ -20,7 +72,7 @@ use std::sync::Arc; /// /// // Access the committed chain via the shortcut method. /// assert!(notification.committed_chain().is_some()); -/// assert!(notification.reverted_chain().is_none()); +/// assert!(notification.revert_range().is_none()); /// # } /// ``` #[derive(Debug, Clone)] @@ -41,24 +93,28 @@ impl HostNotification { self.kind.committed_chain() } - /// Returns the reverted chain, if any. Shortcut for - /// `self.kind.reverted_chain()`. - pub const fn reverted_chain(&self) -> Option<&Arc> { - self.kind.reverted_chain() + /// Returns the revert range, if any. Shortcut for + /// `self.kind.revert_range()`. + pub const fn revert_range(&self) -> Option { + self.kind.revert_range() } } /// The kind of chain event in a [`HostNotification`]. /// +/// Only committed chain segments carry full block and receipt data. +/// Reverted segments are represented as a [`RevertRange`] containing +/// only the first and tip block numbers. +/// /// # Examples /// /// ``` /// # use std::sync::Arc; -/// # use signet_node_types::HostNotificationKind; -/// # fn example(old: Arc, new: Arc) { +/// # use signet_node_types::{HostNotificationKind, RevertRange}; +/// # fn example(new: Arc) { /// let kind = HostNotificationKind::ChainReorged { -/// old: old.clone(), -/// new: new.clone(), +/// old: RevertRange::new(5, 10), +/// new, /// }; /// # } /// ``` @@ -71,14 +127,14 @@ pub enum HostNotificationKind { }, /// A chain segment was reverted. ChainReverted { - /// The reverted chain segment. - old: Arc, + /// The range of reverted host blocks. + old: RevertRange, }, /// A chain reorg occurred: one segment was reverted and replaced by /// another. ChainReorged { - /// The reverted chain segment. - old: Arc, + /// The range of reverted host blocks. + old: RevertRange, /// The newly committed chain segment. new: Arc, }, @@ -100,7 +156,7 @@ impl HostNotificationKind { } } - /// Returns the reverted chain, if any. + /// Returns the revert range, if any. /// /// Returns `Some` for [`ChainReverted`] and [`ChainReorged`], `None` /// for [`ChainCommitted`]. @@ -108,10 +164,38 @@ impl HostNotificationKind { /// [`ChainReverted`]: HostNotificationKind::ChainReverted /// [`ChainReorged`]: HostNotificationKind::ChainReorged /// [`ChainCommitted`]: HostNotificationKind::ChainCommitted - pub const fn reverted_chain(&self) -> Option<&Arc> { + pub const fn revert_range(&self) -> Option { match self { - Self::ChainReverted { old } | Self::ChainReorged { old, .. } => Some(old), + Self::ChainReverted { old } | Self::ChainReorged { old, .. } => Some(*old), Self::ChainCommitted { .. } => None, } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn revert_range_valid() { + let range = RevertRange::new(1, 1); + assert_eq!(range.first(), 1); + assert_eq!(range.tip(), 1); + assert_eq!(range.len(), 1); + + let range = RevertRange::new(5, 10); + assert_eq!(range.len(), 6); + } + + #[test] + #[should_panic(expected = "first block number must be > 0")] + fn revert_range_zero_first() { + RevertRange::new(0, 5); + } + + #[test] + #[should_panic(expected = "first (10) must be <= tip (5)")] + fn revert_range_inverted() { + RevertRange::new(10, 5); + } +} diff --git a/crates/node/src/metrics.rs b/crates/node/src/metrics.rs index f43d25f..4464081 100644 --- a/crates/node/src/metrics.rs +++ b/crates/node/src/metrics.rs @@ -7,7 +7,6 @@ //! - Number of reorgs processed use metrics::{Counter, counter, describe_counter}; -use signet_extract::Extractable; use signet_node_types::HostNotification; use std::sync::LazyLock; @@ -66,16 +65,16 @@ fn inc_reorgs_processed() { reorgs_processed().increment(1); } -pub(crate) fn record_notification_received(notification: &HostNotification) { +pub(crate) fn record_notification_received(notification: &HostNotification) { inc_notifications_received(); - if notification.reverted_chain().is_some() { + if notification.revert_range().is_some() { inc_reorgs_received(); } } -pub(crate) fn record_notification_processed(notification: &HostNotification) { +pub(crate) fn record_notification_processed(notification: &HostNotification) { inc_notifications_processed(); - if notification.reverted_chain().is_some() { + if notification.revert_range().is_some() { inc_reorgs_processed(); } } diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index 6a9b097..3537916 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -6,7 +6,7 @@ use signet_block_processor::{AliasOracleFactory, SignetBlockProcessorV1}; use signet_evm::EthereumHardfork; use signet_extract::{Extractable, Extractor}; use signet_node_config::SignetNodeConfig; -use signet_node_types::{HostNotification, HostNotifier}; +use signet_node_types::{HostNotification, HostNotifier, RevertRange}; use signet_rpc::{ ChainNotifier, NewBlockNotification, RemovedBlock, ReorgNotification, RpcServerGuard, ServeConfig, StorageRpcConfig, @@ -222,7 +222,7 @@ where /// /// Returns `true` if any rollup state changed. #[instrument(parent = None, skip_all, fields( - reverted = notification.reverted_chain().map(|c| c.len()).unwrap_or_default(), + reverted = notification.revert_range().map(|r| r.len()).unwrap_or_default(), committed = notification.committed_chain().map(|c| c.len()).unwrap_or_default(), ))] pub async fn on_notification( @@ -234,9 +234,9 @@ where let mut changed = false; // NB: REVERTS MUST RUN FIRST - if let Some(chain) = notification.reverted_chain() { + if let Some(range) = notification.revert_range() { changed |= - self.on_host_revert(chain).await.wrap_err("error encountered during revert")?; + self.on_host_revert(range).await.wrap_err("error encountered during revert")?; } if let Some(chain) = notification.committed_chain() { @@ -385,19 +385,36 @@ where /// Called when the host chain has reverted a block or set of blocks. /// /// Returns `true` if any rollup state was unwound. + /// + /// # Errors + /// + /// Returns an error if the revert range is inconsistent with stored + /// state — i.e. the range tip does not cover the node's current + /// rollup tip. #[instrument(skip_all, fields( - first = chain.first_number(), - tip = chain.tip_number(), + first = range.first(), + tip = range.tip(), ))] - pub async fn on_host_revert(&self, chain: &Arc) -> eyre::Result { - let tip = chain.tip_number(); - let first = chain.first_number(); + pub async fn on_host_revert(&self, range: RevertRange) -> eyre::Result { + let tip = range.tip(); + let first = range.first(); // If the end is before the RU genesis, nothing to do. if tip <= self.constants.host_deploy_height() { return Ok(false); } + // Validate that the revert range is consistent with our stored + // state: the range tip must be at or above the host block that + // produced our current rollup tip. + let rollup_tip = self.last_rollup_block()?; + let range_tip_ru = self.constants.host_block_to_rollup_block_num(tip).unwrap_or_default(); + eyre::ensure!( + range_tip_ru >= rollup_tip, + "revert range tip (host {tip}, rollup {range_tip_ru}) \ + does not cover stored rollup tip ({rollup_tip})" + ); + // Target is the block BEFORE the first block in the chain, or 0. let target = self .constants From 3a0f4d5490e2cf7a0a1fa05a5cb7ee81ad26aeb6 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Mar 2026 10:50:31 -0400 Subject: [PATCH 4/4] fix: use ok_or_eyre for clearer error on unmapped revert range tip Instead of silently defaulting to 0 via unwrap_or_default(), return an explicit error when the revert range tip doesn't map to a rollup block. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/node/src/node.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index 3537916..ea045fc 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -1,6 +1,6 @@ use crate::{NodeStatus, metrics}; use alloy::consensus::BlockHeader; -use eyre::Context; +use eyre::{Context, OptionExt}; use signet_blobber::CacheHandle; use signet_block_processor::{AliasOracleFactory, SignetBlockProcessorV1}; use signet_evm::EthereumHardfork; @@ -408,7 +408,10 @@ where // state: the range tip must be at or above the host block that // produced our current rollup tip. let rollup_tip = self.last_rollup_block()?; - let range_tip_ru = self.constants.host_block_to_rollup_block_num(tip).unwrap_or_default(); + let range_tip_ru = self + .constants + .host_block_to_rollup_block_num(tip) + .ok_or_eyre("revert range tip does not map to a rollup block number")?; eyre::ensure!( range_tip_ru >= rollup_tip, "revert range tip (host {tip}, rollup {range_tip_ru}) \