Skip to content

Commit 384fc12

Browse files
prestwichclaude
andcommitted
feat: add signet-host-reth crate with RethHostNotifier
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6663b5e commit 384fc12

16 files changed

Lines changed: 633 additions & 72 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ incremental = false
3737
signet-blobber = { version = "0.16.0-rc.7", path = "crates/blobber" }
3838
signet-block-processor = { version = "0.16.0-rc.7", path = "crates/block-processor" }
3939
signet-genesis = { version = "0.16.0-rc.7", path = "crates/genesis" }
40+
signet-host-reth = { version = "0.16.0-rc.7", path = "crates/host-reth" }
4041
signet-node = { version = "0.16.0-rc.7", path = "crates/node" }
4142
signet-node-config = { version = "0.16.0-rc.7", path = "crates/node-config" }
4243
signet-node-tests = { version = "0.16.0-rc.7", path = "crates/node-tests" }

crates/host-reth/Cargo.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[package]
2+
name = "signet-host-reth"
3+
description = "Reth ExEx implementation of the `HostNotifier` trait for signet-node."
4+
version.workspace = true
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
authors.workspace = true
8+
license.workspace = true
9+
homepage.workspace = true
10+
repository.workspace = true
11+
12+
[dependencies]
13+
signet-node-types.workspace = true
14+
signet-blobber.workspace = true
15+
signet-extract.workspace = true
16+
signet-rpc.workspace = true
17+
signet-block-processor.workspace = true
18+
signet-types.workspace = true
19+
20+
alloy.workspace = true
21+
reth.workspace = true
22+
reth-exex.workspace = true
23+
reth-node-api.workspace = true
24+
reth-stages-types.workspace = true
25+
26+
eyre.workspace = true
27+
futures-util.workspace = true
28+
thiserror.workspace = true
29+
tokio.workspace = true
30+
tracing.workspace = true

crates/host-reth/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# signet-host-reth
2+
3+
Reth ExEx implementation of the `HostNotifier` trait for signet-node.

crates/host-reth/src/alias.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use alloy::{consensus::constants::KECCAK_EMPTY, primitives::Address};
2+
use core::{
3+
fmt,
4+
future::{self, Future},
5+
};
6+
use eyre::OptionExt;
7+
use reth::providers::{StateProviderBox, StateProviderFactory};
8+
use signet_block_processor::{AliasOracle, AliasOracleFactory};
9+
10+
/// An [`AliasOracle`] backed by a reth [`StateProviderBox`].
11+
///
12+
/// Checks whether an address has non-delegation bytecode, indicating it
13+
/// should be aliased during transaction processing.
14+
pub struct RethAliasOracle(StateProviderBox);
15+
16+
impl fmt::Debug for RethAliasOracle {
17+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18+
f.debug_struct("RethAliasOracle").finish_non_exhaustive()
19+
}
20+
}
21+
22+
impl RethAliasOracle {
23+
/// Synchronously check whether the given address should be aliased.
24+
fn check_alias(&self, address: Address) -> eyre::Result<bool> {
25+
let Some(acct) = self.0.basic_account(&address)? else { return Ok(false) };
26+
// Get the bytecode hash for this account.
27+
let bch = match acct.bytecode_hash {
28+
Some(hash) => hash,
29+
// No bytecode hash; not a contract.
30+
None => return Ok(false),
31+
};
32+
// No code at this address.
33+
if bch == KECCAK_EMPTY {
34+
return Ok(false);
35+
}
36+
// Fetch the code associated with this bytecode hash.
37+
let code = self
38+
.0
39+
.bytecode_by_hash(&bch)?
40+
.ok_or_eyre("code not found. This indicates a corrupted database")?;
41+
42+
// If not a 7702 delegation contract, alias it.
43+
Ok(!code.is_eip7702())
44+
}
45+
}
46+
47+
impl AliasOracle for RethAliasOracle {
48+
fn should_alias(&self, address: Address) -> impl Future<Output = eyre::Result<bool>> + Send {
49+
future::ready(self.check_alias(address))
50+
}
51+
}
52+
53+
/// An [`AliasOracleFactory`] backed by a `Box<dyn StateProviderFactory>`.
54+
///
55+
/// Creates [`RethAliasOracle`] instances from the latest host chain state.
56+
pub struct RethAliasOracleFactory(Box<dyn StateProviderFactory>);
57+
58+
impl fmt::Debug for RethAliasOracleFactory {
59+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60+
f.debug_struct("RethAliasOracleFactory").finish_non_exhaustive()
61+
}
62+
}
63+
64+
impl RethAliasOracleFactory {
65+
/// Create a new [`RethAliasOracleFactory`] from a boxed state provider
66+
/// factory.
67+
pub fn new(provider: Box<dyn StateProviderFactory>) -> Self {
68+
Self(provider)
69+
}
70+
}
71+
72+
impl AliasOracleFactory for RethAliasOracleFactory {
73+
type Oracle = RethAliasOracle;
74+
75+
fn create(&self) -> eyre::Result<Self::Oracle> {
76+
// We use `Latest` rather than a pinned host height because pinning
77+
// would require every node to be an archive node, which is impractical.
78+
//
79+
// This is safe because alias status is stable across blocks: an EOA
80+
// cannot become a non-delegation contract without a birthday attack
81+
// (c.f. EIP-3607), and EIP-7702 delegations are excluded by
82+
// `is_eip7702()`. Even in the (computationally infeasible ~2^80)
83+
// birthday attack scenario, the result is a benign false-positive
84+
// (over-aliasing), never a dangerous false-negative.
85+
self.0
86+
.state_by_block_number_or_tag(alloy::eips::BlockNumberOrTag::Latest)
87+
.map(RethAliasOracle)
88+
.map_err(Into::into)
89+
}
90+
}

