Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
30 changes: 30 additions & 0 deletions crates/host-reth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions crates/host-reth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# signet-host-reth

Reth ExEx implementation of the `HostNotifier` trait for signet-node.
42 changes: 9 additions & 33 deletions crates/node/src/alias.rs → crates/host-reth/src/alias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ impl RethAliasOracle {

impl AliasOracle for RethAliasOracle {
fn should_alias(&self, address: Address) -> impl Future<Output = eyre::Result<bool>> + Send {
let result = self.check_alias(address);
future::ready(result)
future::ready(self.check_alias(address))
}
}

Expand All @@ -74,38 +73,15 @@ impl AliasOracleFactory for RethAliasOracleFactory {
type Oracle = RethAliasOracle;

fn create(&self) -> eyre::Result<Self::Oracle> {
// ## Why `Latest` instead of a pinned host height
// We use `Latest` rather than a pinned host height because pinning
// would require every node to be an archive node, which is impractical.
//
// 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.
// 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)
Expand Down
90 changes: 90 additions & 0 deletions crates/host-reth/src/chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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<Block<TransactionSigned>>;

/// 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<Chain<EthPrimitives>>,
}

impl RethChain {
/// Wrap a reth chain.
pub const fn new(chain: Arc<Chain<EthPrimitives>>) -> Self {
Self { inner: chain }
}
}

impl Extractable for RethChain {
type Block = RecoveredBlockShim;
type Receipt = reth::primitives::Receipt;

fn blocks_and_receipts(
&self,
) -> impl Iterator<Item = BlockAndReceipts<'_, Self::Block, Self::Receipt>> {
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::<RecoveredBlockShim>() == size_of::<RethRecovered>(),
"RecoveredBlockShim layout diverged from RethRecovered"
);
assert!(
align_of::<RecoveredBlockShim>() == align_of::<RethRecovered>(),
"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<Chain>`), which outlives the returned iterator.
let block =
unsafe { std::mem::transmute::<&RethRecovered, &RecoveredBlockShim>(block) };
BlockAndReceipts { block, receipts }
})
}

fn first_number(&self) -> Option<u64> {
Some(self.inner.first().number())
}

fn tip_number(&self) -> Option<u64> {
Some(self.inner.tip().number())
}

fn len(&self) -> usize {
self.inner.len()
}

fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
89 changes: 89 additions & 0 deletions crates/host-reth/src/config.rs
Original file line number Diff line number Diff line change
@@ -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());
}
}
24 changes: 24 additions & 0 deletions crates/host-reth/src/error.rs
Original file line number Diff line number Diff line change
@@ -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<dyn core::error::Error + Send + Sync>),
/// 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<ExExEvent>),
/// A required header was missing from the provider.
#[error("missing header for block {0}")]
MissingHeader(u64),
}

impl From<eyre::Report> for RethHostError {
fn from(e: eyre::Report) -> Self {
Self::Notification(e.into())
}
}
26 changes: 26 additions & 0 deletions crates/host-reth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Loading