Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ nybbles = { version = "0.4.2", default-features = false }
once_cell = { version = "1.19", default-features = false, features = ["critical-section"] }
parking_lot = "0.12"
paste = "1.0"
prost = "0.13"
rand = "0.9"
rayon = "1.7"
rustc-hash = { version = "2.0", default-features = false }
Expand Down Expand Up @@ -645,6 +646,7 @@ hyper-util = "0.1.5"
pin-project = "1.0.12"
reqwest = { version = "0.12", default-features = false }
tracing-futures = "0.2"
tonic = { version = "0.12", default-features = false }
tower = "0.5"
tower-http = "0.6"

Expand Down
45 changes: 35 additions & 10 deletions bin/seismic-reth/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
#![allow(missing_docs)]

use clap::Parser;
use reth_node_core::args::{EnclaveArgs, ScreeningArgs};
use reth_seismic_cli::{chainspec::SeismicChainSpecParser, Cli};
use reth_seismic_node::{enclave::boot_enclave_and_fetch_keys, node::SeismicNode};

/// Combined CLI extension args for the Seismic node.
///
/// Wraps both enclave and address screening configuration.
#[derive(Debug, Clone, clap::Args)]
struct SeismicExtArgs {
/// Enclave configuration.
#[command(flatten)]
enclave: EnclaveArgs,
/// Address screening configuration.
#[command(flatten)]
screening: ScreeningArgs,
}

impl AsRef<EnclaveArgs> for SeismicExtArgs {
fn as_ref(&self) -> &EnclaveArgs {
&self.enclave
}
}