crates/host-reth/src/chain.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use alloy::{consensus::Block, consensus::BlockHeader};
2+
use reth::primitives::{EthPrimitives, RecoveredBlock};
3+
use reth::providers::Chain;
4+
use signet_blobber::RecoveredBlockShim;
5+
use signet_extract::{BlockAndReceipts, Extractable};
6+
use signet_types::primitives::TransactionSigned;
7+
use std::sync::Arc;
8+
9+
/// Reth's recovered block type, aliased for readability.
10+
type RethRecovered = RecoveredBlock<Block<TransactionSigned>>;
11+
12+
/// An owning wrapper around reth's [`Chain`] that implements [`Extractable`]
13+
/// with O(1) metadata accessors.
14+
///
15+
/// # Usage
16+
///
17+
/// `RethChain` is typically obtained from [`HostNotification`] events, not
18+
/// constructed directly. To extract blocks and receipts:
19+
///
20+
/// ```ignore
21+
/// # // Requires reth ExEx runtime — shown for API illustration only.
22+
/// use signet_extract::Extractable;
23+
///
24+
/// fn process(chain: &RethChain) {
25+
/// for bar in chain.blocks_and_receipts() {
26+
/// println!("block receipts: {}", bar.receipts.len());
27+
/// }
28+
/// }
29+
/// ```
30+
///
31+
/// [`HostNotification`]: signet_node_types::HostNotification
32+
#[derive(Debug)]
33+
pub struct RethChain {
34+
inner: Arc<Chain<EthPrimitives>>,
35+
}
36+
37+
impl RethChain {
38+
/// Wrap a reth chain.
39+
pub const fn new(chain: Arc<Chain<EthPrimitives>>) -> Self {
40+
Self { inner: chain }
41+
}
42+
}
43+
44+
impl Extractable for RethChain {
45+
type Block = RecoveredBlockShim;
46+
type Receipt = reth::primitives::Receipt;
47+
48+
fn blocks_and_receipts(
49+
&self,
50+
) -> impl Iterator<Item = BlockAndReceipts<'_, Self::Block, Self::Receipt>> {
51+
self.inner.blocks_and_receipts().map(|(block, receipts)| {
52+
// Compile-time check: RecoveredBlockShim must have the same
53+
// layout as RethRecovered (guaranteed by #[repr(transparent)]
54+
// on RecoveredBlockShim in signet-blobber/src/shim.rs).
55+
const {
56+
assert!(
57+
size_of::<RecoveredBlockShim>() == size_of::<RethRecovered>(),
58+
"RecoveredBlockShim layout diverged from RethRecovered"
59+
);
60+
assert!(
61+
align_of::<RecoveredBlockShim>() == align_of::<RethRecovered>(),
62+
"RecoveredBlockShim alignment diverged from RethRecovered"
63+
);
64+
}
65+
// SAFETY: `RecoveredBlockShim` is `#[repr(transparent)]` over
66+
// `RethRecovered`, so these types have identical memory layouts.
67+
// The lifetime of the reference is tied to `self.inner` (the
68+
// `Arc<Chain>`), which outlives the returned iterator.
69+
let block =
70+
unsafe { std::mem::transmute::<&RethRecovered, &RecoveredBlockShim>(block) };
71+
BlockAndReceipts { block, receipts }
72+
})
73+
}
74+
75+
fn first_number(&self) -> u64 {
76+
self.inner.first().number()
77+
}
78+
79+
fn tip_number(&self) -> u64 {
80+
self.inner.tip().number()
81+
}
82+
83+
fn len(&self) -> usize {
84+
self.inner.len()
85+
}
86+
}

crates/host-reth/src/config.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use reth::args::RpcServerArgs;
2+
use signet_rpc::{ServeConfig, StorageRpcConfig};
3+
use std::net::SocketAddr;
4+
5+
/// Extract [`StorageRpcConfig`] values from reth's host RPC settings.
6+
///
7+
/// Fields with no reth equivalent retain their defaults.
8+
pub fn rpc_config_from_args(args: &RpcServerArgs) -> StorageRpcConfig {
9+
let gpo = &args.gas_price_oracle;
10+
StorageRpcConfig::builder()
11+
.rpc_gas_cap(args.rpc_gas_cap)
12+
.max_tracing_requests(args.rpc_max_tracing_requests)
13+
.gas_oracle_block_count(u64::from(gpo.blocks))
14+
.gas_oracle_percentile(f64::from(gpo.percentile))
15+
.ignore_price(Some(u128::from(gpo.ignore_price)))
16+
.max_price(Some(u128::from(gpo.max_price)))
17+
.build()
18+
}
19+
20+
/// Convert reth [`RpcServerArgs`] into a reth-free [`ServeConfig`].
21+
pub fn serve_config_from_args(args: &RpcServerArgs) -> ServeConfig {
22+
let http =
23+
if args.http { vec![SocketAddr::from((args.http_addr, args.http_port))] } else { vec![] };
24+
let ws = if args.ws { vec![SocketAddr::from((args.ws_addr, args.ws_port))] } else { vec![] };
25+
let ipc = if !args.ipcdisable { Some(args.ipcpath.clone()) } else { None };
26+
27+
ServeConfig {
28+
http,
29+
http_cors: args.http_corsdomain.clone(),
30+
ws,
31+
ws_cors: args.ws_allowed_origins.clone(),
32+
ipc,
33+
}
34+
}
35+
36+
#[cfg(test)]
37+
mod tests {
38+
use crate::config::{rpc_config_from_args, serve_config_from_args};
39+
use reth::args::RpcServerArgs;
40+
41+
#[test]
42+
fn rpc_config_from_default_args() {
43+
let args = RpcServerArgs::default();
44+
let gpo = &args.gas_price_oracle;
45+
let config = rpc_config_from_args(&args);
46+
47+
assert_eq!(config.rpc_gas_cap, args.rpc_gas_cap);
48+
assert_eq!(config.max_tracing_requests, args.rpc_max_tracing_requests);
49+
assert_eq!(config.gas_oracle_block_count, u64::from(gpo.blocks));
50+
assert_eq!(config.gas_oracle_percentile, f64::from(gpo.percentile));
51+
assert_eq!(config.ignore_price, Some(u128::from(gpo.ignore_price)));
52+
assert_eq!(config.max_price, Some(u128::from(gpo.max_price)));
53+
}
54+
55+
#[test]
56+
fn serve_config_http_disabled_by_default() {
57+
let args = RpcServerArgs::default();
58+
let config = serve_config_from_args(&args);
59+
60+
assert!(config.http.is_empty());
61+
assert!(config.ws.is_empty());
62+
}
63+
64+
#[test]
65+
fn serve_config_http_enabled() {
66+
let args = RpcServerArgs { http: true, ..Default::default() };
67+
let config = serve_config_from_args(&args);
68+
69+
assert_eq!(config.http.len(), 1);
70+
assert_eq!(config.http[0].port(), args.http_port);
71+
}
72+
73+
#[test]
74+
fn serve_config_ws_enabled() {
75+
let args = RpcServerArgs { ws: true, ..Default::default() };
76+
let config = serve_config_from_args(&args);
77+
78+
assert_eq!(config.ws.len(), 1);
79+
assert_eq!(config.ws[0].port(), args.ws_port);
80+
}
81+
82+
#[test]
83+
fn serve_config_ipc_enabled_by_default() {
84+
let args = RpcServerArgs::default();
85+
let config = serve_config_from_args(&args);
86+
87+
assert!(config.ipc.is_some());
88+
}
89+
}

crates/host-reth/src/error.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use reth_exex::ExExEvent;
2+
3+
/// Errors from the [`RethHostNotifier`](crate::RethHostNotifier).
4+
#[derive(Debug, thiserror::Error)]
5+
pub enum RethHostError {
6+
/// A notification stream error forwarded from reth.
7+
#[error("notification stream error: {0}")]
8+
Notification(#[source] Box<dyn core::error::Error + Send + Sync>),
9+
/// The provider failed to look up a header or block tag.
10+
#[error("provider error: {0}")]
11+
Provider(#[from] reth::providers::ProviderError),
12+
/// Failed to send an ExEx event back to the host.
13+
#[error("failed to send ExEx event")]
14+
EventSend(#[from] tokio::sync::mpsc::error::SendError<ExExEvent>),
15+
/// A required header was missing from the provider.
16+
#[error("missing header for block {0}")]
17+
MissingHeader(u64),
18+
}
19+
20+
impl From<eyre::Report> for RethHostError {
21+
fn from(e: eyre::Report) -> Self {
22+
Self::Notification(e.into())
23+
}
24+
}

crates/host-reth/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#![doc = include_str!("../README.md")]
2+
#![warn(
3+
missing_copy_implementations,
4+
missing_debug_implementations,
5+
missing_docs,
6+
unreachable_pub,
7+
clippy::missing_const_for_fn,
8+
rustdoc::all
9+
)]
10+
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
11+
#![deny(unused_must_use, rust_2018_idioms)]
12+
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
13+
14+
mod alias;
15+
pub use alias::{RethAliasOracle, RethAliasOracleFactory};
16+
mod error;
17+
pub use error::RethHostError;
18+
19+
mod chain;
20+
pub use chain::RethChain;
21+
22+
mod config;
23+
pub use config::{rpc_config_from_args, serve_config_from_args};
24+
25+
mod notifier;
26+
pub use notifier::{DecomposedContext, RethHostNotifier, decompose_exex_context};

0 commit comments

Comments
 (0)