fn main() {
// Enable backtraces unless we explicitly set RUST_BACKTRACE
if std::env::var_os("RUST_BACKTRACE").is_none() {
Expand All @@ -12,18 +32,23 @@ fn main() {

reth_cli_util::sigsegv_handler::install();

if let Err(err) = Cli::<SeismicChainSpecParser>::parse().run(|builder, encl| async move {
// Boot enclave and fetch purpose keys BEFORE building node components
let purpose_keys = boot_enclave_and_fetch_keys(&encl).await;
if let Err(err) =
Cli::<SeismicChainSpecParser, SeismicExtArgs>::parse().run(|builder, ext| async move {
// Boot enclave and fetch purpose keys BEFORE building node components
let purpose_keys = boot_enclave_and_fetch_keys(&ext).await;

// Store purpose keys in global static storage before building the node.
// Seismic RPC modules (seismic_ namespace + eth_ overrides) are registered
// in SeismicAddOns::launch_add_ons, which reads keys via get_purpose_keys().
reth_seismic_node::purpose_keys::init_purpose_keys(purpose_keys);
// Store purpose keys in global static storage before building the node.
// Seismic RPC modules (seismic_ namespace + eth_ overrides) are registered
// in SeismicAddOns::launch_add_ons, which reads keys via get_purpose_keys().
reth_seismic_node::purpose_keys::init_purpose_keys(purpose_keys);

let node = builder.node(SeismicNode::default()).launch_with_debug_capabilities().await?;
node.node_exit_future.await
}) {
let node = builder
.node(SeismicNode::new(Some(ext.screening)))
.launch_with_debug_capabilities()
.await?;
node.node_exit_future.await
})
{
eprintln!("Error: {err:?}");
std::process::exit(1);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/ethereum/primitives/src/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,8 @@ pub(super) mod serde_bincode_compat {
mod compact {
use super::*;
use reth_codecs::{
__private::{modular_bitfield::prelude::*, Buf},
Compact,
__private::{modular_bitfield::prelude::*, Buf},
};

impl Receipt {
Expand Down
4 changes: 4 additions & 0 deletions crates/node/core/src/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
mod enclave;
pub use enclave::EnclaveArgs;

/// ScreeningArgs struct for configuring address screening
mod screening;
pub use screening::ScreeningArgs;

/// NetworkArg struct for configuring the network
mod network;
pub use network::{DiscoveryArgs, NetworkArgs};
Expand Down
124 changes: 124 additions & 0 deletions crates/node/core/src/args/screening.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! clap [Args](clap::Args) for address screening configuration.

use clap::Args;

/// Parameters for configuring the ECSD address screening sidecar.
///
/// When enabled, transactions are screened against a blocklist via the ECSD
/// (Ethereum Compliance Screening Daemon) gRPC service before pool admission.
#[derive(Debug, Clone, Args, PartialEq, Eq)]
#[command(next_help_heading = "Address Screening")]
pub struct ScreeningArgs {
/// Enable address screening via ECSD sidecar.
#[arg(long = "screening.enable", default_value_t = false)]
pub enable: bool,

/// gRPC endpoint of the ECSD sidecar.
#[arg(long = "screening.endpoint", default_value = "http://127.0.0.1:9090")]
pub endpoint: String,

/// Request timeout in milliseconds.
#[arg(long = "screening.timeout-ms", default_value_t = 100)]
pub timeout_ms: u64,

/// Behavior when ECSD is unavailable: "open" or "closed".
///
/// - "open": transactions pass through when ECSD is unreachable (permissive)
/// - "closed": transactions are rejected when ECSD is unreachable (restrictive)
#[arg(
long = "screening.fail-mode",
default_value = "open",
value_parser = clap::builder::PossibleValuesParser::new(["open", "closed"])
)]
pub fail_mode: String,
}

impl Default for ScreeningArgs {
fn default() -> Self {
Self {
enable: false,
endpoint: "http://127.0.0.1:9090".to_string(),
timeout_ms: 100,
fail_mode: "open".to_string(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use clap::{Args, Parser};

/// A helper type to parse Args more easily
#[derive(Debug, Parser)]
struct CommandParser<T: Args> {
#[command(flatten)]
args: T,
}

#[test]
fn test_default_screening_args() {
let args = CommandParser::<ScreeningArgs>::parse_from(["reth node"]).args;

assert!(!args.enable);
assert_eq!(args.endpoint, "http://127.0.0.1:9090");
assert_eq!(args.timeout_ms, 100);
assert_eq!(args.fail_mode, "open");
}

#[test]
fn test_screening_args_all_flags() {
let args = CommandParser::<ScreeningArgs>::parse_from([
"reth node",
"--screening.enable",
"--screening.endpoint",
"http://10.0.0.5:9090",
"--screening.timeout-ms",
"200",
"--screening.fail-mode",
"closed",
])
.args;

assert!(args.enable);
assert_eq!(args.endpoint, "http://10.0.0.5:9090");
assert_eq!(args.timeout_ms, 200);
assert_eq!(args.fail_mode, "closed");
}

#[test]
fn test_screening_args_invalid_fail_mode() {
// Typo: "close" instead of "closed" should fail fast
let result = CommandParser::<ScreeningArgs>::try_parse_from([
"reth node",
"--screening.fail-mode",
"close",
]);

assert!(result.is_err(), "Expected parsing to fail for invalid value 'close'");
let err = result.unwrap_err().to_string();
assert!(
err.contains("invalid value 'close'"),
"Error message should mention the invalid value: {}",
err
);
}

#[test]
fn test_screening_args_invalid_fail_mode_permissive() {
// Invalid value should fail fast
let result = CommandParser::<ScreeningArgs>::try_parse_from([
"reth node",
"--screening.fail-mode",
"permissive",
]);

assert!(result.is_err(), "Expected parsing to fail for invalid value 'permissive'");
let err = result.unwrap_err().to_string();
assert!(
err.contains("invalid value 'permissive'"),
"Error message should mention the invalid value: {}",
err
);
}
}
1 change: 1 addition & 0 deletions crates/seismic/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ seismic-alloy-rpc-types.workspace = true
seismic-revm = { workspace = true }

# misc
futures-util.workspace = true
serde.workspace = true
eyre.workspace = true
tracing.workspace = true
Expand Down
61 changes: 54 additions & 7 deletions crates/seismic/node/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,20 @@ use crate::{purpose_keys::get_purpose_keys, seismic_evm_config};
/// Storage implementation for Seismic.
pub type SeismicStorage = EthStorage<SeismicTransactionSigned>;

#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
/// Type configuration for a regular Seismic node.
pub struct SeismicNode;
pub struct SeismicNode {
/// Optional screening args for address screening via ECSD sidecar.
pub screening_args: Option<reth_node_core::args::ScreeningArgs>,
}

impl SeismicNode {
/// Creates a new `SeismicNode` with optional address screening configuration.
pub const fn new(screening_args: Option<reth_node_core::args::ScreeningArgs>) -> Self {
Self { screening_args }
}

/// Returns the components for the given [`EnclaveArgs`].
pub fn components<Node>(
&self,
Expand All @@ -88,7 +96,7 @@ impl SeismicNode {
{
ComponentsBuilder::default()
.node_types::<Node>()
.pool(SeismicPoolBuilder::default())
.pool(SeismicPoolBuilder { screening_args: self.screening_args.clone() })
.executor(SeismicExecutorBuilder::default())
.payload(BasicPayloadServiceBuilder::<SeismicPayloadBuilder>::default())
.network(SeismicNetworkBuilder::default())
Expand Down Expand Up @@ -457,9 +465,12 @@ where
///
/// This contains various settings that can be configured and take precedence over the node's
/// config.
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct SeismicPoolBuilder;
pub struct SeismicPoolBuilder {
/// Optional screening args for ECSD address screening.
pub screening_args: Option<reth_node_core::args::ScreeningArgs>,
}

impl<Node> PoolBuilder<Node> for SeismicPoolBuilder
where
Expand Down Expand Up @@ -516,8 +527,44 @@ where
.disable_balance_check()
.build_with_tasks(ctx.task_executor().clone(), blob_store.clone());

// Wrap the eth validator with seismic-specific validation
let validator = eth_validator.map(reth_seismic_txpool::SeismicTransactionValidator::new);
// Wrap the eth validator with seismic-specific validation (protocol invariants)
let seismic_validator =
eth_validator.map(reth_seismic_txpool::SeismicTransactionValidator::new);

// Conditionally wrap with address screening (operator policy), using Either
// for uniform type: Left = no screening, Right = with screening
let validator = match &self.screening_args {
Some(args) if args.enable => {
#[allow(clippy::expect_used)]
let screening_client =
reth_seismic_txpool::screening::ScreeningClientBuilder::new(&args.endpoint)
.timeout(std::time::Duration::from_millis(args.timeout_ms))
.fail_mode(
args.fail_mode
.parse()
.expect("fail_mode validated by clap to be 'open' or 'closed'"),
)
.build()?;
tracing::info!(
target: "reth::cli",
endpoint = %args.endpoint,
timeout_ms = %args.timeout_ms,
fail_mode = %args.fail_mode,
"Address screening enabled via ECSD sidecar"
);
let purpose_keys = crate::purpose_keys::get_purpose_keys();
seismic_validator.map(|inner| {
futures_util::future::Either::Right(
reth_seismic_txpool::ScreeningTransactionValidator::new(
inner,
screening_client.clone(),
purpose_keys,
),
)
})
}
_ => seismic_validator.map(futures_util::future::Either::Left),
};

let transaction_pool = reth_transaction_pool::Pool::new(
validator,
Expand Down
23 changes: 22 additions & 1 deletion crates/seismic/txpool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,40 @@ seismic-alloy-consensus.workspace = true
# reth
reth-chainspec.workspace = true
reth-execution-types.workspace = true
reth-metrics.workspace = true
reth-primitives-traits.workspace = true
reth-provider.workspace = true
reth-transaction-pool = {workspace = true, features = ["serde", "reth-codec", "serde-bincode-compat"]}

# seismic
reth-seismic-primitives = {workspace = true, features = ["serde", "reth-codec", "serde-bincode-compat"]}
seismic-enclave.workspace = true

# grpc
prost.workspace = true
tonic = { workspace = true, features = ["transport", "codegen", "prost", "channel"] }

# async
futures-util.workspace = true
tokio = { workspace = true, features = ["sync", "time"] }

# misc
c-kzg.workspace = true
derive_more.workspace = true
metrics.workspace = true
tracing.workspace = true

[dev-dependencies]
reth-seismic-chainspec.workspace = true
reth-provider = { workspace = true, features = ["test-utils"] }
tokio.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net"] }
criterion.workspace = true
tokio-stream.workspace = true

[[bench]]
name = "calldata_extraction"
harness = false

[[bench]]
name = "screening"
harness = false
Loading