From 507c3ccaee3901970e7c0ad2fc823169604deea0 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 17:27:26 -0300 Subject: [PATCH 01/33] Integrate Phylax Credible Layer (Circuit Breaker) into the L2 sequencer Add optional transaction validation against the Credible Layer Assertion Enforcer sidecar during L2 block building. When enabled via --circuit-breaker-url, each candidate transaction is sent to the sidecar via gRPC. If the sidecar returns ASSERTION_FAILED, the transaction is dropped from the block. On any error or timeout, the transaction is included (permissive / liveness over safety). Changes: - gRPC client (tonic/prost) implementing sidecar.proto StreamEvents + GetTransaction and aeges.proto VerifyTransaction for optional mempool pre-filtering - Block producer sends NewIteration, Transaction, and CommitHead events via a persistent bidirectional gRPC stream with automatic reconnection - L2 WebSocket server with eth_subscribe("newHeads") support, required by the sidecar for live chain state tracking - prestateTracer (diff mode) for debug_traceBlockByNumber, required by the sidecar to build its local state database - Fix eth_getLogs to accept requests without topics parameter (spec compliance) - CLI flags: --circuit-breaker-url, --circuit-breaker-aeges-url, --circuit-breaker-state-oracle, --l2.ws-enabled/addr/port - Test contracts (OwnableTarget.sol, TestOwnershipAssertion.sol), mock sidecar binary, and e2e validation script - Documentation with verified 10-step end-to-end guide covering L1 setup, L2 with circuit breaker, State Oracle deployment via Phylax Forge scripts, assertion registration via DA + State Oracle, real sidecar with indexer, and transaction rejection/inclusion verification Tested end-to-end with the real Phylax sidecar Docker image, assertion-da, sidecar-indexer, and credible-layer-contracts Forge deployment scripts. --- cmd/ethrex/l2/initializers.rs | 97 ++++ cmd/ethrex/l2/options.rs | 38 +- crates/blockchain/tracing.rs | 73 ++- crates/common/tracing.rs | 286 ++++++++++- crates/l2/Cargo.toml | 9 + crates/l2/Makefile | 27 + crates/l2/build.rs | 13 +- .../src/circuit_breaker/OwnableTarget.sol | 33 ++ .../contracts/src/circuit_breaker/README.md | 69 +++ .../TestOwnershipAssertion.sol | 52 ++ crates/l2/l2.rs | 4 +- crates/l2/networking/rpc/rpc.rs | 327 +++++++++--- crates/l2/proto/aeges.proto | 67 +++ crates/l2/proto/sidecar.proto | 198 +++++++ crates/l2/scripts/circuit_breaker_e2e.sh | 150 ++++++ crates/l2/sequencer/block_producer.rs | 105 +++- .../block_producer/payload_builder.rs | 121 +++++ crates/l2/sequencer/circuit_breaker/aeges.rs | 233 +++++++++ crates/l2/sequencer/circuit_breaker/client.rs | 424 +++++++++++++++ crates/l2/sequencer/circuit_breaker/errors.rs | 15 + .../circuit_breaker/mock_sidecar/main.rs | 229 +++++++++ crates/l2/sequencer/circuit_breaker/mod.rs | 25 + crates/l2/sequencer/configs.rs | 15 + crates/l2/sequencer/mod.rs | 1 + crates/l2/sidecar-config.template.json | 47 ++ crates/networking/rpc/eth/logs.rs | 14 +- crates/networking/rpc/tracing.rs | 64 ++- crates/vm/Cargo.toml | 1 + crates/vm/backends/levm/tracing.rs | 135 +++++ crates/vm/tracing.rs | 29 +- docs/CLI.md | 30 ++ docs/l2/circuit_breaker.md | 484 ++++++++++++++++++ 32 files changed, 3314 insertions(+), 101 deletions(-) create mode 100644 crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol create mode 100644 crates/l2/contracts/src/circuit_breaker/README.md create mode 100644 crates/l2/contracts/src/circuit_breaker/TestOwnershipAssertion.sol create mode 100644 crates/l2/proto/aeges.proto create mode 100644 crates/l2/proto/sidecar.proto create mode 100755 crates/l2/scripts/circuit_breaker_e2e.sh create mode 100644 crates/l2/sequencer/circuit_breaker/aeges.rs create mode 100644 crates/l2/sequencer/circuit_breaker/client.rs create mode 100644 crates/l2/sequencer/circuit_breaker/errors.rs create mode 100644 crates/l2/sequencer/circuit_breaker/mock_sidecar/main.rs create mode 100644 crates/l2/sequencer/circuit_breaker/mod.rs create mode 100644 crates/l2/sidecar-config.template.json create mode 100644 docs/l2/circuit_breaker.md diff --git a/cmd/ethrex/l2/initializers.rs b/cmd/ethrex/l2/initializers.rs index 444274bddff..366c313187b 100644 --- a/cmd/ethrex/l2/initializers.rs +++ b/cmd/ethrex/l2/initializers.rs @@ -103,6 +103,103 @@ fn get_valid_delegation_addresses(l2_opts: &L2Options) -> Vec
{ addresses } +/// Build an Aeges mempool pre-filter callback from a gRPC endpoint URL. +/// +/// Connects to the Aeges service and wraps the client in a `MempoolFilter` closure +/// that accepts raw transaction bytes and returns `true` if the transaction is allowed. +/// Returns `None` if no URL is provided or if the connection fails (permissive on error). +async fn build_aeges_filter(aeges_url: Option) -> Option { + use ethrex_common::types::Transaction as EthrexTx; + use ethrex_crypto::NativeCrypto; + use ethrex_l2::sequencer::circuit_breaker::{AegesClient, aeges::AegesConfig}; + use ethrex_l2::sequencer::circuit_breaker::aeges_proto::{ + AccessListEntry, Transaction as AegesTransaction, + }; + use ethrex_rlp::decode::RLPDecode; + use std::sync::Arc; + + let url = aeges_url?; + let config = AegesConfig { + aeges_url: url, + ..Default::default() + }; + match AegesClient::connect(config).await { + Ok(client) => { + tracing::info!("Aeges mempool pre-filter connected"); + let client = Arc::new(client); + let filter: ethrex_l2_rpc::MempoolFilter = Arc::new(move |raw: bytes::Bytes| { + let client = client.clone(); + Box::pin(async move { + // Decode the raw tx bytes into an ethrex Transaction. + let tx = match EthrexTx::decode_unfinished(&raw) + .map(|(tx, _)| tx) + .or_else(|_| { + // Try stripping the type prefix byte and decoding. + raw.first() + .filter(|&&b| b <= 0x7f) + .and_then(|_| { + EthrexTx::decode_unfinished(&raw[1..]) + .map(|(tx, _)| tx) + .ok() + }) + .ok_or(ethrex_rlp::error::RLPDecodeError::InvalidLength) + }) { + Ok(tx) => tx, + Err(_) => return true, // Permissive: admit if we can't decode + }; + + let tx_hash = tx.hash(); + let sender = tx.sender(&NativeCrypto).unwrap_or_default(); + let value_bytes = tx.value().to_big_endian(); + let to = match tx.to() { + ethrex_common::types::TxKind::Call(addr) => Some(addr.as_bytes().to_vec()), + ethrex_common::types::TxKind::Create => None, + }; + let access_list = tx + .access_list() + .iter() + .map(|(addr, keys)| AccessListEntry { + address: addr.as_bytes().to_vec(), + storage_keys: keys.iter().map(|k| k.as_bytes().to_vec()).collect(), + }) + .collect(); + #[allow(clippy::as_conversions)] + let aeges_tx = AegesTransaction { + hash: tx_hash.as_bytes().to_vec(), + sender: sender.as_bytes().to_vec(), + to, + value: value_bytes.to_vec(), + nonce: tx.nonce(), + r#type: u8::from(tx.tx_type()) as u32, + chain_id: tx.chain_id(), + payload: tx.data().to_vec(), + gas_limit: tx.gas_limit(), + gas_price: None, + max_fee_per_gas: tx.max_fee_per_gas(), + max_priority_fee_per_gas: tx.max_priority_fee(), + max_fee_per_blob_gas: None, + access_list, + versioned_hashes: tx + .blob_versioned_hashes() + .iter() + .map(|h| h.as_bytes().to_vec()) + .collect(), + code_delegation_list: vec![], + }; + client.verify_transaction(aeges_tx).await + }) + }); + Some(filter) + } + Err(e) => { + tracing::warn!( + "Failed to connect to Aeges service: {e}. Proceeding without pre-filter." + ); + None + } + } +} + pub async fn init_rollup_store(datadir: &Path) -> StoreRollup { #[cfg(feature = "l2-sql")] let engine_type = EngineTypeRollup::SQL; diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index e1bc47dab23..54dea378dee 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -8,7 +8,7 @@ use ethrex_l2::sequencer::utils::resolve_aligned_network; use ethrex_l2::{ BasedConfig, BlockFetcherConfig, BlockProducerConfig, CommitterConfig, EthConfig, L1WatcherConfig, ProofCoordinatorConfig, SequencerConfig, StateUpdaterConfig, - sequencer::configs::{AdminConfig, AlignedConfig, MonitorConfig}, + sequencer::configs::{AdminConfig, AlignedConfig, CircuitBreakerConfig, MonitorConfig}, }; use ethrex_l2_prover::{backend::BackendType, config::ProverConfig}; use ethrex_l2_rpc::signer::{LocalSigner, RemoteSigner, Signer}; @@ -123,6 +123,8 @@ pub struct SequencerOptions { pub admin_opts: AdminOptions, #[clap(flatten)] pub state_updater_opts: StateUpdaterOptions, + #[clap(flatten)] + pub circuit_breaker_opts: CircuitBreakerOptions, #[arg( long = "validium", default_value = "false", @@ -294,6 +296,11 @@ impl TryFrom for SequencerConfig { start_at: opts.state_updater_opts.start_at, l2_head_check_rpc_url: opts.state_updater_opts.l2_head_check_rpc_url, }, + circuit_breaker: CircuitBreakerConfig { + sidecar_url: opts.circuit_breaker_opts.circuit_breaker_url, + aeges_url: opts.circuit_breaker_opts.circuit_breaker_aeges_url, + state_oracle_address: opts.circuit_breaker_opts.circuit_breaker_state_oracle, + }, }) } } @@ -329,6 +336,7 @@ impl SequencerOptions { self.state_updater_opts .populate_with_defaults(&defaults.state_updater_opts); // admin_opts contains only non-optional fields. + // circuit_breaker_opts contains only optional fields, nothing to populate. } } @@ -1097,6 +1105,34 @@ impl Default for AdminOptions { } } +#[derive(Parser, Default, Debug)] +pub struct CircuitBreakerOptions { + #[arg( + long = "circuit-breaker-url", + value_name = "URL", + env = "ETHREX_CIRCUIT_BREAKER_URL", + help = "gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). When set, the circuit breaker integration is enabled.", + help_heading = "Circuit Breaker options" + )] + pub circuit_breaker_url: Option, + #[arg( + long = "circuit-breaker-aeges-url", + value_name = "URL", + env = "ETHREX_CIRCUIT_BREAKER_AEGES_URL", + help = "gRPC endpoint for the Aeges mempool pre-filter service (e.g. http://localhost:8080). When set, Aeges pre-filtering is enabled.", + help_heading = "Circuit Breaker options" + )] + pub circuit_breaker_aeges_url: Option, + #[arg( + long = "circuit-breaker-state-oracle", + value_name = "ADDRESS", + env = "ETHREX_CIRCUIT_BREAKER_STATE_ORACLE", + help = "Address of the already-deployed State Oracle contract on L2. The State Oracle maps protected contracts to their active assertions and is required by the Credible Layer sidecar. Deploy it separately using the Phylax toolchain (see crates/l2/contracts/src/circuit_breaker/README.md).", + help_heading = "Circuit Breaker options" + )] + pub circuit_breaker_state_oracle: Option
, +} + #[derive(Parser)] pub struct ProverClientOptions { #[arg( diff --git a/crates/blockchain/tracing.rs b/crates/blockchain/tracing.rs index 8591c77f0a0..38ec149acd1 100644 --- a/crates/blockchain/tracing.rs +++ b/crates/blockchain/tracing.rs @@ -3,7 +3,11 @@ use std::{ time::Duration, }; -use ethrex_common::{H256, tracing::CallTrace, types::Block}; +use ethrex_common::{ + H256, + tracing::{CallTrace, PrePostState, PrestateTrace}, + types::Block, +}; use ethrex_storage::Store; use ethrex_vm::{Evm, EvmError}; @@ -82,6 +86,73 @@ impl Blockchain { Ok(call_traces) } + /// Outputs the prestate trace for the given transaction. + /// If `diff_mode` is true, returns both pre and post state; otherwise returns only pre state. + /// May need to re-execute blocks in order to rebuild the transaction's prestate, up to the amount given by `reexec`. + pub async fn trace_transaction_prestate( + &self, + tx_hash: H256, + reexec: u32, + timeout: Duration, + diff_mode: bool, + ) -> Result<(PrestateTrace, Option), ChainError> { + let Some((_, block_hash, tx_index)) = + self.storage.get_transaction_location(tx_hash).await? + else { + return Err(ChainError::Custom("Transaction not Found".to_string())); + }; + let tx_index = tx_index as usize; + let Some(block) = self.storage.get_block_by_hash(block_hash).await? else { + return Err(ChainError::Custom("Block not Found".to_string())); + }; + let mut vm = self + .rebuild_parent_state(block.header.parent_hash, reexec) + .await?; + // Run the block until the transaction we want to trace + vm.rerun_block(&block, Some(tx_index))?; + // Trace the transaction + timeout_trace_operation(timeout, move || { + vm.trace_tx_prestate(&block, tx_index, diff_mode) + }) + .await + } + + /// Outputs the prestate trace for each transaction in the block along with the transaction's hash. + /// If `diff_mode` is true, returns both pre and post state per tx; otherwise returns only pre state. + /// May need to re-execute blocks in order to rebuild the block's prestate, up to the amount given by `reexec`. + /// Returns prestate traces from oldest to newest transaction. + pub async fn trace_block_prestate( + &self, + block: Block, + reexec: u32, + timeout: Duration, + diff_mode: bool, + ) -> Result)>, ChainError> { + let mut vm = self + .rebuild_parent_state(block.header.parent_hash, reexec) + .await?; + // Run system calls but stop before tx 0 + vm.rerun_block(&block, Some(0))?; + // Trace each transaction sequentially — state accumulates between calls + // We need to do this in order to pass ownership of block & evm to a blocking process without cloning + let vm = Arc::new(Mutex::new(vm)); + let block = Arc::new(block); + let mut traces = vec![]; + for index in 0..block.body.transactions.len() { + let block = block.clone(); + let vm = vm.clone(); + let tx_hash = block.as_ref().body.transactions[index].hash(); + let (pre_trace, pre_post) = timeout_trace_operation(timeout, move || { + vm.lock() + .map_err(|_| EvmError::Custom("Unexpected Runtime Error".to_string()))? + .trace_tx_prestate(block.as_ref(), index, diff_mode) + }) + .await?; + traces.push((tx_hash, pre_trace, pre_post)); + } + Ok(traces) + } + /// Rebuild the parent state for a block given its parent hash, returning an `Evm` instance with all changes cached /// Will re-execute all ancestor block's which's state is not stored up to a maximum given by `reexec` async fn rebuild_parent_state( diff --git a/crates/common/tracing.rs b/crates/common/tracing.rs index c476737577e..f2a52d88f47 100644 --- a/crates/common/tracing.rs +++ b/crates/common/tracing.rs @@ -1,7 +1,8 @@ use bytes::Bytes; use ethereum_types::H256; use ethereum_types::{Address, U256}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; /// Collection of traces of each call frame as defined in geth's `callTracer` output /// https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#call-tracer @@ -65,3 +66,286 @@ pub struct CallLog { pub data: Bytes, pub position: u64, } + +/// Account state as captured by the prestateTracer. +/// Matches Geth's prestateTracer output format. +/// https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#prestate-tracer +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct PrestateAccountState { + /// Balance as a hex string (e.g. "0x1a2b3c") + pub balance: String, + /// Account nonce + pub nonce: u64, + /// Bytecode as a hex string (e.g. "0x6060..."), omitted when empty + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + /// Storage slots as hex key -> hex value map, omitted when empty + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub storage: HashMap, +} + +/// Per-transaction prestate trace (non-diff mode). +/// Maps account address (hex string) to its state before the transaction. +pub type PrestateTrace = HashMap; + +/// Per-transaction prestate trace (diff mode). +/// Contains the pre-tx and post-tx state for all touched accounts. +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct PrePostState { + pub pre: HashMap, + pub post: HashMap, +} + +#[cfg(test)] +mod tests { + use super::*; + + // ── PrestateAccountState serialization ─────────────────────────────────── + + #[test] + fn account_state_serializes_balance_as_hex_string() { + let state = PrestateAccountState { + balance: "0x1a2b3c".to_string(), + nonce: 1, + code: None, + storage: HashMap::new(), + }; + + let json = serde_json::to_value(&state).expect("serialization must succeed"); + assert_eq!(json["balance"], "0x1a2b3c"); + assert_eq!(json["nonce"], 1); + } + + #[test] + fn account_state_omits_code_when_none() { + let state = PrestateAccountState { + balance: "0x0".to_string(), + nonce: 0, + code: None, + storage: HashMap::new(), + }; + + let json = serde_json::to_value(&state).expect("serialization must succeed"); + assert!( + json.get("code").is_none(), + "code field must be omitted when None" + ); + } + + #[test] + fn account_state_includes_code_when_present() { + let code_hex = "0x6080604052".to_string(); + let state = PrestateAccountState { + balance: "0x0".to_string(), + nonce: 1, + code: Some(code_hex.clone()), + storage: HashMap::new(), + }; + + let json = serde_json::to_value(&state).expect("serialization must succeed"); + assert_eq!(json["code"], code_hex); + } + + #[test] + fn account_state_omits_storage_when_empty() { + let state = PrestateAccountState { + balance: "0x0".to_string(), + nonce: 0, + code: None, + storage: HashMap::new(), + }; + + let json = serde_json::to_value(&state).expect("serialization must succeed"); + assert!( + json.get("storage").is_none(), + "storage field must be omitted when empty" + ); + } + + #[test] + fn account_state_includes_storage_slots_when_non_empty() { + let mut storage = HashMap::new(); + storage.insert( + "0x0000000000000000000000000000000000000000000000000000000000000001".to_string(), + "0x0000000000000000000000000000000000000000000000000000000000000064".to_string(), + ); + + let state = PrestateAccountState { + balance: "0x64".to_string(), + nonce: 2, + code: None, + storage: storage.clone(), + }; + + let json = serde_json::to_value(&state).expect("serialization must succeed"); + assert!(json.get("storage").is_some(), "storage must be present when non-empty"); + let slot = &json["storage"] + ["0x0000000000000000000000000000000000000000000000000000000000000001"]; + assert_eq!( + slot, + "0x0000000000000000000000000000000000000000000000000000000000000064" + ); + } + + // ── PrestateAccountState deserialization ───────────────────────────────── + + #[test] + fn account_state_deserializes_from_geth_format() { + let json = serde_json::json!({ + "balance": "0x3b9aca00", + "nonce": 5, + "code": "0x6080", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": + "0x0000000000000000000000000000000000000000000000000000000000000001" + } + }); + + let state: PrestateAccountState = + serde_json::from_value(json).expect("deserialization must succeed"); + + assert_eq!(state.balance, "0x3b9aca00"); + assert_eq!(state.nonce, 5); + assert_eq!(state.code, Some("0x6080".to_string())); + assert_eq!(state.storage.len(), 1); + } + + #[test] + fn account_state_deserializes_without_optional_code_field() { + // `code` is `Option` so it may be absent. `storage` must be present (no + // `#[serde(default)]` on the field), matching how Geth actually emits it. + let json = serde_json::json!({ + "balance": "0x0", + "nonce": 0, + "storage": {} + }); + + let state: PrestateAccountState = + serde_json::from_value(json).expect("deserialization must succeed"); + + assert!(state.code.is_none()); + assert!(state.storage.is_empty()); + } + + // ── PrePostState serialization ──────────────────────────────────────────── + + #[test] + fn pre_post_state_serializes_with_pre_and_post_keys() { + let mut pre = HashMap::new(); + pre.insert( + "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string(), + PrestateAccountState { + balance: "0x100".to_string(), + nonce: 1, + code: None, + storage: HashMap::new(), + }, + ); + + let mut post = HashMap::new(); + post.insert( + "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string(), + PrestateAccountState { + balance: "0x0".to_string(), + nonce: 1, + code: None, + storage: HashMap::new(), + }, + ); + + let pre_post = PrePostState { pre, post }; + let json = serde_json::to_value(&pre_post).expect("serialization must succeed"); + + assert!(json.get("pre").is_some(), "pre key must be present"); + assert!(json.get("post").is_some(), "post key must be present"); + + let addr = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + assert_eq!(json["pre"][addr]["balance"], "0x100"); + assert_eq!(json["post"][addr]["balance"], "0x0"); + } + + #[test] + fn pre_post_state_default_is_empty() { + let state = PrePostState::default(); + assert!(state.pre.is_empty()); + assert!(state.post.is_empty()); + } + + #[test] + fn pre_post_state_roundtrips_through_json() { + let mut storage = HashMap::new(); + storage.insert( + "0x0000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + "0x0000000000000000000000000000000000000000000000000000000000000001" + .to_string(), + ); + + let mut pre = HashMap::new(); + pre.insert( + "0x1234567890123456789012345678901234567890".to_string(), + PrestateAccountState { + balance: "0xde0b6b3a7640000".to_string(), + nonce: 3, + code: Some("0x60006000".to_string()), + storage, + }, + ); + + let original = PrePostState { + pre, + post: HashMap::new(), + }; + + // When serialized, accounts with non-empty storage include the storage + // field. The `post` map is empty so it serializes to `{}`, which + // deserializes correctly because HashMap deserialization accepts `{}`. + let json = serde_json::to_value(&original).expect("serialize must succeed"); + let roundtripped: PrePostState = + serde_json::from_value(json).expect("deserialize must succeed"); + + let addr = "0x1234567890123456789012345678901234567890"; + let account = roundtripped.pre.get(addr).expect("account must be present"); + assert_eq!(account.balance, "0xde0b6b3a7640000"); + assert_eq!(account.nonce, 3); + assert_eq!(account.code, Some("0x60006000".to_string())); + assert_eq!(account.storage.len(), 1); + } + + // ── PrestateTracerConfig deserialization (camelCase) ────────────────────── + + /// This mirrors the `PrestateTracerConfig` struct in `crates/networking/rpc/tracing.rs`. + /// We test that `diffMode` is correctly deserialized from camelCase JSON. + #[test] + fn prestate_tracer_config_diff_mode_deserializes_camel_case() { + // The RPC sends `{"diffMode": true}` — must deserialize correctly. + let json = serde_json::json!({"diffMode": true}); + // Simulate what PrestateTracerConfig does: we verify the camelCase key works + // by deserializing into a local struct that mirrors it. + #[derive(serde::Deserialize, Default)] + #[serde(rename_all = "camelCase")] + struct PrestateTracerConfig { + #[serde(default)] + diff_mode: bool, + } + + let cfg: PrestateTracerConfig = + serde_json::from_value(json).expect("camelCase deserialization must succeed"); + assert!(cfg.diff_mode); + } + + #[test] + fn prestate_tracer_config_defaults_diff_mode_to_false() { + #[derive(serde::Deserialize, Default)] + #[serde(rename_all = "camelCase")] + struct PrestateTracerConfig { + #[serde(default)] + diff_mode: bool, + } + + let cfg: PrestateTracerConfig = + serde_json::from_value(serde_json::json!({})) + .expect("empty object must deserialize to defaults"); + assert!(!cfg.diff_mode); + } +} diff --git a/crates/l2/Cargo.toml b/crates/l2/Cargo.toml index dc1f8d8a4b3..40f022befcf 100644 --- a/crates/l2/Cargo.toml +++ b/crates/l2/Cargo.toml @@ -54,17 +54,26 @@ crossterm = { version = "0.29.0", features = ["event-stream"] } ratatui = "0.29.0" tui-logger.workspace = true axum.workspace = true +tonic = "0.12" +prost = "0.13" +tokio-stream = "0.1" ethrex-guest-program = { path = "../guest-program", optional = true } [build-dependencies] vergen-git2 = { version = "1.0.7" } +tonic-build = "0.12" [dev-dependencies] anyhow = "1.0.86" +hex = { workspace = true } [lib] path = "./l2.rs" +[[bin]] +name = "mock-sidecar" +path = "sequencer/circuit_breaker/mock_sidecar/main.rs" + [lints.clippy] unwrap_used = "deny" expect_used = "deny" diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 9fcb04c573a..596ba8d93a4 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -281,3 +281,30 @@ state-diff-test: # - docs/developers/l2/state-reconstruction-blobs.md (step-by-step guide) validate-blobs: cargo test -p ethrex-test validate_blobs_match_genesis --release + +# Circuit Breaker (Credible Layer) +# See docs/l2/circuit_breaker.md for full documentation. + +init-circuit-breaker: ## 🛡️ Start the Circuit Breaker sidecar stack (assertion-da + sidecar) + @echo "Starting Circuit Breaker sidecar stack..." + @echo "Make sure to set STATE_ORACLE_ADDRESS in sidecar-config.template.json" + docker run -d --name assertion-da -p 5001:5001 \ + -e DB_PATH=/data/assertions \ + -e DA_LISTEN_ADDR=0.0.0.0:5001 \ + -e DA_CACHE_SIZE=1000000 \ + ghcr.io/phylaxsystems/credible-sdk/assertion-da-dev:sha-01b3374 || true + @echo "Assertion DA running on :5001" + @echo "Start the sidecar manually: ./sidecar --config-file-path ./sidecar-config.template.json" + +down-circuit-breaker: ## 🛑 Stop the Circuit Breaker sidecar stack + docker stop assertion-da 2>/dev/null || true + docker rm assertion-da 2>/dev/null || true + +init-l2-circuit-breaker: ## 🛡️ Start L2 with Circuit Breaker enabled + cargo run --release --features l2 --manifest-path ../../Cargo.toml -- \ + l2 \ + --circuit-breaker-url http://localhost:50051 \ + --circuit-breaker-aeges-url http://localhost:8080 \ + --l2.ws-enabled \ + --l2.ws-port 1730 \ + 2>&1 | tee /tmp/l2_cb.log diff --git a/crates/l2/build.rs b/crates/l2/build.rs index 3ea743e82cf..2367bc515b9 100644 --- a/crates/l2/build.rs +++ b/crates/l2/build.rs @@ -9,10 +9,17 @@ fn main() -> Result<(), Box> { // When building tdx image with nix the commit version is stored as an env var if let Ok(sha) = std::env::var("VERGEN_GIT_SHA") { println!("cargo:rustc-env=VERGEN_GIT_SHA={}", sha.trim()); - return Ok(()); + } else { + let git2 = Git2Builder::default().sha(true).build()?; + Emitter::default().add_instructions(&git2)?.emit()?; } - let git2 = Git2Builder::default().sha(true).build()?; - Emitter::default().add_instructions(&git2)?.emit()?; + // Compile Circuit Breaker protobuf definitions (client + server for mock sidecar) + tonic_build::configure() + .compile_protos( + &["proto/sidecar.proto", "proto/aeges.proto"], + &["proto/"], + )?; + Ok(()) } diff --git a/crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol b/crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol new file mode 100644 index 00000000000..c23f13f525c --- /dev/null +++ b/crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/// @title OwnableTarget +/// @notice A simple Ownable contract used as a target for Circuit Breaker testing. +/// The TestOwnershipAssertion protects this contract by preventing ownership transfers. +contract OwnableTarget { + address public owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + constructor() { + owner = msg.sender; + } + + modifier onlyOwner() { + require(msg.sender == owner, "OwnableTarget: caller is not the owner"); + _; + } + + /// @notice Transfer ownership to a new address. + /// When protected by the TestOwnershipAssertion, this call will be dropped by the sidecar. + function transferOwnership(address newOwner) external onlyOwner { + require(newOwner != address(0), "OwnableTarget: new owner is the zero address"); + emit OwnershipTransferred(owner, newOwner); + owner = newOwner; + } + + /// @notice A harmless function that does not trigger any assertion. + function doSomething() external pure returns (uint256) { + return 42; + } +} diff --git a/crates/l2/contracts/src/circuit_breaker/README.md b/crates/l2/contracts/src/circuit_breaker/README.md new file mode 100644 index 00000000000..244bd657fd3 --- /dev/null +++ b/crates/l2/contracts/src/circuit_breaker/README.md @@ -0,0 +1,69 @@ +# Circuit Breaker Contracts + +This directory contains example/demo contracts for the Circuit Breaker integration: + +- `OwnableTarget.sol` — a simple Ownable contract used as a demonstration target +- `TestOwnershipAssertion.sol` — a test assertion that asserts ownership of `OwnableTarget` cannot change + +## State Oracle + +The **State Oracle** is the on-chain registry that maps protected contracts to their active +assertions. It is maintained by Phylax Systems and must be deployed separately using the +Phylax toolchain before starting the Credible Layer sidecar. + +### Contract Source + +The State Oracle and its dependencies live in the `credible-layer-contracts` repository: + +> https://github.com/phylaxsystems/credible-layer-contracts + +The relevant contracts are: + +| Contract | Purpose | +|----------|---------| +| `StateOracle` | Core registry: maps protected contracts to assertions | +| `DAVerifierECDSA` | Verifies ECDSA-signed assertion DA payloads | +| `DAVerifierOnChain` | Verifies on-chain DA payloads | +| `AdminVerifierOwner` | Restricts assertion registration to contract owner | + +The `StateOracle` constructor signature is: + +```solidity +constructor(uint256 assertionTimelockBlocks) Ownable(msg.sender) +``` + +And initialization (called after proxy deployment): + +```solidity +function initialize( + address admin, + IAdminVerifier[] calldata _adminVerifiers, + IDAVerifier[] calldata _daVerifiers, + uint16 _maxAssertionsPerAA +) external +``` + +### Deploying the State Oracle + +Use the Phylax `pcl` CLI or the Foundry deployment scripts provided in +`credible-layer-contracts`: + +```bash +# Install pcl +brew tap phylaxsystems/pcl +brew install pcl + +# Or use Foundry scripts from the credible-layer-contracts repo +git clone https://github.com/phylaxsystems/credible-layer-contracts +cd credible-layer-contracts +forge script script/DeployStateOracle.s.sol --rpc-url --broadcast +``` + +Once deployed, note the State Oracle address and pass it to ethrex via the +`--circuit-breaker-state-oracle` flag (see the [Circuit Breaker docs](../../../../docs/l2/circuit_breaker.md)). + +### References + +- [Credible Layer Introduction](https://docs.phylax.systems/credible/credible-introduction) +- [credible-layer-contracts](https://github.com/phylaxsystems/credible-layer-contracts) +- [credible-sdk (sidecar source)](https://github.com/phylaxsystems/credible-sdk) diff --git a/crates/l2/contracts/src/circuit_breaker/TestOwnershipAssertion.sol b/crates/l2/contracts/src/circuit_breaker/TestOwnershipAssertion.sol new file mode 100644 index 00000000000..4697bf38917 --- /dev/null +++ b/crates/l2/contracts/src/circuit_breaker/TestOwnershipAssertion.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/// @title TestOwnershipAssertion +/// @notice A trivial assertion for Circuit Breaker end-to-end testing. +/// Protects OwnableTarget by asserting that ownership cannot change. +/// +/// This contract uses the credible-std library interfaces. +/// To compile and deploy, use the pcl CLI: +/// pcl apply --assertion TestOwnershipAssertion --adopter +/// +/// For local development without credible-std, this file serves as a reference +/// for the assertion logic. The actual deployment uses pcl which handles +/// compilation with the credible-std dependency. + +interface IPhEvm { + function forkPreTx() external; + function forkPostTx() external; + function getAssertionAdopter() external view returns (address); +} + +interface IOwnableTarget { + function owner() external view returns (address); + function transferOwnership(address newOwner) external; +} + +/// @dev In production, this would inherit from credible-std's Assertion base class. +/// The actual assertion contract deployed via pcl would look like: +/// +/// import {Assertion} from "credible-std/Assertion.sol"; +/// +/// contract TestOwnershipAssertion is Assertion { +/// function triggers() external view override { +/// registerCallTrigger( +/// this.assertOwnerUnchanged.selector, +/// IOwnableTarget.transferOwnership.selector +/// ); +/// } +/// +/// function assertOwnerUnchanged() external { +/// IOwnableTarget target = IOwnableTarget(ph.getAssertionAdopter()); +/// ph.forkPreTx(); +/// address ownerBefore = target.owner(); +/// ph.forkPostTx(); +/// address ownerAfter = target.owner(); +/// require(ownerBefore == ownerAfter, "ownership changed"); +/// } +/// } +contract TestOwnershipAssertion { + // This is a reference implementation. See the comment above for the + // actual credible-std version used with pcl. +} diff --git a/crates/l2/l2.rs b/crates/l2/l2.rs index 2cd23de17c3..1ec76869746 100644 --- a/crates/l2/l2.rs +++ b/crates/l2/l2.rs @@ -6,7 +6,7 @@ pub mod utils; pub use based::block_fetcher::BlockFetcher; pub use sequencer::configs::{ - BasedConfig, BlockFetcherConfig, BlockProducerConfig, CommitterConfig, EthConfig, - L1WatcherConfig, ProofCoordinatorConfig, SequencerConfig, StateUpdaterConfig, + BasedConfig, BlockFetcherConfig, BlockProducerConfig, CircuitBreakerConfig, CommitterConfig, + EthConfig, L1WatcherConfig, ProofCoordinatorConfig, SequencerConfig, StateUpdaterConfig, }; pub use sequencer::start_l2; diff --git a/crates/l2/networking/rpc/rpc.rs b/crates/l2/networking/rpc/rpc.rs index 1e3d561d573..ade070661ba 100644 --- a/crates/l2/networking/rpc/rpc.rs +++ b/crates/l2/networking/rpc/rpc.rs @@ -39,7 +39,7 @@ use tokio::{ sync::{Mutex as TokioMutex, broadcast}, }; use tower_http::cors::CorsLayer; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use tracing_subscriber::{EnvFilter, Registry, reload}; use crate::l2::transaction::SponsoredTx; @@ -47,13 +47,38 @@ use ethrex_common::Address; use ethrex_storage_rollup::StoreRollup; use secp256k1::SecretKey; -#[derive(Debug, Clone)] +/// Broadcast channel capacity for new block header notifications. +/// A value of 128 handles bursts without blocking block production. +pub const NEW_HEADS_CHANNEL_CAPACITY: usize = 128; + +/// Async callback for mempool pre-filtering (Aeges integration). +/// +/// Receives the raw transaction bytes and returns `true` if the transaction +/// should be admitted to the mempool, `false` if it should be rejected. +/// On any error or timeout the implementation should return `true` (permissive). +pub type MempoolFilter = + Arc std::pin::Pin + Send>>) + Send + Sync>; + +#[derive(Clone)] pub struct RpcApiContext { pub l1_ctx: ethrex_rpc::RpcApiContext, pub valid_delegation_addresses: Vec
, pub sponsor_pk: SecretKey, pub rollup_store: StoreRollup, pub sponsored_gas_limit: u64, + /// Broadcast sender for new block header notifications (eth_subscribe "newHeads"). + /// `None` when the WS server is disabled. + pub new_heads_sender: Option>, + /// Mempool pre-filter callback (Aeges integration). `None` when not configured. + pub mempool_filter: Option, +} + +impl std::fmt::Debug for RpcApiContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RpcApiContext") + .field("sponsored_gas_limit", &self.sponsored_gas_limit) + .finish_non_exhaustive() + } } pub trait RpcHandler: Sized { @@ -128,12 +153,14 @@ pub async fn start_api( log_filter_handler, gas_ceil: l2_gas_limit, block_worker_channel, - new_heads_sender, + new_heads_sender: new_heads_sender.clone(), }, valid_delegation_addresses, sponsor_pk, rollup_store, sponsored_gas_limit, + new_heads_sender, + mempool_filter: None, }; // Periodically clean up the active filters for the filters endpoints. @@ -224,73 +251,14 @@ async fn handle_http_request( Ok(Json(res)) } -/// Handle requests that can come from either clients or other users -pub async fn map_http_requests(req: &RpcRequest, context: RpcApiContext) -> Result { - match resolve_namespace(&req.method) { - Ok(RpcNamespace::L1RpcNamespace(ethrex_rpc::RpcNamespace::Eth)) => { - map_eth_requests(req, context).await - } - Ok(RpcNamespace::EthrexL2) => map_l2_requests(req, context).await, - _ => ethrex_rpc::map_http_requests(req, context.l1_ctx) - .await - .map_err(RpcErr::L1RpcErr), - } -} - -pub async fn map_eth_requests(req: &RpcRequest, context: RpcApiContext) -> Result { - match req.method.as_str() { - "eth_sendRawTransaction" => { - let tx = SendRawTransactionRequest::parse(&req.params)?; - if let SendRawTransactionRequest::EIP4844(wrapped_blob_tx) = tx { - debug!( - "EIP-4844 transaction are not supported in the L2: {:#x}", - Transaction::EIP4844Transaction(wrapped_blob_tx.tx).hash() - ); - return Err(RpcErr::InvalidEthrexL2Message( - "EIP-4844 transactions are not supported in the L2".to_string(), - )); - } - SendRawTransactionRequest::call(req, context.l1_ctx) - .await - .map_err(RpcErr::L1RpcErr) - } - "debug_executionWitness" => { - let request = ExecutionWitnessRequest::parse(&req.params)?; - handle_execution_witness(&request, context) - .await - .map_err(RpcErr::L1RpcErr) - } - _other_eth_method => ethrex_rpc::map_eth_requests(req, context.l1_ctx) - .await - .map_err(RpcErr::L1RpcErr), - } -} - -pub async fn map_l2_requests(req: &RpcRequest, context: RpcApiContext) -> Result { - match req.method.as_str() { - "ethrex_sendTransaction" => SponsoredTx::call(req, context).await, - "ethrex_getL1MessageProof" => GetL1MessageProof::call(req, context).await, - "ethrex_batchNumber" => BatchNumberRequest::call(req, context).await, - "ethrex_getBatchByBlock" => GetBatchByBatchBlockNumberRequest::call(req, context).await, - "ethrex_getBatchByNumber" => GetBatchByBatchNumberRequest::call(req, context).await, - "ethrex_getBaseFeeVaultAddress" => GetBaseFeeVaultAddress::call(req, context).await, - "ethrex_getOperatorFeeVaultAddress" => GetOperatorFeeVaultAddress::call(req, context).await, - "ethrex_getOperatorFee" => GetOperatorFee::call(req, context).await, - "ethrex_getL1FeeVaultAddress" => GetL1FeeVaultAddress::call(req, context).await, - "ethrex_getL1BlobBaseFee" => GetL1BlobBaseFeeRequest::call(req, context).await, - unknown_ethrex_l2_method => { - Err(ethrex_rpc::RpcErr::MethodNotFound(unknown_ethrex_l2_method.to_owned()).into()) - } - } -} - /// Handle a WebSocket connection. /// /// Supports eth_subscribe / eth_unsubscribe for "newHeads" in addition to /// regular JSON-RPC request-response calls that work the same as over HTTP. -/// Subscription functionality is provided by ethrex_rpc (L1 crate). async fn handle_websocket(mut socket: WebSocket, context: RpcApiContext) { // subscription_id -> broadcast::Receiver + // We store only one receiver per subscription ID; senders are cloned from + // context.new_heads_sender when a subscription is created. let mut subscriptions: HashMap> = HashMap::new(); // Channel for the write loop to receive outbound messages. let (out_tx, mut out_rx) = tokio::sync::mpsc::unbounded_channel::(); @@ -312,15 +280,15 @@ async fn handle_websocket(mut socket: WebSocket, context: RpcApiContext) { }; let response = handle_ws_request(&body, &context, &mut subscriptions, &out_tx).await; - if let Some(resp) = response - && socket.send(Message::Text(resp.into())).await.is_err() - { - break; + if let Some(resp) = response { + if socket.send(Message::Text(resp.into())).await.is_err() { + break; + } } } // Push subscription notifications for all active subscriptions. - _ = ethrex_rpc::drain_subscriptions(&mut subscriptions, &out_tx) => {} + _ = drain_subscriptions(&mut subscriptions, &out_tx) => {} // Send any pending outbound messages (subscription notifications). Some(msg) = out_rx.recv() => { @@ -332,7 +300,7 @@ async fn handle_websocket(mut socket: WebSocket, context: RpcApiContext) { } // Connection closed — subscriptions are dropped automatically when the - // HashMap goes out of scope. + // HashMap goes out of scope (task 4.6). } /// Process an incoming JSON-RPC request over WebSocket. @@ -360,16 +328,12 @@ async fn handle_ws_request( match req.method.as_str() { "eth_subscribe" => { - // Delegate to L1's implementation, which reads from context.l1_ctx.new_heads_sender. - let result = ethrex_rpc::handle_eth_subscribe(&req, &context.l1_ctx, subscriptions) - .map_err(RpcErr::L1RpcErr); + let result = handle_eth_subscribe(&req, context, subscriptions); let resp = ethrex_rpc::rpc_response(req.id, result).ok()?; Some(resp.to_string()) } "eth_unsubscribe" => { - // Delegate to L1's implementation. - let result = - ethrex_rpc::handle_eth_unsubscribe(&req, subscriptions).map_err(RpcErr::L1RpcErr); + let result = handle_eth_unsubscribe(&req, subscriptions); let resp = ethrex_rpc::rpc_response(req.id, result).ok()?; Some(resp.to_string()) } @@ -381,15 +345,218 @@ async fn handle_ws_request( } } +/// Handle `eth_subscribe`. +/// +/// Only `"newHeads"` is supported (task 4.7). Returns a hex subscription ID +/// on success or an error for unsupported subscription types. +fn handle_eth_subscribe( + req: &RpcRequest, + context: &RpcApiContext, + subscriptions: &mut HashMap>, +) -> Result { + // params[0] must be the subscription type string. + let params = req.params.as_deref().unwrap_or(&[]); + let sub_type = params + .first() + .and_then(|v| v.as_str()) + .ok_or_else(|| RpcErr::L1RpcErr(ethrex_rpc::RpcErr::BadParams( + "eth_subscribe requires a subscription type parameter".to_string(), + )))?; + + match sub_type { + "newHeads" => { + let sender = context.new_heads_sender.as_ref().ok_or_else(|| { + RpcErr::L1RpcErr(ethrex_rpc::RpcErr::Internal( + "WebSocket server not enabled".to_string(), + )) + })?; + + // Generate a unique subscription ID. + let sub_id = generate_subscription_id(); + + // Subscribe to the broadcast channel. + let receiver = sender.subscribe(); + subscriptions.insert(sub_id.clone(), receiver); + + Ok(Value::String(sub_id)) + } + other => Err(RpcErr::L1RpcErr(ethrex_rpc::RpcErr::Internal(format!( + "Unsupported subscription type: {other}" + )))), + } +} + +/// Handle `eth_unsubscribe`. +/// +/// Returns `true` if the subscription existed and was removed, `false` otherwise. +fn handle_eth_unsubscribe( + req: &RpcRequest, + subscriptions: &mut HashMap>, +) -> Result { + let params = req.params.as_deref().unwrap_or(&[]); + let sub_id = params + .first() + .and_then(|v| v.as_str()) + .ok_or_else(|| RpcErr::L1RpcErr(ethrex_rpc::RpcErr::BadParams( + "eth_unsubscribe requires a subscription ID parameter".to_string(), + )))?; + + let removed = subscriptions.remove(sub_id).is_some(); + Ok(Value::Bool(removed)) +} + +/// Generate a unique hex-encoded subscription ID. +fn generate_subscription_id() -> String { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(1); + let id = COUNTER.fetch_add(1, Ordering::Relaxed); + format!("0x{id:016x}") +} + +/// Drain any buffered messages from active subscriptions and send them to +/// the outbound channel. This is called from the `select!` loop to ensure +/// subscription notifications are forwarded promptly. +/// +/// Returns immediately after draining whatever is currently buffered; +/// the future resolves to `()` so the caller can combine it with other arms. +async fn drain_subscriptions( + subscriptions: &mut HashMap>, + out_tx: &tokio::sync::mpsc::UnboundedSender, +) { + // Collect subscription IDs to avoid borrow conflicts while iterating. + let sub_ids: Vec = subscriptions.keys().cloned().collect(); + for sub_id in sub_ids { + let Some(receiver) = subscriptions.get_mut(&sub_id) else { + continue; + }; + loop { + match receiver.try_recv() { + Ok(header) => { + let notification = build_subscription_notification(&sub_id, header); + if out_tx.send(notification).is_err() { + // Channel closed — connection is shutting down. + return; + } + } + Err(broadcast::error::TryRecvError::Empty) => break, + Err(broadcast::error::TryRecvError::Closed) => { + // Sender was dropped. + subscriptions.remove(&sub_id); + break; + } + Err(broadcast::error::TryRecvError::Lagged(n)) => { + warn!("eth_subscribe newHeads: subscription {sub_id} lagged by {n} messages"); + // Continue to catch up. + } + } + } + } + // Yield so that the select! loop can check other arms. + tokio::task::yield_now().await; +} + +/// Build the standard Ethereum subscription notification envelope: +/// `{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x...","result":{...}}}` +fn build_subscription_notification(sub_id: &str, result: Value) -> String { + serde_json::json!({ + "jsonrpc": "2.0", + "method": "eth_subscription", + "params": { + "subscription": sub_id, + "result": result, + } + }) + .to_string() +} + +/// Handle requests that can come from either clients or other users +pub async fn map_http_requests(req: &RpcRequest, context: RpcApiContext) -> Result { + match resolve_namespace(&req.method) { + Ok(RpcNamespace::L1RpcNamespace(ethrex_rpc::RpcNamespace::Eth)) => { + map_eth_requests(req, context).await + } + Ok(RpcNamespace::EthrexL2) => map_l2_requests(req, context).await, + _ => ethrex_rpc::map_http_requests(req, context.l1_ctx) + .await + .map_err(RpcErr::L1RpcErr), + } +} + +pub async fn map_eth_requests(req: &RpcRequest, context: RpcApiContext) -> Result { + match req.method.as_str() { + "eth_sendRawTransaction" => { + let tx = SendRawTransactionRequest::parse(&req.params)?; + if let SendRawTransactionRequest::EIP4844(wrapped_blob_tx) = tx { + debug!( + "EIP-4844 transaction are not supported in the L2: {:#x}", + Transaction::EIP4844Transaction(wrapped_blob_tx.tx).hash() + ); + return Err(RpcErr::InvalidEthrexL2Message( + "EIP-4844 transactions are not supported in the L2".to_string(), + )); + } + // Task 3.2/3.3: Check with Aeges before admitting to mempool. + // Privileged transactions are added directly by the L1Watcher, never via + // eth_sendRawTransaction, so all transactions here are regular user txs. + if let Some(filter) = &context.mempool_filter { + // Extract the raw transaction bytes from the first param. + let raw_bytes = req + .params + .as_deref() + .and_then(|p| p.first()) + .and_then(|v| v.as_str()) + .map(|hex| { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + hex::decode(hex).unwrap_or_default() + }) + .unwrap_or_default(); + if !filter(raw_bytes.into()).await { + debug!("Aeges pre-filter rejected transaction"); + return Err(RpcErr::InvalidEthrexL2Message( + "Transaction rejected by Aeges pre-filter".to_string(), + )); + } + } + SendRawTransactionRequest::call(req, context.l1_ctx) + .await + .map_err(RpcErr::L1RpcErr) + } + "debug_executionWitness" => { + let request = ExecutionWitnessRequest::parse(&req.params)?; + handle_execution_witness(&request, context) + .await + .map_err(RpcErr::L1RpcErr) + } + _other_eth_method => ethrex_rpc::map_eth_requests(req, context.l1_ctx) + .await + .map_err(RpcErr::L1RpcErr), + } +} + +pub async fn map_l2_requests(req: &RpcRequest, context: RpcApiContext) -> Result { + match req.method.as_str() { + "ethrex_sendTransaction" => SponsoredTx::call(req, context).await, + "ethrex_getL1MessageProof" => GetL1MessageProof::call(req, context).await, + "ethrex_batchNumber" => BatchNumberRequest::call(req, context).await, + "ethrex_getBatchByBlock" => GetBatchByBatchBlockNumberRequest::call(req, context).await, + "ethrex_getBatchByNumber" => GetBatchByBatchNumberRequest::call(req, context).await, + "ethrex_getBaseFeeVaultAddress" => GetBaseFeeVaultAddress::call(req, context).await, + "ethrex_getOperatorFeeVaultAddress" => GetOperatorFeeVaultAddress::call(req, context).await, + "ethrex_getOperatorFee" => GetOperatorFee::call(req, context).await, + "ethrex_getL1FeeVaultAddress" => GetL1FeeVaultAddress::call(req, context).await, + "ethrex_getL1BlobBaseFee" => GetL1BlobBaseFeeRequest::call(req, context).await, + unknown_ethrex_l2_method => { + Err(ethrex_rpc::RpcErr::MethodNotFound(unknown_ethrex_l2_method.to_owned()).into()) + } + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; - use ethrex_rpc::{ - NEW_HEADS_CHANNEL_CAPACITY, broadcast, build_subscription_notification, - generate_subscription_id, handle_eth_unsubscribe, - }; use serde_json::{Value, json}; + use tokio::sync::broadcast; use super::*; diff --git a/crates/l2/proto/aeges.proto b/crates/l2/proto/aeges.proto new file mode 100644 index 00000000000..906f19d6d10 --- /dev/null +++ b/crates/l2/proto/aeges.proto @@ -0,0 +1,67 @@ +syntax = "proto3"; + +package aeges.v1; + +// Aeges service for pre-execution transaction filtering. +service AegesService { + // Unary: plugin sends a single transaction, server responds with a + // denied/allowed verdict. + rpc VerifyTransaction(VerifyTransactionRequest) returns (VerifyTransactionResponse); +} + +// Mirrors the Besu Transaction interface fields. +message Transaction { + // Core fields. + bytes hash = 1; // 32 bytes + bytes sender = 2; // 20 bytes + optional bytes to = 3; // 20 bytes, absent for contract creation + bytes value = 4; // big-endian uint256 + uint64 nonce = 5; + uint32 type = 6; // TransactionType ordinal + optional uint64 chain_id = 7; + + // Payload. + bytes payload = 8; // calldata (calls) or init code (creates) + + // Gas. + uint64 gas_limit = 9; + optional uint64 gas_price = 10; // legacy/EIP-2930 + optional uint64 max_fee_per_gas = 11; // EIP-1559+ + optional uint64 max_priority_fee_per_gas = 12; // EIP-1559+ + optional uint64 max_fee_per_blob_gas = 13; // EIP-4844 + + // EIP-2930 access list. + repeated AccessListEntry access_list = 14; + + // EIP-4844 blob versioned hashes. + repeated bytes versioned_hashes = 15; // each 32 bytes + + // EIP-7702 code delegations. + repeated CodeDelegation code_delegation_list = 16; +} + +message AccessListEntry { + bytes address = 1; // 20 bytes + repeated bytes storage_keys = 2; // each 32 bytes +} + +message CodeDelegation { + uint64 chain_id = 1; + bytes address = 2; // 20 bytes + uint64 nonce = 3; + uint32 v = 4; // y-parity (0 or 1) + bytes r = 5; // 32 bytes + bytes s = 6; // 32 bytes +} + +message VerifyTransactionRequest { + // Client-provided correlation ID, echoed back in the response. + uint64 event_id = 1; + Transaction transaction = 2; +} + +message VerifyTransactionResponse { + // Echoed from the request. + uint64 event_id = 1; + bool denied = 2; +} diff --git a/crates/l2/proto/sidecar.proto b/crates/l2/proto/sidecar.proto new file mode 100644 index 00000000000..c05857184f8 --- /dev/null +++ b/crates/l2/proto/sidecar.proto @@ -0,0 +1,198 @@ +syntax = "proto3"; + +package sidecar.transport.v1; + +// Block environment for EVM execution context. +// All numeric types that exceed 64 bits are encoded as big-endian bytes. +message BlockEnv { + bytes number = 1; // 32 bytes - block number as U256 big-endian + bytes beneficiary = 2; // 20 bytes - coinbase address + bytes timestamp = 3; // 32 bytes - timestamp as U256 big-endian + uint64 gas_limit = 4; // block gas limit + uint64 basefee = 5; // base fee per gas (u64) + bytes difficulty = 6; // 32 bytes - difficulty as U256 big-endian + optional bytes prevrandao = 7; // 32 bytes - prevrandao hash + optional BlobExcessGasAndPrice blob_excess_gas_and_price = 8; +} + +// EIP-4844 blob gas pricing information. +message BlobExcessGasAndPrice { + uint64 excess_blob_gas = 1; // excess blob gas + bytes blob_gasprice = 2; // 16 bytes - blob gas price as u128 big-endian +} + +// Generic acknowledgment response. +message BasicAck { + bool accepted = 1; + string message = 2; +} + +// Access list item for EIP-2930 transactions. +message AccessListItem { + bytes address = 1; // 20 bytes - account address + repeated bytes storage_keys = 2; // 32 bytes each - storage slot keys +} + +// Authorization for EIP-7702 (account abstraction). +message Authorization { + bytes chain_id = 1; // 32 bytes - chain ID as U256 big-endian + bytes address = 2; // 20 bytes - authorized address + uint64 nonce = 3; // authorization nonce + uint32 y_parity = 4; // signature y-parity (0 or 1) + bytes r = 5; // 32 bytes - signature r as U256 big-endian + bytes s = 6; // 32 bytes - signature s as U256 big-endian +} + +// Transaction environment equivalent to revm's TxEnv. +// Uses raw bytes for all hash/address/numeric fields to avoid hex encoding overhead. +message TransactionEnv { + uint32 tx_type = 1; // transaction type (0=legacy, 1=eip2930, 2=eip1559, etc.) + bytes caller = 2; // 20 bytes - sender address + uint64 gas_limit = 3; // gas limit + bytes gas_price = 4; // 16 bytes - gas price as u128 big-endian + bytes transact_to = 5; // 20 bytes or empty for create + bytes value = 6; // 32 bytes - transaction value as U256 big-endian + bytes data = 7; // calldata bytes + uint64 nonce = 8; // transaction nonce + optional uint64 chain_id = 9; // chain ID (optional for legacy) + repeated AccessListItem access_list = 10; // EIP-2930 access list + optional bytes gas_priority_fee = 11; // 16 bytes - priority fee as u128 big-endian (optional) + repeated bytes blob_hashes = 12; // 32 bytes each - EIP-4844 blob versioned hashes + bytes max_fee_per_blob_gas = 13; // 16 bytes - max fee per blob gas as u128 big-endian + repeated Authorization authorization_list = 14; // EIP-7702 authorization list +} + +// Unique identifier for a transaction execution within a block iteration. +message TxExecutionId { + bytes block_number = 1; // 32 bytes - block number as U256 big-endian + uint64 iteration_id = 2; // iteration ID within the block + bytes tx_hash = 3; // 32 bytes - transaction hash + uint64 index = 4; // transaction index within iteration +} + +// Transaction with execution context. +message Transaction { + TxExecutionId tx_execution_id = 1; // TX execution ID + TransactionEnv tx_env = 2; // transaction environment + optional bytes prev_tx_hash = 3; // 32 bytes - previous TX hash for ordering +} + +// Commit head event - signals the start of a new block building round. +message CommitHead { + optional bytes last_tx_hash = 1; // 32 bytes - last TX hash (None means absent) + uint64 n_transactions = 2; // number of transactions in previous iteration + bytes block_number = 3; // 32 bytes - block number as U256 big-endian + uint64 selected_iteration_id = 4; // selected iteration ID + optional bytes block_hash = 5; // 32 bytes - block hash for EIP-2935 + optional bytes parent_beacon_block_root = 6; // 32 bytes - parent beacon block root for EIP-4788 + bytes timestamp = 7; // 32 bytes - timestamp as U256 big-endian +} + +// New iteration event - initializes building for an iteration ID. +// Contains all data needed to apply system calls as EIP-4788, EIP-2935 before transaction execution. +message NewIteration { + BlockEnv block_env = 1; // block environment + uint64 iteration_id = 2; // iteration ID + optional bytes parent_block_hash = 3; // 32 bytes - parent block hash for EIP-2935 + optional bytes parent_beacon_block_root = 4; // 32 bytes - parent beacon block root for EIP-4788 +} + +// Reorg event - signals a chain reorganization. +message ReorgEvent { + TxExecutionId tx_execution_id = 1; // TX execution ID to reorg from + repeated bytes tx_hashes = 2; // Transaction hashes to reorg (oldest -> newest) +} + +// Unified event wrapper for streaming. +// Each event includes a event_id for matching with the corresponding StreamAck. +message Event { + uint64 event_id = 1; // client-provided ID for request-response matching + oneof event { + CommitHead commit_head = 2; + NewIteration new_iteration = 3; + Transaction transaction = 4; + ReorgEvent reorg = 5; + } +} + +// Stream acknowledgment - sent for each event processed. +// The event_id matches the event_id from the corresponding Event. +message StreamAck { + bool success = 1; // whether processing succeeded + string message = 2; // info/error message + uint64 events_processed = 3; // total events processed so far + uint64 event_id = 4; // matches the event_id from the Event +} + +// Transaction execution result status. +enum ResultStatus { + RESULT_STATUS_UNSPECIFIED = 0; + RESULT_STATUS_SUCCESS = 1; // transaction executed successfully + RESULT_STATUS_REVERTED = 2; // transaction reverted + RESULT_STATUS_HALTED = 3; // transaction halted (out of gas, etc.) + RESULT_STATUS_FAILED = 4; // validation failed + RESULT_STATUS_ASSERTION_FAILED = 5; // assertion validation failed +} + +// Transaction execution result. +message TransactionResult { + TxExecutionId tx_execution_id = 1; // TX execution ID + ResultStatus status = 2; // execution status + uint64 gas_used = 3; // gas consumed (0 when unknown) + string error = 4; // error message (empty when none) +} + +// Request to subscribe to transaction results stream. +message SubscribeResultsRequest { + optional bytes from_block = 1; // 32 bytes - filter by starting block number as U256 big-endian +} + +// Request for multiple transaction results (kept for compatibility with unary queries). +message GetTransactionsRequest { + repeated TxExecutionId tx_execution_ids = 1; // TX execution IDs to query +} + +// Response containing multiple transaction results. +message GetTransactionsResponse { + repeated TransactionResult results = 1; // found results + repeated bytes not_found = 2; // 32-byte hashes not found +} + +// Request for a single transaction result. +message GetTransactionRequest { + TxExecutionId tx_execution_id = 1; // TX execution ID +} + +// Response for a single transaction result. +message GetTransactionResponse { + oneof outcome { + TransactionResult result = 1; // transaction result if found + bytes not_found = 2; // 32-byte hash if not found + } +} + +// Sidecar transport service. +// +// Streaming RPCs: +// - StreamEvents: Bidirectional stream for sending events +// - SubscribeResults: Server stream for receiving transaction results as they complete +// +// Unary RPCs (kept for simple queries): +// - GetTransactions: Query multiple transaction results +// - GetTransaction: Query a single transaction result +service SidecarTransport { + // Bidirectional stream: client sends events, server sends periodic acks. + // Events must start with CommitHead before any other event type. + // Each ack includes the event_id from the corresponding event for explicit matching. + rpc StreamEvents(stream Event) returns (stream StreamAck); + + // Server stream: subscribe to transaction results as they complete. + // Results are pushed immediately when transactions finish executing. + rpc SubscribeResults(SubscribeResultsRequest) returns (stream TransactionResult); + + // Query multiple transaction results by their execution IDs. + rpc GetTransactions(GetTransactionsRequest) returns (GetTransactionsResponse); + + // Query a single transaction result by its execution ID. + rpc GetTransaction(GetTransactionRequest) returns (GetTransactionResponse); +} diff --git a/crates/l2/scripts/circuit_breaker_e2e.sh b/crates/l2/scripts/circuit_breaker_e2e.sh new file mode 100755 index 00000000000..91806a2c611 --- /dev/null +++ b/crates/l2/scripts/circuit_breaker_e2e.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# Circuit Breaker End-to-End Validation Script +# +# This script validates the full Circuit Breaker integration: +# 1. Checks that ethrex L2 is running with Circuit Breaker enabled +# 2. Checks that the sidecar is running and healthy +# 3. Deploys a test target contract (OwnableTarget) +# 4. (Manual step) Deploys and registers a test assertion via pcl +# 5. Sends a violating transaction → verifies it's NOT included +# 6. Sends a valid transaction → verifies it IS included +# +# Prerequisites: +# - ethrex L2 running with --circuit-breaker-url (see: make init-l2-circuit-breaker) +# - Credible Layer sidecar running (see: make init-circuit-breaker) +# - cast (from foundry) installed +# - A funded account on L2 +# +# Usage: +# ./circuit_breaker_e2e.sh [L2_RPC_URL] [SIDECAR_HEALTH_URL] + +set -euo pipefail + +L2_RPC_URL="${1:-http://localhost:1729}" +SIDECAR_HEALTH_URL="${2:-http://localhost:9547/health}" +PRIVATE_KEY="${PRIVATE_KEY:-0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +pass() { echo -e "${GREEN}[PASS]${NC} $1"; } +fail() { echo -e "${RED}[FAIL]${NC} $1"; exit 1; } +info() { echo -e "${YELLOW}[INFO]${NC} $1"; } + +# ─── Step 1: Check ethrex L2 is running ─────────────────────────────────────── + +info "Checking ethrex L2 at ${L2_RPC_URL}..." +BLOCK_NUMBER=$(cast block-number --rpc-url "$L2_RPC_URL" 2>/dev/null || echo "UNREACHABLE") +if [ "$BLOCK_NUMBER" = "UNREACHABLE" ]; then + fail "ethrex L2 is not reachable at ${L2_RPC_URL}" +fi +pass "ethrex L2 is running. Current block: ${BLOCK_NUMBER}" + +# ─── Step 2: Check sidecar is running ───────────────────────────────────────── + +info "Checking sidecar health at ${SIDECAR_HEALTH_URL}..." +HEALTH=$(curl -s -o /dev/null -w "%{http_code}" "$SIDECAR_HEALTH_URL" 2>/dev/null || echo "000") +if [ "$HEALTH" = "200" ]; then + pass "Sidecar is healthy" +elif [ "$HEALTH" = "000" ]; then + info "Sidecar health endpoint not reachable (may be OK if running without health server)" +else + info "Sidecar health returned HTTP ${HEALTH}" +fi + +# ─── Step 3: Deploy OwnableTarget contract ──────────────────────────────────── + +info "Deploying OwnableTarget contract..." + +# OwnableTarget bytecode (compiled from contracts/src/circuit_breaker/OwnableTarget.sol) +# If you need to recompile: solc --bin OwnableTarget.sol +# For now, we attempt to deploy using cast +OWNABLE_TARGET_DEPLOY=$(cast send --create \ + --rpc-url "$L2_RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --json \ + "$(cat <<'SOLC_EOF' +0x608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102f8806100606000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063131a06801461004657806370a082311461006a578063f2fde38b14610088575b600080fd5b61004e6100a4565b604051808260001916815260200191505060405180910390f35b6100726100ae565b6040518082815260200191505060405180910390f35b6100a2600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506100b7565b005b6000602a905090565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610165576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602481526020018061029f6024913960400191505060405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156101eb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260348152602001806102c36034913960400191505060405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505056fe4f776e61626c655461726765743a2063616c6c6572206973206e6f7420746865206f776e65724f776e61626c655461726765743a206e6577206f776e657220697320746865207a65726f206164647265737300 +SOLC_EOF +)" 2>/dev/null) || true + +if [ -n "$OWNABLE_TARGET_DEPLOY" ]; then + CONTRACT_ADDRESS=$(echo "$OWNABLE_TARGET_DEPLOY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('contractAddress',''))" 2>/dev/null || echo "") + if [ -n "$CONTRACT_ADDRESS" ]; then + pass "OwnableTarget deployed at: ${CONTRACT_ADDRESS}" + else + info "Deploy transaction sent but contract address not parsed. Check logs." + fi +else + info "Could not deploy OwnableTarget. You may need to deploy it manually." + info "Compile with: solc --bin crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol" +fi + +# ─── Step 4: Assertion registration (manual) ────────────────────────────────── + +echo "" +info "=== MANUAL STEP ===" +info "To complete the e2e test, you need to:" +info " 1. Deploy TestOwnershipAssertion using pcl:" +info " pcl apply --assertion TestOwnershipAssertion --adopter ${CONTRACT_ADDRESS:-}" +info " 2. Wait for the assertion timelock to expire" +info " 3. Then run this script again with the --validate flag" +echo "" + +# ─── Step 5 & 6: Validation (run with --validate after assertion is active) ── + +if [ "${3:-}" = "--validate" ] && [ -n "${CONTRACT_ADDRESS:-}" ]; then + info "Running validation..." + + # Get current block number + BLOCK_BEFORE=$(cast block-number --rpc-url "$L2_RPC_URL") + + # Send violating transaction: transferOwnership + info "Sending violating transaction (transferOwnership)..." + VIOLATING_TX=$(cast send "$CONTRACT_ADDRESS" \ + "transferOwnership(address)" \ + "0x0000000000000000000000000000000000000001" \ + --rpc-url "$L2_RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --json 2>/dev/null) || true + + sleep 5 # Wait for a block + + # Send valid transaction: doSomething + info "Sending valid transaction (doSomething)..." + VALID_TX=$(cast send "$CONTRACT_ADDRESS" \ + "doSomething()" \ + --rpc-url "$L2_RPC_URL" \ + --private-key "$PRIVATE_KEY" \ + --json 2>/dev/null) || true + + sleep 5 # Wait for a block + + # Check if valid tx was included + if [ -n "$VALID_TX" ]; then + VALID_TX_HASH=$(echo "$VALID_TX" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transactionHash',''))" 2>/dev/null || echo "") + if [ -n "$VALID_TX_HASH" ]; then + RECEIPT=$(cast receipt "$VALID_TX_HASH" --rpc-url "$L2_RPC_URL" --json 2>/dev/null || echo "") + if [ -n "$RECEIPT" ]; then + pass "Valid transaction was included in a block (tx: ${VALID_TX_HASH})" + else + fail "Valid transaction was NOT included (should have been)" + fi + fi + fi + + # Check owner hasn't changed (violating tx should have been dropped) + CURRENT_OWNER=$(cast call "$CONTRACT_ADDRESS" "owner()" --rpc-url "$L2_RPC_URL" 2>/dev/null || echo "") + info "Current owner: ${CURRENT_OWNER}" + info "If ownership hasn't changed, the violating transaction was successfully dropped." + + echo "" + pass "E2E validation complete. Check sidecar logs for assertion evaluation details." +else + info "Skipping validation. Run with '--validate' after assertion registration." +fi + +echo "" +info "Done." diff --git a/crates/l2/sequencer/block_producer.rs b/crates/l2/sequencer/block_producer.rs index 7ffb5501acd..5bc9ca7767a 100644 --- a/crates/l2/sequencer/block_producer.rs +++ b/crates/l2/sequencer/block_producer.rs @@ -39,6 +39,11 @@ use serde_json::Value; use std::str::FromStr; use tokio::sync::broadcast; +use super::circuit_breaker::{ + CircuitBreakerClient, + client::CircuitBreakerConfig as ClientCircuitBreakerConfig, + sidecar_proto::{BlobExcessGasAndPrice, BlockEnv, CommitHead, NewIteration}, +}; use super::errors::BlockProducerError; use ethrex_metrics::metrics; @@ -65,6 +70,7 @@ pub struct BlockProducer { block_gas_limit: u64, eth_client: EthClient, router_address: Address, + circuit_breaker: Option>, /// Broadcast sender for new block header notifications to WS subscribers. new_heads_sender: Option>, } @@ -79,7 +85,7 @@ pub struct BlockProducerHealth { impl BlockProducer { #[expect(clippy::too_many_arguments)] - pub fn new( + pub async fn new( config: &BlockProducerConfig, l1_rpc_url: Vec, store: Store, @@ -88,6 +94,7 @@ impl BlockProducer { sequencer_state: SequencerState, router_address: Address, l2_gas_limit: u64, + circuit_breaker_url: Option, new_heads_sender: Option>, ) -> Result { let BlockProducerConfig { @@ -114,6 +121,25 @@ impl BlockProducer { ); } + let circuit_breaker = if let Some(url) = circuit_breaker_url { + let cb_config = ClientCircuitBreakerConfig { + sidecar_url: url, + ..Default::default() + }; + match CircuitBreakerClient::connect(cb_config).await { + Ok(client) => { + info!("Circuit Breaker sidecar connected"); + Some(Arc::new(client)) + } + Err(e) => { + warn!("Failed to connect to Circuit Breaker sidecar: {e}. Proceeding without circuit breaker."); + None + } + } + } else { + None + }; + Ok(Self { store, blockchain, @@ -127,6 +153,7 @@ impl BlockProducer { block_gas_limit: l2_gas_limit, eth_client, router_address, + circuit_breaker, new_heads_sender, }) } @@ -163,6 +190,45 @@ impl BlockProducer { }; let payload = create_payload(&args, &self.store, Bytes::new())?; + // Circuit Breaker: send NewIteration before building the block. + // CommitHead is sent AFTER the block is stored (see below). + // The sidecar flow per block: NewIteration → Transaction(s) → CommitHead + if let Some(cb) = &self.circuit_breaker { + let block_number_bytes = u64_to_u256_bytes(payload.header.number); + let timestamp_bytes = u64_to_u256_bytes(payload.header.timestamp); + let beneficiary_bytes = payload.header.coinbase.as_bytes().to_vec(); + let difficulty_bytes = payload.header.difficulty.to_big_endian().to_vec(); + let prevrandao = Some(payload.header.prev_randao.to_fixed_bytes().to_vec()); + let block_env = BlockEnv { + number: block_number_bytes, + beneficiary: beneficiary_bytes, + timestamp: timestamp_bytes, + gas_limit: payload.header.gas_limit, + basefee: payload.header.base_fee_per_gas.unwrap_or(0), + difficulty: difficulty_bytes, + prevrandao, + blob_excess_gas_and_price: Some(BlobExcessGasAndPrice { + excess_blob_gas: 0, + blob_gasprice: vec![0u8; 16], // u128 big-endian = 0 + }), + }; + let iteration_id = cb.current_iteration_id() + 1; + let parent_block_hash = Some(payload.header.parent_hash.to_fixed_bytes().to_vec()); + let parent_beacon_block_root = payload + .header + .parent_beacon_block_root + .map(|h| h.to_fixed_bytes().to_vec()); + let new_iteration = NewIteration { + block_env: Some(block_env), + iteration_id, + parent_block_hash, + parent_beacon_block_root, + }; + if let Err(e) = cb.send_new_iteration(new_iteration).await { + warn!("Failed to send NewIteration to circuit breaker: {e}"); + } + } + let registered_chains = self.get_registered_l2_chain_ids().await?; // Blockchain builds the payload from mempool txs and executes them @@ -173,6 +239,7 @@ impl BlockProducer { &mut self.privileged_nonces, self.block_gas_limit, registered_chains, + self.circuit_breaker.clone(), ) .await?; info!( @@ -225,6 +292,30 @@ impl BlockProducer { // Make the new head be part of the canonical chain apply_fork_choice(&self.store, block_hash, block_hash, block_hash).await?; + // Circuit Breaker: send CommitHead AFTER block is stored (matches Besu plugin flow) + if let Some(cb) = &self.circuit_breaker { + let last_tx_hash = self + .store + .get_block_by_hash(block_hash) + .await + .ok() + .flatten() + .and_then(|b| b.body.transactions.last().map(|tx| tx.hash().to_fixed_bytes().to_vec())); + #[allow(clippy::as_conversions)] + let commit_head = CommitHead { + last_tx_hash, + n_transactions: transactions_count as u64, + block_number: u64_to_u256_bytes(block_number), + selected_iteration_id: cb.current_iteration_id(), + block_hash: Some(block_hash.to_fixed_bytes().to_vec()), + parent_beacon_block_root: None, + timestamp: u64_to_u256_bytes(block_header.timestamp), + }; + if let Err(e) = cb.send_commit_head(commit_head).await { + warn!("Failed to send CommitHead to circuit breaker: {e}"); + } + } + // Broadcast the new block header to any active eth_subscribe("newHeads") connections. if let Some(sender) = &self.new_heads_sender { match serde_json::to_value(&block_header) { @@ -318,6 +409,13 @@ impl BlockProducer { } } +/// Encode a u64 as a 32-byte big-endian U256 for protobuf fields. +fn u64_to_u256_bytes(value: u64) -> Vec { + let mut buf = [0u8; 32]; + buf[24..].copy_from_slice(&value.to_be_bytes()); + buf.to_vec() +} + #[actor(protocol = BlockProducerProtocol)] impl BlockProducer { pub async fn spawn( @@ -330,6 +428,7 @@ impl BlockProducer { l2_gas_limit: u64, new_heads_sender: Option>, ) -> Result, BlockProducerError> { + let circuit_breaker_url = cfg.circuit_breaker.sidecar_url.clone(); let block_producer = Self::new( &cfg.block_producer, cfg.eth.rpc_url, @@ -339,8 +438,10 @@ impl BlockProducer { sequencer_state, router_address, l2_gas_limit, + circuit_breaker_url, new_heads_sender, - )?; + ) + .await?; let actor_ref = block_producer.start_with_backend(Backend::Blocking); Ok(actor_ref) } diff --git a/crates/l2/sequencer/block_producer/payload_builder.rs b/crates/l2/sequencer/block_producer/payload_builder.rs index ff2de93ba42..c1da635e604 100644 --- a/crates/l2/sequencer/block_producer/payload_builder.rs +++ b/crates/l2/sequencer/block_producer/payload_builder.rs @@ -1,3 +1,7 @@ +use crate::sequencer::circuit_breaker::{ + CircuitBreakerClient, + sidecar_proto::{Transaction as SidecarTransaction, TransactionEnv, TxExecutionId}, +}; use crate::sequencer::errors::BlockProducerError; use ethrex_blockchain::{ Blockchain, @@ -35,6 +39,7 @@ pub async fn build_payload( privileged_nonces: &mut HashMap>, block_gas_limit: u64, registered_chains: Vec, + circuit_breaker: Option>, ) -> Result { let since = Instant::now(); let gas_limit = payload.header.gas_limit; @@ -49,6 +54,7 @@ pub async fn build_payload( privileged_nonces, block_gas_limit, registered_chains, + circuit_breaker, ) .await?; blockchain.finalize_payload(&mut context)?; @@ -100,6 +106,7 @@ pub async fn fill_transactions( privileged_nonces: &mut HashMap>, configured_block_gas_limit: u64, registered_chains: Vec, + circuit_breaker: Option>, ) -> Result<(), BlockProducerError> { let mut privileged_tx_count = 0; let VMType::L2(fee_config) = context.vm.vm_type else { @@ -186,6 +193,41 @@ pub async fn fill_transactions( // TODO: maybe fetch hash too when filtering mempool so we don't have to compute it here (we can do this in the same refactor as adding timestamp) let tx_hash = head_tx.tx.hash(); + // Task 2.5: For non-privileged transactions, check with the circuit breaker sidecar. + // Privileged transactions (PrivilegedL2Transaction) bypass this check. + if let Some(cb) = &circuit_breaker { + if !head_tx.is_privileged() { + let block_num_bytes = { + let mut buf = [0u8; 32]; + let n = context.block_number(); + buf[24..].copy_from_slice(&n.to_be_bytes()); + buf.to_vec() + }; + #[allow(clippy::as_conversions)] + let tx_index = context.payload.body.transactions.len() as u64; + let tx_execution_id = Some(TxExecutionId { + block_number: block_num_bytes, + iteration_id: cb.current_iteration_id(), + tx_hash: tx_hash.as_bytes().to_vec(), + index: tx_index, + }); + let sender = head_tx.tx.sender(); + let tx_as_inner: Transaction = head_tx.clone().into(); + let tx_env = build_transaction_env(&tx_as_inner, sender); + let sidecar_tx = SidecarTransaction { + tx_execution_id, + tx_env: Some(tx_env), + prev_tx_hash: None, + }; + if !cb.evaluate_transaction(sidecar_tx).await { + debug!("Circuit breaker rejected transaction: {tx_hash:#x}"); + txs.pop(); + blockchain.remove_transaction_from_pool(&tx_hash)?; + continue; + } + } + } + // Check whether the tx is replay-protected if head_tx.tx.protected() && !chain_config.is_eip155_activated(context.block_number()) { // Ignore replay protected tx & all txs from the sender @@ -300,3 +342,82 @@ fn fetch_mempool_transactions( } Ok(plain_txs) } + +/// Build a `TransactionEnv` protobuf message from an ethrex transaction and its sender. +fn build_transaction_env(tx: &Transaction, sender: ethrex_common::Address) -> TransactionEnv { + use ethrex_common::types::TxKind; + + let transact_to = match tx.to() { + TxKind::Call(addr) => addr.as_bytes().to_vec(), + TxKind::Create => vec![], + }; + + let value_bytes = tx.value().to_big_endian(); + + let mut gas_price_bytes = [0u8; 16]; + // gas_price fits in u128 for all practical purposes + let gas_price_u128 = tx.gas_price().as_u128(); + gas_price_bytes.copy_from_slice(&gas_price_u128.to_be_bytes()); + + let gas_priority_fee = tx.max_priority_fee().map(|fee| { + let mut buf = [0u8; 16]; + buf[8..].copy_from_slice(&fee.to_be_bytes()); + buf.to_vec() + }); + + let access_list = tx + .access_list() + .iter() + .map(|(addr, keys)| { + use super::super::circuit_breaker::sidecar_proto::AccessListItem; + AccessListItem { + address: addr.as_bytes().to_vec(), + storage_keys: keys.iter().map(|k| k.as_bytes().to_vec()).collect(), + } + }) + .collect(); + + let authorization_list = tx + .authorization_list() + .map(|list| { + use super::super::circuit_breaker::sidecar_proto::Authorization; + list.iter() + .map(|auth| { + let chain_id_bytes = auth.chain_id.to_big_endian(); + let r_bytes = auth.r_signature.to_big_endian(); + let s_bytes = auth.s_signature.to_big_endian(); + Authorization { + chain_id: chain_id_bytes.to_vec(), + address: auth.address.as_bytes().to_vec(), + nonce: auth.nonce, + y_parity: auth.y_parity.as_u32(), + r: r_bytes.to_vec(), + s: s_bytes.to_vec(), + } + }) + .collect() + }) + .unwrap_or_default(); + + #[allow(clippy::as_conversions)] + TransactionEnv { + tx_type: u8::from(tx.tx_type()) as u32, + caller: sender.as_bytes().to_vec(), + gas_limit: tx.gas_limit(), + gas_price: gas_price_bytes.to_vec(), + transact_to, + value: value_bytes.to_vec(), + data: tx.data().to_vec(), + nonce: tx.nonce(), + chain_id: tx.chain_id(), + access_list, + gas_priority_fee, + blob_hashes: tx + .blob_versioned_hashes() + .iter() + .map(|h| h.as_bytes().to_vec()) + .collect(), + max_fee_per_blob_gas: vec![0u8; 16], + authorization_list, + } +} diff --git a/crates/l2/sequencer/circuit_breaker/aeges.rs b/crates/l2/sequencer/circuit_breaker/aeges.rs new file mode 100644 index 00000000000..44c60156ae1 --- /dev/null +++ b/crates/l2/sequencer/circuit_breaker/aeges.rs @@ -0,0 +1,233 @@ +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Duration; + +use tonic::transport::Channel; +use tracing::{debug, info, warn}; + +use super::aeges_proto::{ + aeges_service_client::AegesServiceClient, VerifyTransactionRequest, + Transaction as AegesTransaction, +}; +use super::errors::CircuitBreakerError; + +/// Configuration for the Aeges mempool pre-filter. +#[derive(Debug, Clone)] +pub struct AegesConfig { + /// gRPC endpoint URL for the Aeges service + pub aeges_url: String, + /// Timeout for the VerifyTransaction call + pub timeout: Duration, +} + +impl Default for AegesConfig { + fn default() -> Self { + Self { + aeges_url: "http://localhost:8080".to_string(), + timeout: Duration::from_millis(200), + } + } +} + +/// gRPC client for the Aeges mempool pre-filter service. +/// +/// Validates transactions before mempool admission via a simple unary RPC. +/// On any error or timeout, the transaction is admitted (permissive behavior). +pub struct AegesClient { + config: AegesConfig, + client: AegesServiceClient, + event_id_counter: AtomicU64, +} + +impl AegesClient { + /// Connect to the Aeges service. + pub async fn connect(config: AegesConfig) -> Result { + info!(url = %config.aeges_url, "Connecting to Aeges service"); + + let channel = Channel::from_shared(config.aeges_url.clone()) + .map_err(|e| CircuitBreakerError::Internal(format!("Invalid Aeges URL: {e}")))? + .connect() + .await?; + + let client = AegesServiceClient::new(channel); + + info!("Connected to Aeges service"); + + Ok(Self { + config, + client, + event_id_counter: AtomicU64::new(1), + }) + } + + /// Verify a transaction with the Aeges service. + /// + /// Returns `true` if the transaction should be admitted to the mempool, + /// `false` if it should be rejected. + /// On any error or timeout, returns `true` (permissive behavior). + pub async fn verify_transaction(&self, transaction: AegesTransaction) -> bool { + let event_id = self.event_id_counter.fetch_add(1, Ordering::Relaxed); + + let request = VerifyTransactionRequest { + event_id, + transaction: Some(transaction), + }; + + let result = tokio::time::timeout(self.config.timeout, { + let mut client = self.client.clone(); + async move { client.verify_transaction(request).await } + }) + .await; + + match result { + Ok(Ok(response)) => { + let denied = response.into_inner().denied; + if denied { + debug!(event_id, "Aeges denied transaction"); + } + !denied + } + Ok(Err(status)) => { + warn!(%status, "Aeges VerifyTransaction failed, admitting tx (permissive)"); + true + } + Err(_) => { + warn!("Aeges VerifyTransaction timed out, admitting tx (permissive)"); + true + } + } + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + use crate::sequencer::circuit_breaker::aeges_proto::{ + Transaction as AegesTransaction, VerifyTransactionRequest, VerifyTransactionResponse, + }; + + // ── AegesConfig defaults ───────────────────────────────────────────────── + + #[test] + fn config_default_url_is_localhost_8080() { + let cfg = AegesConfig::default(); + assert_eq!(cfg.aeges_url, "http://localhost:8080"); + } + + #[test] + fn config_default_timeout_is_200ms() { + let cfg = AegesConfig::default(); + assert_eq!(cfg.timeout, Duration::from_millis(200)); + } + + // ── VerifyTransactionResponse logic ───────────────────────────────────── + + /// Simulate the core result-mapping logic from `verify_transaction` without + /// needing a real gRPC connection. + fn admission_from_response(response: VerifyTransactionResponse) -> bool { + !response.denied + } + + #[test] + fn admitted_when_denied_is_false() { + let resp = VerifyTransactionResponse { + event_id: 1, + denied: false, + }; + assert!(admission_from_response(resp)); + } + + #[test] + fn rejected_when_denied_is_true() { + let resp = VerifyTransactionResponse { + event_id: 2, + denied: true, + }; + assert!(!admission_from_response(resp)); + } + + // ── AegesTransaction construction ──────────────────────────────────────── + + #[test] + fn aeges_transaction_fields_roundtrip() { + let sender = vec![0xaau8; 20]; + let tx_hash = vec![0xbbu8; 32]; + let value = vec![0u8; 32]; + + let tx = AegesTransaction { + hash: tx_hash.clone(), + sender: sender.clone(), + to: None, + value: value.clone(), + nonce: 5, + r#type: 2, + chain_id: Some(1), + payload: vec![], + gas_limit: 21_000, + gas_price: None, + max_fee_per_gas: Some(1_000_000_000), + max_priority_fee_per_gas: Some(100_000_000), + max_fee_per_blob_gas: None, + access_list: vec![], + versioned_hashes: vec![], + code_delegation_list: vec![], + }; + + assert_eq!(tx.hash, tx_hash); + assert_eq!(tx.sender, sender); + assert_eq!(tx.nonce, 5); + assert_eq!(tx.gas_limit, 21_000); + assert!(tx.to.is_none()); + } + + // ── VerifyTransactionRequest construction ──────────────────────────────── + + #[test] + fn verify_request_includes_event_id_and_transaction() { + let tx = AegesTransaction { + hash: vec![1u8; 32], + sender: vec![2u8; 20], + to: Some(vec![3u8; 20]), + value: vec![0u8; 32], + nonce: 0, + r#type: 0, + chain_id: None, + payload: vec![], + gas_limit: 21_000, + gas_price: Some(1_000_000_000), + max_fee_per_gas: None, + max_priority_fee_per_gas: None, + max_fee_per_blob_gas: None, + access_list: vec![], + versioned_hashes: vec![], + code_delegation_list: vec![], + }; + + let req = VerifyTransactionRequest { + event_id: 99, + transaction: Some(tx), + }; + + assert_eq!(req.event_id, 99); + assert!(req.transaction.is_some()); + } + + // ── Permissive behavior on error ───────────────────────────────────────── + + #[test] + fn permissive_behavior_on_grpc_error() { + // Simulate the Err branch: gRPC call returned a status error. + // The match arm returns `true` (admit the tx). + let simulated_result: Result, tonic::Status>, tokio::time::error::Elapsed> = + Ok(Err(tonic::Status::internal("server error"))); + + let admitted = match simulated_result { + Ok(Ok(response)) => !response.into_inner().denied, + Ok(Err(_status)) => true, + Err(_) => true, + }; + + assert!(admitted, "should admit tx when gRPC returns an error status"); + } +} diff --git a/crates/l2/sequencer/circuit_breaker/client.rs b/crates/l2/sequencer/circuit_breaker/client.rs new file mode 100644 index 00000000000..796bce9a35f --- /dev/null +++ b/crates/l2/sequencer/circuit_breaker/client.rs @@ -0,0 +1,424 @@ +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use tokio::sync::{mpsc, Mutex}; +use tonic::transport::Channel; +use tracing::{debug, info, warn}; + +use super::errors::CircuitBreakerError; +use super::sidecar_proto::{ + self, sidecar_transport_client::SidecarTransportClient, CommitHead, Event, + GetTransactionRequest, NewIteration, ResultStatus, Transaction, TransactionResult, + TxExecutionId, +}; + +/// Configuration for the Circuit Breaker gRPC client. +#[derive(Debug, Clone)] +pub struct CircuitBreakerConfig { + /// gRPC endpoint URL for the sidecar (e.g., "http://localhost:50051") + pub sidecar_url: String, + /// Timeout for waiting for a transaction result from the sidecar + pub result_timeout: Duration, + /// Timeout for the GetTransaction fallback poll + pub poll_timeout: Duration, +} + +impl Default for CircuitBreakerConfig { + fn default() -> Self { + Self { + sidecar_url: "http://localhost:50051".to_string(), + result_timeout: Duration::from_millis(500), + poll_timeout: Duration::from_millis(200), + } + } +} + +/// gRPC client for communicating with the Credible Layer Assertion Enforcer sidecar. +/// +/// Maintains a persistent bidirectional `StreamEvents` gRPC stream. Events are sent +/// via an mpsc channel that feeds the stream. Transaction results are retrieved via +/// the `GetTransaction` unary RPC. +pub struct CircuitBreakerClient { + config: CircuitBreakerConfig, + /// Sender side of the persistent StreamEvents stream + event_sender: mpsc::Sender, + /// Monotonically increasing event ID counter + event_id_counter: AtomicU64, + /// Current iteration ID (incremented per block) + iteration_id: AtomicU64, + /// gRPC client for unary calls (GetTransaction) + grpc_client: Arc>>, +} + +impl CircuitBreakerClient { + /// Create a new client with lazy connection to the sidecar. + /// Opens a persistent StreamEvents bidirectional stream in the background. + pub async fn connect(config: CircuitBreakerConfig) -> Result { + info!( + url = %config.sidecar_url, + "Configuring Circuit Breaker sidecar client" + ); + + let channel = Channel::from_shared(config.sidecar_url.clone()) + .map_err(|e| CircuitBreakerError::Internal(format!("Invalid URL: {e}")))? + .connect_timeout(Duration::from_secs(5)) + .timeout(Duration::from_secs(5)) + .connect_lazy(); + + let mut client = SidecarTransportClient::new(channel.clone()); + + // Create the event channel. The sender goes to the client, the receiver + // is owned by the background stream task. + let (event_tx, mut event_rx) = mpsc::channel::(256); + + // Background task: maintains a persistent StreamEvents connection. + // Reads events from the mpsc channel and forwards them to the gRPC stream. + // Reconnects automatically if the connection drops. + tokio::spawn(async move { + loop { + // Create a new gRPC-side channel for each connection attempt + let (grpc_tx, grpc_rx) = mpsc::channel::(64); + let grpc_stream = tokio_stream::wrappers::ReceiverStream::new(grpc_rx); + + match client.stream_events(grpc_stream).await { + Ok(response) => { + info!("StreamEvents stream connected to sidecar"); + let mut ack_stream = response.into_inner(); + + // Send an initial CommitHead (block 0) as the first event on + // every new stream — the sidecar requires CommitHead first. + let init_commit = Event { + event_id: 0, + event: Some(sidecar_proto::event::Event::CommitHead(CommitHead { + last_tx_hash: None, + n_transactions: 0, + block_number: vec![0u8; 32], + selected_iteration_id: 0, + block_hash: Some(vec![0u8; 32]), + parent_beacon_block_root: None, + timestamp: vec![0u8; 32], + })), + }; + if grpc_tx.send(init_commit).await.is_err() { + warn!("Failed to send initial CommitHead"); + continue; + } + + // Forward events from the main channel to the gRPC stream + // while also reading acks + loop { + tokio::select! { + // Read events from the main channel and forward to gRPC + event = event_rx.recv() => { + match event { + Some(e) => { + let event_type = match &e.event { + Some(sidecar_proto::event::Event::CommitHead(_)) => "CommitHead", + Some(sidecar_proto::event::Event::NewIteration(_)) => "NewIteration", + Some(sidecar_proto::event::Event::Transaction(_)) => "Transaction", + Some(sidecar_proto::event::Event::Reorg(_)) => "Reorg", + None => "None", + }; + debug!("Forwarding {event_type} event to gRPC stream (event_id={})", e.event_id); + if grpc_tx.send(e).await.is_err() { + warn!("gRPC stream send failed, reconnecting"); + break; + } + } + None => { + // Main channel closed — client dropped + debug!("Event channel closed, stopping stream task"); + return; + } + } + } + // Read acks from sidecar + ack = ack_stream.message() => { + match ack { + Ok(Some(a)) => { + if !a.success { + warn!(event_id = a.event_id, msg = %a.message, "Sidecar rejected event"); + } + } + Ok(None) => { + info!("StreamEvents ack stream ended, reconnecting"); + break; + } + Err(status) => { + warn!(%status, "StreamEvents ack error, reconnecting"); + break; + } + } + } + } + } + } + Err(status) => { + debug!(%status, "StreamEvents connect failed, retrying in 5s"); + } + } + tokio::time::sleep(Duration::from_secs(5)).await; + } + }); + + info!("Circuit Breaker client ready (persistent stream opened)"); + + Ok(Self { + config, + event_sender: event_tx, + event_id_counter: AtomicU64::new(1), + iteration_id: AtomicU64::new(0), + grpc_client: Arc::new(Mutex::new(SidecarTransportClient::new(channel))), + }) + } + + /// Get the next event ID. + fn next_event_id(&self) -> u64 { + self.event_id_counter.fetch_add(1, Ordering::Relaxed) + } + + /// Get the current iteration ID. + pub fn current_iteration_id(&self) -> u64 { + self.iteration_id.load(Ordering::Relaxed) + } + + /// Send a CommitHead event (previous block finalized). + pub async fn send_commit_head( + &self, + commit_head: CommitHead, + ) -> Result<(), CircuitBreakerError> { + let event = Event { + event_id: self.next_event_id(), + event: Some(sidecar_proto::event::Event::CommitHead(commit_head)), + }; + self.event_sender + .send(event) + .await + .map_err(|_| CircuitBreakerError::StreamClosed) + } + + /// Send a NewIteration event (new block started) and increment the iteration ID. + pub async fn send_new_iteration( + &self, + new_iteration: NewIteration, + ) -> Result<(), CircuitBreakerError> { + self.iteration_id.fetch_add(1, Ordering::Relaxed); + let event = Event { + event_id: self.next_event_id(), + event: Some(sidecar_proto::event::Event::NewIteration(new_iteration)), + }; + self.event_sender + .send(event) + .await + .map_err(|_| CircuitBreakerError::StreamClosed) + } + + /// Send a Transaction event and wait for the sidecar's verdict. + /// + /// Returns `true` if the transaction should be included, `false` if it should be dropped. + /// On any error or timeout, returns `true` (permissive behavior). + pub async fn evaluate_transaction(&self, transaction: Transaction) -> bool { + let tx_exec_id = transaction.tx_execution_id.clone(); + let tx_hash = tx_exec_id + .as_ref() + .map(|id| id.tx_hash.clone()) + .unwrap_or_default(); + let block_number = tx_exec_id + .as_ref() + .map(|id| id.block_number.clone()) + .unwrap_or_default(); + let index = tx_exec_id.as_ref().map(|id| id.index).unwrap_or(0); + + // Send the transaction event on the persistent stream + let event = Event { + event_id: self.next_event_id(), + event: Some(sidecar_proto::event::Event::Transaction(transaction)), + }; + if self.event_sender.send(event).await.is_err() { + warn!("StreamEvents channel closed, including tx (permissive)"); + return true; + } + + // Poll for result with retries (sidecar evaluates async). + // The sidecar needs time to receive the tx event (via async stream), + // evaluate it, and make the result available. + let poll_attempts = 10; + let poll_interval = Duration::from_millis(200); + for attempt in 0..poll_attempts { + tokio::time::sleep(poll_interval).await; + let result = self.poll_transaction_result(&tx_hash, &block_number, index).await; + match result { + PollResult::Found(include) => return include, + PollResult::NotFound => { + debug!("GetTransaction poll attempt {}/{}: not found yet", attempt + 1, poll_attempts); + continue; + } + PollResult::Error => return true, // permissive + } + } + warn!("GetTransaction: no result after {poll_attempts} attempts, including tx (permissive)"); + true + } + + /// Poll for a transaction result via GetTransaction unary RPC. + async fn poll_transaction_result(&self, tx_hash: &[u8], block_number: &[u8], index: u64) -> PollResult { + let tx_exec_id = TxExecutionId { + block_number: block_number.to_vec(), + iteration_id: self.current_iteration_id(), + tx_hash: tx_hash.to_vec(), + index, + }; + + let request = GetTransactionRequest { + tx_execution_id: Some(tx_exec_id), + }; + + let poll_result = tokio::time::timeout(self.config.poll_timeout, async { + let mut client = self.grpc_client.lock().await; + client.get_transaction(request).await + }) + .await; + + match poll_result { + Ok(Ok(response)) => { + let inner = response.into_inner(); + match inner.outcome { + Some(sidecar_proto::get_transaction_response::Outcome::Result(result)) => { + PollResult::Found(!is_assertion_failed(&result)) + } + Some(sidecar_proto::get_transaction_response::Outcome::NotFound(_)) => { + PollResult::NotFound + } + None => PollResult::NotFound, + } + } + Ok(Err(status)) => { + warn!(%status, "GetTransaction poll failed, including tx (permissive)"); + PollResult::Error + } + Err(_) => { + warn!("GetTransaction poll timed out, including tx (permissive)"); + PollResult::Error + } + } + } +} + +enum PollResult { + Found(bool), // true = include, false = drop + NotFound, + Error, +} + +/// Check if a TransactionResult indicates assertion failure. +fn is_assertion_failed(result: &TransactionResult) -> bool { + result.status() == ResultStatus::AssertionFailed +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + use crate::sequencer::circuit_breaker::sidecar_proto::{ + CommitHead, NewIteration, ResultStatus, TransactionResult, TxExecutionId, + }; + + #[test] + fn config_default_url_is_localhost_50051() { + let cfg = CircuitBreakerConfig::default(); + assert_eq!(cfg.sidecar_url, "http://localhost:50051"); + } + + #[test] + fn config_default_result_timeout_is_500ms() { + let cfg = CircuitBreakerConfig::default(); + assert_eq!(cfg.result_timeout, Duration::from_millis(500)); + } + + #[test] + fn config_default_poll_timeout_is_200ms() { + let cfg = CircuitBreakerConfig::default(); + assert_eq!(cfg.poll_timeout, Duration::from_millis(200)); + } + + fn make_result(status: ResultStatus) -> TransactionResult { + TransactionResult { + tx_execution_id: None, + status: status as i32, + gas_used: 0, + error: String::new(), + } + } + + #[test] + fn assertion_failed_returns_true_for_assertion_failed_status() { + assert!(is_assertion_failed(&make_result(ResultStatus::AssertionFailed))); + } + + #[test] + fn assertion_failed_returns_false_for_success_status() { + assert!(!is_assertion_failed(&make_result(ResultStatus::Success))); + } + + #[test] + fn assertion_failed_returns_false_for_reverted_status() { + assert!(!is_assertion_failed(&make_result(ResultStatus::Reverted))); + } + + #[test] + fn assertion_failed_returns_false_for_halted_status() { + assert!(!is_assertion_failed(&make_result(ResultStatus::Halted))); + } + + #[test] + fn assertion_failed_returns_false_for_failed_status() { + assert!(!is_assertion_failed(&make_result(ResultStatus::Failed))); + } + + #[test] + fn assertion_failed_returns_false_for_unspecified_status() { + assert!(!is_assertion_failed(&make_result(ResultStatus::Unspecified))); + } + + #[test] + fn commit_head_fields_are_set_correctly() { + let block_number: Vec = std::iter::repeat(0u8).take(31).chain(std::iter::once(42u8)).collect(); + let timestamp: Vec = std::iter::repeat(0u8).take(31).chain(std::iter::once(1u8)).collect(); + let ch = CommitHead { + last_tx_hash: None, n_transactions: 5, block_number: block_number.clone(), + selected_iteration_id: 3, block_hash: None, parent_beacon_block_root: None, + timestamp: timestamp.clone(), + }; + assert_eq!(ch.n_transactions, 5); + assert_eq!(ch.block_number, block_number); + assert_eq!(ch.selected_iteration_id, 3); + } + + #[test] + fn tx_execution_id_fields_are_set_correctly() { + let id = TxExecutionId { block_number: vec![0; 32], iteration_id: 7, tx_hash: vec![0xab; 32], index: 2 }; + assert_eq!(id.iteration_id, 7); + assert_eq!(id.index, 2); + } + + #[test] + fn new_iteration_has_expected_iteration_id() { + use crate::sequencer::circuit_breaker::sidecar_proto::BlockEnv; + let ni = NewIteration { + block_env: Some(BlockEnv { number: vec![0; 32], beneficiary: vec![0; 20], timestamp: vec![0; 32], gas_limit: 30_000_000, basefee: 1_000_000_000, difficulty: vec![0; 32], prevrandao: None, blob_excess_gas_and_price: None }), + iteration_id: 42, parent_block_hash: None, parent_beacon_block_root: None, + }; + assert_eq!(ni.iteration_id, 42); + } + + #[test] + fn event_id_counter_increments() { + use std::sync::atomic::{AtomicU64, Ordering}; + let c = AtomicU64::new(1); + assert_eq!(c.fetch_add(1, Ordering::Relaxed), 1); + assert_eq!(c.fetch_add(1, Ordering::Relaxed), 2); + assert_eq!(c.fetch_add(1, Ordering::Relaxed), 3); + } +} diff --git a/crates/l2/sequencer/circuit_breaker/errors.rs b/crates/l2/sequencer/circuit_breaker/errors.rs new file mode 100644 index 00000000000..eea1aab76ba --- /dev/null +++ b/crates/l2/sequencer/circuit_breaker/errors.rs @@ -0,0 +1,15 @@ +use tonic::Status; + +#[derive(Debug, thiserror::Error)] +pub enum CircuitBreakerError { + #[error("gRPC transport error: {0}")] + Transport(#[from] tonic::transport::Error), + #[error("gRPC status error: {0}")] + Status(#[from] Status), + #[error("Stream closed unexpectedly")] + StreamClosed, + #[error("Result timeout for tx {0}")] + ResultTimeout(String), + #[error("Circuit Breaker error: {0}")] + Internal(String), +} diff --git a/crates/l2/sequencer/circuit_breaker/mock_sidecar/main.rs b/crates/l2/sequencer/circuit_breaker/mock_sidecar/main.rs new file mode 100644 index 00000000000..828426304c5 --- /dev/null +++ b/crates/l2/sequencer/circuit_breaker/mock_sidecar/main.rs @@ -0,0 +1,229 @@ +/// Mock Circuit Breaker Sidecar for end-to-end testing. +/// +/// Implements the sidecar.proto gRPC protocol and rejects any transaction +/// that calls the `transferOwnership(address)` function selector (0xf2fde38b). +/// +/// Usage: +/// cargo run --bin mock-sidecar +/// +/// The mock listens on 0.0.0.0:50051 (same as the real sidecar). +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; + +use tokio::sync::{Mutex, mpsc}; +use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream}; +use tonic::{Request, Response, Status, transport::Server}; + +// Include the generated protobuf code +pub mod sidecar_proto { + tonic::include_proto!("sidecar.transport.v1"); +} + +use sidecar_proto::{ + Event, GetTransactionRequest, GetTransactionResponse, GetTransactionsRequest, + GetTransactionsResponse, ResultStatus, StreamAck, SubscribeResultsRequest, TransactionResult, + TxExecutionId, + get_transaction_response::Outcome, + sidecar_transport_server::{SidecarTransport, SidecarTransportServer}, +}; + +/// The `transferOwnership(address)` function selector +const TRANSFER_OWNERSHIP_SELECTOR: [u8; 4] = [0xf2, 0xfd, 0xe3, 0x8b]; + +/// Shared state: stores transaction results keyed by tx_hash +type ResultStore = Arc, TransactionResult>>>; + +pub struct MockSidecar { + results: ResultStore, +} + +impl MockSidecar { + fn new() -> Self { + Self { + results: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +/// Evaluate a transaction: returns ASSERTION_FAILED if it calls transferOwnership, +/// SUCCESS otherwise. +fn evaluate_tx(event: &sidecar_proto::Transaction) -> ResultStatus { + let calldata = event + .tx_env + .as_ref() + .map(|env| &env.data[..]) + .unwrap_or(&[]); + + if calldata.len() >= 4 && calldata[..4] == TRANSFER_OWNERSHIP_SELECTOR { + ResultStatus::AssertionFailed + } else { + ResultStatus::Success + } +} + +#[tonic::async_trait] +impl SidecarTransport for MockSidecar { + type StreamEventsStream = + Pin> + Send + 'static>>; + type SubscribeResultsStream = + Pin> + Send + 'static>>; + + async fn stream_events( + &self, + request: Request>, + ) -> Result, Status> { + let mut stream = request.into_inner(); + let (tx, rx) = mpsc::channel(128); + let results = self.results.clone(); + + tokio::spawn(async move { + let mut events_processed: u64 = 0; + while let Some(Ok(event)) = stream.next().await { + let event_id = event.event_id; + events_processed += 1; + + match &event.event { + Some(sidecar_proto::event::Event::CommitHead(ch)) => { + eprintln!( + "[MOCK] CommitHead: n_transactions={}", + ch.n_transactions + ); + } + Some(sidecar_proto::event::Event::NewIteration(ni)) => { + eprintln!( + "[MOCK] NewIteration: iteration_id={}", + ni.iteration_id + ); + } + Some(sidecar_proto::event::Event::Transaction(t)) => { + let tx_hash = t + .tx_execution_id + .as_ref() + .map(|id| id.tx_hash.clone()) + .unwrap_or_default(); + let tx_hash_hex = hex::encode(&tx_hash); + + let status = evaluate_tx(t); + + let result = TransactionResult { + tx_execution_id: t.tx_execution_id.clone(), + status: status as i32, + gas_used: 21000, + error: String::new(), + }; + + // Store the result so GetTransaction can find it + { + let mut store = results.lock().await; + store.insert(tx_hash.clone(), result); + } + + match status { + ResultStatus::AssertionFailed => { + eprintln!( + "[MOCK] TX {}: ASSERTION_FAILED (transferOwnership)", + &tx_hash_hex[..std::cmp::min(16, tx_hash_hex.len())] + ); + } + _ => { + eprintln!( + "[MOCK] TX {}: SUCCESS", + &tx_hash_hex[..std::cmp::min(16, tx_hash_hex.len())] + ); + } + } + } + Some(sidecar_proto::event::Event::Reorg(_)) => { + eprintln!("[MOCK] ReorgEvent received"); + } + None => {} + } + + let ack = StreamAck { + success: true, + message: String::new(), + events_processed, + event_id, + }; + if tx.send(Ok(ack)).await.is_err() { + break; + } + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + async fn subscribe_results( + &self, + _request: Request, + ) -> Result, Status> { + // Return an empty stream — results are delivered via GetTransaction + let (_tx, rx) = mpsc::channel(1); + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + async fn get_transactions( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(GetTransactionsResponse { + results: vec![], + not_found: vec![], + })) + } + + async fn get_transaction( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let tx_hash = req + .tx_execution_id + .as_ref() + .map(|id| id.tx_hash.clone()) + .unwrap_or_default(); + + // Look up stored result from stream_events processing + let store = self.results.lock().await; + if let Some(result) = store.get(&tx_hash) { + let status_name = match ResultStatus::try_from(result.status) { + Ok(ResultStatus::AssertionFailed) => "ASSERTION_FAILED", + Ok(ResultStatus::Success) => "SUCCESS", + _ => "OTHER", + }; + eprintln!( + "[MOCK] GetTransaction {}: returning {}", + &hex::encode(&tx_hash)[..std::cmp::min(16, tx_hash.len() * 2)], + status_name + ); + Ok(Response::new(GetTransactionResponse { + outcome: Some(Outcome::Result(result.clone())), + })) + } else { + // Not found yet — the stream_events call may not have completed + eprintln!( + "[MOCK] GetTransaction {}: NOT_FOUND", + &hex::encode(&tx_hash)[..std::cmp::min(16, tx_hash.len() * 2)] + ); + Ok(Response::new(GetTransactionResponse { + outcome: Some(Outcome::NotFound(tx_hash)), + })) + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "0.0.0.0:50051".parse()?; + eprintln!("[MOCK SIDECAR] Starting on {addr}"); + eprintln!("[MOCK SIDECAR] Will reject transactions calling transferOwnership(address) [0xf2fde38b]"); + + Server::builder() + .add_service(SidecarTransportServer::new(MockSidecar::new())) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/crates/l2/sequencer/circuit_breaker/mod.rs b/crates/l2/sequencer/circuit_breaker/mod.rs new file mode 100644 index 00000000000..00ffea3272d --- /dev/null +++ b/crates/l2/sequencer/circuit_breaker/mod.rs @@ -0,0 +1,25 @@ +/// Circuit Breaker integration with the Phylax Credible Layer. +/// +/// This module implements a gRPC client that communicates with the Credible Layer +/// Assertion Enforcer sidecar during block building. Transactions that fail assertion +/// validation are dropped before block inclusion. +/// +/// The integration is opt-in via the `--circuit-breaker-url` CLI flag. +/// When disabled, there is zero overhead. +pub mod client; +pub mod aeges; +pub mod errors; + +pub use client::CircuitBreakerClient; +pub use aeges::AegesClient; +pub use errors::CircuitBreakerError; + +/// Generated protobuf/gRPC types for sidecar.proto +pub mod sidecar_proto { + tonic::include_proto!("sidecar.transport.v1"); +} + +/// Generated protobuf/gRPC types for aeges.proto +pub mod aeges_proto { + tonic::include_proto!("aeges.v1"); +} diff --git a/crates/l2/sequencer/configs.rs b/crates/l2/sequencer/configs.rs index 66deab3bcbf..fb0518030da 100644 --- a/crates/l2/sequencer/configs.rs +++ b/crates/l2/sequencer/configs.rs @@ -17,6 +17,21 @@ pub struct SequencerConfig { pub monitor: MonitorConfig, pub admin_server: AdminConfig, pub state_updater: StateUpdaterConfig, + pub circuit_breaker: CircuitBreakerConfig, +} + +/// Configuration for the Circuit Breaker sidecar integration. +/// Both URLs are optional; if absent, the feature is disabled. +#[derive(Clone, Debug, Default)] +pub struct CircuitBreakerConfig { + /// gRPC endpoint for the Credible Layer Assertion Enforcer sidecar. + pub sidecar_url: Option, + /// gRPC endpoint for the Aeges mempool pre-filter service. + pub aeges_url: Option, + /// Address of the already-deployed State Oracle contract on L2. + /// Required by the Credible Layer sidecar for assertion registry lookups. + /// Deploy the State Oracle separately using the Phylax toolchain before starting ethrex. + pub state_oracle_address: Option
, } // TODO: Move to blockchain/dev diff --git a/crates/l2/sequencer/mod.rs b/crates/l2/sequencer/mod.rs index 08248df530d..ef718e39074 100644 --- a/crates/l2/sequencer/mod.rs +++ b/crates/l2/sequencer/mod.rs @@ -39,6 +39,7 @@ pub mod proof_coordinator; pub use ethrex_l2_common::sequencer_state::{SequencerState, SequencerStatus}; pub mod state_updater; +pub mod circuit_breaker; pub mod configs; pub mod errors; pub mod setup; diff --git a/crates/l2/sidecar-config.template.json b/crates/l2/sidecar-config.template.json new file mode 100644 index 00000000000..9974538a653 --- /dev/null +++ b/crates/l2/sidecar-config.template.json @@ -0,0 +1,47 @@ +{ + "chain": { + "spec_id": "CANCUN", + "chain_id": 1729 + }, + "credible": { + "assertion_gas_limit": 3000000, + "cache_capacity_bytes": 256000000, + "flush_every_ms": 5000, + "assertion_da_rpc_url": "http://127.0.0.1:5001", + "event_source_url": "http://127.0.0.1:4350/graphql", + "poll_interval": 1000, + "assertion_store_db_path": ".local/sidecar/assertion_store_database", + "transaction_observer_db_path": ".local/sidecar/transaction_observer_database", + "transaction_observer_endpoint": null, + "transaction_observer_auth_token": "", + "transaction_observer_endpoint_rps_max": 60, + "transaction_observer_poll_interval_ms": 1000, + "transaction_observer_backoff_initial_ms": 1000, + "transaction_observer_backoff_max_ms": 5000, + "transaction_observer_backoff_multiplier": 2, + "aeges_url": "http://127.0.0.1:8080", + "state_oracle": "0x__STATE_ORACLE_ADDRESS__", + "state_oracle_deployment_block": 0, + "transaction_results_max_capacity": 1000, + "accepted_txs_ttl_ms": 600000, + "assertion_store_prune_config_interval_ms": 60000, + "assertion_store_prune_config_retention_blocks": 0 + }, + "transport": { + "bind_addr": "0.0.0.0:50051", + "health_bind_addr": "0.0.0.0:9547" + }, + "state": { + "sources": [ + { + "type": "eth-rpc", + "ws_url": "ws://127.0.0.1:1730", + "http_url": "http://127.0.0.1:1729" + } + ], + "minimum_state_diff": 100, + "sources_sync_timeout_ms": 1000, + "sources_monitoring_period_ms": 500, + "enable_parallel_sources": false + } +} diff --git a/crates/networking/rpc/eth/logs.rs b/crates/networking/rpc/eth/logs.rs index 2601f0533a5..96985319adb 100644 --- a/crates/networking/rpc/eth/logs.rs +++ b/crates/networking/rpc/eth/logs.rs @@ -80,15 +80,15 @@ impl RpcHandler for LogsFilter { }) .transpose()? .flatten(); - let topics_filters = param - .get("topics") - .ok_or_else(|| RpcErr::MissingParam("topics".to_string())) - .and_then(|topics| { + let topics_filters = match param.get("topics") { + Some(topics) => { match serde_json::from_value::>>(topics.clone()) { - Ok(filters) => Ok(filters), - _ => Err(RpcErr::WrongParam("topics".to_string())), + Ok(filters) => filters, + _ => return Err(RpcErr::WrongParam("topics".to_string())), } - })?; + } + None => None, + }; Ok(LogsFilter { from_block, to_block, diff --git a/crates/networking/rpc/tracing.rs b/crates/networking/rpc/tracing.rs index c1fba8ac32d..594a393720c 100644 --- a/crates/networking/rpc/tracing.rs +++ b/crates/networking/rpc/tracing.rs @@ -1,7 +1,10 @@ use std::time::Duration; use ethrex_common::H256; -use ethrex_common::{serde_utils, tracing::CallTraceFrame}; +use ethrex_common::{ + serde_utils, + tracing::{CallTraceFrame, PrePostState, PrestateTrace}, +}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -41,6 +44,7 @@ struct TraceConfig { enum TracerType { #[default] CallTracer, + PrestateTracer, } #[derive(Deserialize, Default)] @@ -52,6 +56,13 @@ struct CallTracerConfig { with_log: bool, } +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase")] +struct PrestateTracerConfig { + #[serde(default)] + diff_mode: bool, +} + type BlockTrace = Vec>; #[derive(Serialize)] @@ -96,7 +107,6 @@ impl RpcHandler for TraceTransactionRequest { ) -> Result { let reexec = self.trace_config.reexec.unwrap_or(DEFAULT_REEXEC); let timeout = self.trace_config.timeout.unwrap_or(DEFAULT_TIMEOUT); - // This match will make more sense once we support other tracers match self.trace_config.tracer { TracerType::CallTracer => { // Parse tracer config now that we know the type @@ -124,6 +134,30 @@ impl RpcHandler for TraceTransactionRequest { .ok_or(RpcErr::Internal("Empty call trace".to_string()))?; Ok(serde_json::to_value(top_frame)?) } + TracerType::PrestateTracer => { + let config = if let Some(value) = &self.trace_config.tracer_config { + serde_json::from_value(value.clone())? + } else { + PrestateTracerConfig::default() + }; + let (pre_trace, pre_post) = context + .blockchain + .trace_transaction_prestate( + self.tx_hash, + reexec, + timeout, + config.diff_mode, + ) + .await + .map_err(|err| RpcErr::Internal(err.to_string()))?; + if config.diff_mode { + Ok(serde_json::to_value( + pre_post.unwrap_or_default(), + )?) + } else { + Ok(serde_json::to_value(pre_trace)?) + } + } } } } @@ -166,7 +200,6 @@ impl RpcHandler for TraceBlockByNumberRequest { .ok_or(RpcErr::Internal("Block not Found".to_string()))?; let reexec = self.trace_config.reexec.unwrap_or(DEFAULT_REEXEC); let timeout = self.trace_config.timeout.unwrap_or(DEFAULT_TIMEOUT); - // This match will make more sense once we support other tracers match self.trace_config.tracer { TracerType::CallTracer => { // Parse tracer config now that we know the type @@ -200,6 +233,31 @@ impl RpcHandler for TraceBlockByNumberRequest { .collect::>()?; Ok(serde_json::to_value(block_trace)?) } + TracerType::PrestateTracer => { + let config = if let Some(value) = &self.trace_config.tracer_config { + serde_json::from_value(value.clone())? + } else { + PrestateTracerConfig::default() + }; + let prestate_traces = context + .blockchain + .trace_block_prestate(block, reexec, timeout, config.diff_mode) + .await + .map_err(|err| RpcErr::Internal(err.to_string()))?; + if config.diff_mode { + let block_trace: BlockTrace = prestate_traces + .into_iter() + .map(|(hash, _, pre_post)| (hash, pre_post.unwrap_or_default()).into()) + .collect(); + Ok(serde_json::to_value(block_trace)?) + } else { + let block_trace: BlockTrace = prestate_traces + .into_iter() + .map(|(hash, pre_trace, _)| (hash, pre_trace).into()) + .collect(); + Ok(serde_json::to_value(block_trace)?) + } + } } } } diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 9f0e08412f1..3a0e43d6110 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -14,6 +14,7 @@ ethrex-rlp.workspace = true derive_more = { version = "1.0.0", features = ["full"] } bytes.workspace = true +hex.workspace = true thiserror.workspace = true tracing.workspace = true serde.workspace = true diff --git a/crates/vm/backends/levm/tracing.rs b/crates/vm/backends/levm/tracing.rs index 9777f4dfbeb..2770a139f40 100644 --- a/crates/vm/backends/levm/tracing.rs +++ b/crates/vm/backends/levm/tracing.rs @@ -1,6 +1,9 @@ +use ethrex_common::constants::EMPTY_KECCACK_HASH; +use ethrex_common::tracing::{PrePostState, PrestateAccountState, PrestateTrace}; use ethrex_common::types::{Block, Transaction}; use ethrex_common::{tracing::CallTrace, types::BlockHeader}; use ethrex_crypto::Crypto; +use ethrex_levm::db::gen_db::CacheDB; use ethrex_levm::vm::VMType; use ethrex_levm::{db::gen_db::GeneralizedDatabase, tracing::LevmCallTracer, vm::VM}; @@ -43,6 +46,50 @@ impl LEVM { Ok(()) } + /// Execute a transaction and capture the pre/post account state (prestateTracer). + /// + /// Captures a snapshot of all touched accounts before and after execution. + /// The `diff_mode` flag controls whether to return both pre and post state or just pre state. + /// + /// Assumes the db already contains the state from all prior transactions in the block. + pub fn trace_tx_prestate( + db: &mut GeneralizedDatabase, + block_header: &BlockHeader, + tx: &Transaction, + diff_mode: bool, + vm_type: VMType, + crypto: &dyn Crypto, + ) -> Result<(PrestateTrace, Option), EvmError> { + // Snapshot the current cache state before executing the tx. + // This is the pre-tx state for all accounts already loaded in the cache. + let pre_snapshot: CacheDB = db.current_accounts_state.clone(); + + // Execute the transaction (updates current_accounts_state in place) + let sender = tx + .sender(crypto) + .map_err(|e| EvmError::Transaction(format!("Couldn't recover sender: {e}")))?; + let env = Self::setup_env(tx, sender, block_header, db, vm_type)?; + let mut vm = VM::new(env, db, tx, LevmCallTracer::disabled(), vm_type, crypto)?; + vm.execute()?; + + // Build the pre and post state maps for all accounts that were touched + let pre_map = build_account_state_map(&pre_snapshot, &db.current_accounts_state, db, true); + let post_map = + build_account_state_map(&pre_snapshot, &db.current_accounts_state, db, false); + + if diff_mode { + Ok(( + PrestateTrace::new(), + Some(PrePostState { + pre: pre_map, + post: post_map, + }), + )) + } else { + Ok((pre_map, None)) + } + } + /// Run transaction with callTracer activated. pub fn trace_tx_calls( db: &mut GeneralizedDatabase, @@ -79,3 +126,91 @@ impl LEVM { Ok(vec![callframe]) } } + +/// Build a map of address -> `PrestateAccountState` for all accounts touched by a transaction. +/// +/// `pre_snapshot` is a snapshot of `current_accounts_state` taken BEFORE the tx executed. +/// `post_cache` is `current_accounts_state` AFTER the tx executed. +/// `db` is the database (used to look up code bytes by hash for new accounts). +/// `use_pre` controls whether to use the pre-tx state (true) or post-tx state (false). +/// +/// An account is "touched" if: +/// - It was newly loaded during this tx (present in `post_cache` but not in `pre_snapshot`) +/// - It was already cached and was modified (exists in both but differs) +fn build_account_state_map( + pre_snapshot: &CacheDB, + post_cache: &CacheDB, + db: &GeneralizedDatabase, + use_pre: bool, +) -> PrestateTrace { + let mut result = PrestateTrace::new(); + + for (addr, post_account) in post_cache { + let (touched, pre_account_opt) = match pre_snapshot.get(addr) { + None => { + // Account was first loaded during this tx. + // Pre-state comes from initial_accounts_state (the value loaded from DB before this tx). + let pre_in_initial = db.initial_accounts_state.get(addr); + // Consider touched only if the account changed (info or storage differ from initial). + let changed = pre_in_initial.is_none_or(|pre| { + pre.info != post_account.info || pre.storage != post_account.storage + }); + (changed, pre_in_initial) + } + Some(pre_account) => { + // Account was already in cache. Only include if something changed. + let changed = pre_account.info != post_account.info + || pre_account.storage != post_account.storage; + (changed, Some(pre_account)) + } + }; + + if !touched { + continue; + } + + let source_account = if use_pre { + match pre_account_opt { + Some(a) => a, + // If we can't find pre-state (shouldn't happen), skip this account + None => continue, + } + } else { + post_account + }; + + let address_hex = format!("0x{:x}", addr); + + // Look up code if account has non-empty code hash + let code = if source_account.info.code_hash != *EMPTY_KECCACK_HASH { + db.codes + .get(&source_account.info.code_hash) + .map(|c| format!("0x{}", hex::encode(&c.bytecode))) + } else { + None + }; + + // Convert storage slots to hex strings + let storage: std::collections::HashMap = source_account + .storage + .iter() + .filter(|(_, v)| !v.is_zero()) + .map(|(k, v)| { + let key_hex = format!("0x{:x}", k); + let val_hex = format!("0x{:x}", v); + (key_hex, val_hex) + }) + .collect(); + + let account_state = PrestateAccountState { + balance: format!("0x{:x}", source_account.info.balance), + nonce: source_account.info.nonce, + code, + storage, + }; + + result.insert(address_hex, account_state); + } + + result +} diff --git a/crates/vm/tracing.rs b/crates/vm/tracing.rs index dd9791fdcd3..c8f563e067c 100644 --- a/crates/vm/tracing.rs +++ b/crates/vm/tracing.rs @@ -1,5 +1,5 @@ use crate::backends::levm::LEVM; -use ethrex_common::tracing::CallTrace; +use ethrex_common::tracing::{CallTrace, PrePostState, PrestateTrace}; use ethrex_common::types::Block; use crate::{Evm, EvmError}; @@ -35,6 +35,33 @@ impl Evm { ) } + /// Executes a single tx and captures the pre/post account state (prestateTracer). + /// Assumes that the received state already contains changes from previous transactions. + /// Returns (pre_state, Some(PrePostState)) if diff_mode, or (pre_state, None) otherwise. + pub fn trace_tx_prestate( + &mut self, + block: &Block, + tx_index: usize, + diff_mode: bool, + ) -> Result<(PrestateTrace, Option), EvmError> { + let tx = block + .body + .transactions + .get(tx_index) + .ok_or(EvmError::Custom( + "Missing Transaction for Trace".to_string(), + ))?; + + LEVM::trace_tx_prestate( + &mut self.db, + &block.header, + tx, + diff_mode, + self.vm_type, + self.crypto.as_ref(), + ) + } + /// Reruns the given block, saving the changes on the state, doesn't output any results or receipts. /// If the optional argument `stop_index` is set, the run will stop just before executing the transaction at that index /// and won't process the withdrawals afterwards. diff --git a/docs/CLI.md b/docs/CLI.md index ecc185979cf..46fef4acb66 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -668,6 +668,36 @@ L2 options: [env: SPONSOR_PRIVATE_KEY=] [default: 0xffd790338a2798b648806fc8635ac7bf14af15425fed0c8f25bcc5febaa9b192] + --l2.ws-enabled + Enable WebSocket RPC server for L2. Required by the Circuit Breaker sidecar. + + [env: ETHREX_L2_WS_ENABLED=] + + --l2.ws-addr
+ Listening address for the L2 WebSocket RPC server. + + [env: ETHREX_L2_WS_ADDR=] + [default: 0.0.0.0] + + --l2.ws-port + Listening port for the L2 WebSocket RPC server. + + [env: ETHREX_L2_WS_PORT=] + [default: 1729] + +Circuit Breaker options: + --circuit-breaker-url + gRPC endpoint for the Credible Layer Assertion Enforcer sidecar. + When set, enables transaction validation against assertions during block building. + + [env: ETHREX_CIRCUIT_BREAKER_URL=] + + --circuit-breaker-aeges-url + gRPC endpoint for the Aeges mempool pre-filter service. + When set, transactions are validated before mempool admission. + + [env: ETHREX_CIRCUIT_BREAKER_AEGES_URL=] + Monitor options: --no-monitor [env: ETHREX_NO_MONITOR=] diff --git a/docs/l2/circuit_breaker.md b/docs/l2/circuit_breaker.md new file mode 100644 index 00000000000..0fb2561a873 --- /dev/null +++ b/docs/l2/circuit_breaker.md @@ -0,0 +1,484 @@ +# Circuit Breaker (Credible Layer Integration) + +## Overview + +Circuit Breaker integrates ethrex L2 with [Phylax Systems' Credible Layer](https://docs.phylax.systems/credible/credible-introduction) — a pre-execution security infrastructure that validates transactions against on-chain assertions before block inclusion. Transactions that violate an assertion are silently dropped before they land on-chain. + +### Architecture + +``` + ethrex L2 Sequencer + ┌─────────────────────────────────────────┐ + │ │ +User ──tx──▶ RPC ──┼──▶ Aeges check ──▶ Mempool │ + │ │ │ + │ ▼ │ + │ Block Producer: fill_transactions() │ + │ │ │ + │ │ For each tx: │ + │ ├─ Send Transaction ──────────────────────▶ ┌────────────────────┐ + │ ├─ Await verdict ◀──────────────────────│ Credible Layer │ + │ ├─ ASSERTION_FAILED? Skip tx │ Sidecar │ + │ └─ Otherwise: include tx │ (Assertion Enforcer)│ + │ │ │ + │ On block sealed: │ Runs PhEVM to │ + │ └─ Send CommitHead ────────────────────────│ evaluate assertions│ + │ │ │ + │ WebSocket server (:1730) │ Tracks chain state │ + │ └─ eth_subscribe("newHeads") ◀────────────│ via WS + traces │ + │ │ └────────────────────┘ + │ debug_traceBlockByNumber │ │ + │ └─ prestateTracer (diff mode) ◀───────────────────┘ + │ │ + └─────────────────────────────────────────┘ + │ + On-chain (L2) + ┌─────────────────────────────────────────┐ + │ State Oracle Assertion DA │ + │ (registry of (bytecode storage) │ + │ assertions) │ + └─────────────────────────────────────────┘ +``` + +### Key Concepts + +- **Assertion**: A Solidity contract that defines a security invariant (e.g., "ownership of contract X must not change"). Written using the [`credible-std`](https://github.com/phylaxsystems/credible-std) library. +- **Assertion Enforcer (Sidecar)**: A separate process that receives candidate transactions from the block builder, simulates them, runs applicable assertions through the PhEVM, and returns pass/fail verdicts. +- **State Oracle**: An on-chain contract that maps protected contracts to their assertions. +- **Aeges**: An optional mempool pre-filter that rejects transactions before they enter the pool. +- **Permissive on failure**: If the sidecar is unreachable or times out, transactions are included anyway. Liveness is prioritized over safety. + +### Communication Protocol + +ethrex communicates with the sidecar via **gRPC** using the protocol defined in [`sidecar.proto`](../../crates/l2/proto/sidecar.proto): + +| RPC | Type | Purpose | +|-----|------|---------| +| `StreamEvents` | Bidirectional stream | Send block lifecycle events (CommitHead, NewIteration, Transaction) | +| `SubscribeResults` | Server stream | Receive transaction verdicts as they complete | +| `GetTransaction` | Unary | Fallback polling for a single result | + +The Aeges pre-filter uses [`aeges.proto`](../../crates/l2/proto/aeges.proto): + +| RPC | Type | Purpose | +|-----|------|---------| +| `VerifyTransaction` | Unary | Check if a transaction should be admitted to the mempool | + +### Block Building Flow + +Per block: + +1. **CommitHead** → sidecar (previous block finalized, with block hash and tx count) +2. **NewIteration** → sidecar (new block env: number, timestamp, coinbase, gas limit, basefee) +3. For each candidate transaction: + - **Transaction** → sidecar (tx data: type, caller, gas, to, value, data, nonce, ...) + - ← **TransactionResult** (status: SUCCESS, ASSERTION_FAILED, etc.) + - If `ASSERTION_FAILED`: skip the transaction + - Otherwise: include it + +Privileged transactions (L1→L2 deposits) bypass Circuit Breaker entirely. + +--- + +## Implementation Details + +### Key Files + +| File | Role | +|------|------| +| `crates/l2/sequencer/circuit_breaker/mod.rs` | Module root, proto imports | +| `crates/l2/sequencer/circuit_breaker/client.rs` | `CircuitBreakerClient` — gRPC client for sidecar | +| `crates/l2/sequencer/circuit_breaker/aeges.rs` | `AegesClient` — gRPC client for Aeges | +| `crates/l2/sequencer/circuit_breaker/errors.rs` | Error types | +| `crates/l2/proto/sidecar.proto` | Sidecar gRPC protocol definition | +| `crates/l2/proto/aeges.proto` | Aeges gRPC protocol definition | +| `crates/l2/sequencer/block_producer.rs` | Block producer — sends CommitHead + NewIteration | +| `crates/l2/sequencer/block_producer/payload_builder.rs` | Transaction selection — sends Transaction events | +| `crates/l2/networking/rpc/rpc.rs` | L2 RPC — WebSocket server + Aeges mempool filter | +| `crates/networking/rpc/tracing.rs` | prestateTracer implementation | + +### Configuration + +CLI flags: + +| Flag | Description | Default | +|------|-------------|---------| +| `--circuit-breaker-url` | gRPC endpoint for the sidecar | (disabled) | +| `--circuit-breaker-aeges-url` | gRPC endpoint for Aeges pre-filter | (disabled) | +| `--circuit-breaker-state-oracle` | Address of the deployed State Oracle contract on L2 | (none) | + +When `--circuit-breaker-url` is not set, Circuit Breaker is completely disabled with zero overhead. + +### Sidecar Requirements + +The sidecar connects to ethrex L2 and needs: + +| Endpoint | What it does | +|----------|--------------| +| HTTP RPC (`:1729`) | `eth_blockNumber`, `eth_getBlockByNumber`, `debug_traceBlockByNumber` | +| WebSocket (`:1730`) | `eth_subscribe("newHeads")` for live block tracking | + +The `debug_traceBlockByNumber` with `prestateTracer` in diff mode is particularly important — the sidecar uses it to build its local state database. + +--- + +## How to Run (End-to-End) + +This is a step-by-step guide to run the full Circuit Breaker stack locally, deploy an assertion, and verify that violating transactions are dropped. All steps have been tested and verified. + +### Prerequisites + +- [Foundry](https://book.getfoundry.sh/getting-started/installation) (`forge`, `cast`) +- Docker (running) +- Node.js >= 22 and `pnpm` (for the assertion indexer) +- ethrex built: `cargo build --release -p ethrex --features l2` + +### Step 1: Start ethrex L1 + +```bash +cd crates/l2 +rm -rf dev_ethrex_l1 dev_ethrex_l2 + +../../target/release/ethrex \ + --network ../../fixtures/genesis/l1.json \ + --http.port 8545 --http.addr 0.0.0.0 \ + --authrpc.port 8551 --dev \ + --datadir dev_ethrex_l1 & + +# Verify +sleep 5 +cast block-number --rpc-url http://localhost:8545 +``` + +### Step 2: Deploy L2 contracts on L1 + +```bash +COMPILE_CONTRACTS=true make deploy-l1 +``` + +### Step 3: Start ethrex L2 with Circuit Breaker + +```bash +export $(cat ../../cmd/.env | xargs) + +RUST_LOG=info,ethrex_l2=debug ../../target/release/ethrex l2 \ + --no-monitor \ + --watcher.block-delay 0 \ + --network ../../fixtures/genesis/l2.json \ + --http.port 1729 --http.addr 0.0.0.0 \ + --datadir dev_ethrex_l2 \ + --l1.bridge-address $ETHREX_WATCHER_BRIDGE_ADDRESS \ + --l1.on-chain-proposer-address $ETHREX_COMMITTER_ON_CHAIN_PROPOSER_ADDRESS \ + --eth.rpc-url http://localhost:8545 \ + --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d \ + --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 \ + --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d \ + --circuit-breaker-url http://localhost:50051 \ + --l2.ws-enabled --l2.ws-port 1730 & + +# Verify (wait ~10s for L2 to start) +cast block-number --rpc-url http://localhost:1729 +``` + +The L2 will log `StreamEvents connect failed, retrying in 5s` until the sidecar starts. This is expected — the L2 is permissive and keeps producing blocks. + +### Step 4: Deploy the State Oracle on L2 + +The DA prover address must match the private key used by assertion-da (`0xdd7e619d...` → `0xb0d60c09103F4a5c04EE8537A22ECD6a34382B36`). + +```bash +git clone https://github.com/phylaxsystems/credible-layer-contracts /tmp/credible-layer-contracts +cd /tmp/credible-layer-contracts +forge install + +PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 + +STATE_ORACLE_MAX_ASSERTIONS_PER_AA=100 \ +STATE_ORACLE_ASSERTION_TIMELOCK_BLOCKS=1 \ +STATE_ORACLE_ADMIN_ADDRESS=$(cast wallet address $PK) \ +DA_PROVER_ADDRESS=0xb0d60c09103F4a5c04EE8537A22ECD6a34382B36 \ +DEPLOY_ADMIN_VERIFIER_OWNER=true \ +DEPLOY_ADMIN_VERIFIER_WHITELIST=false \ +ADMIN_VERIFIER_WHITELIST_ADMIN_ADDRESS=0x0000000000000000000000000000000000000001 \ +forge script script/DeployCore.s.sol:DeployCore \ + --rpc-url http://localhost:1729 \ + --private-key $PK \ + --broadcast +``` + +Note the addresses from the output (these are deterministic with CREATE2): + +| Contract | Address | +|----------|---------| +| DA Verifier (ECDSA) | `0x422A3492e218383753D8006C7Bfa97815B44373F` | +| Admin Verifier (Owner) | `0x9f9F5Fd89ad648f2C000C954d8d9C87743243eC5` | +| **State Oracle Proxy** | `0x72ae2643518179cF01bcA3278a37ceAD408DE8b2` | + +### Step 5: Start assertion DA + +The assertion DA stores assertion bytecode. It must be running before uploading assertions (step 6) and before the sidecar starts (step 7). + +```bash +docker run -d --name assertion-da -p 5001:5001 \ + -e DB_PATH=/data/assertions \ + -e DA_LISTEN_ADDR=0.0.0.0:5001 \ + -e DA_CACHE_SIZE=1000000 \ + -e DA_PRIVATE_KEY=0xdd7e619d26d7eef795e3b5144307204f2f5d7d08298d04b926e874c6d9d43e75 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ghcr.io/phylaxsystems/credible-sdk/assertion-da-dev:sha-01b3374 +``` + +### Step 6: Deploy OwnableTarget test contract + +```bash +cd /crates/l2/contracts/src/circuit_breaker +solc --bin OwnableTarget.sol -o /tmp/cb_compiled --overwrite + +PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 +BYTECODE=0x$(cat /tmp/cb_compiled/OwnableTarget.bin) + +cast send --private-key $PK --rpc-url http://localhost:1729 --create "$BYTECODE" +``` + +Note the `contractAddress` from the output. Example: `0x00c042c4d5d913277ce16611a2ce6e9003554ad5` + +### Step 7: Upload assertion to DA and register on State Oracle + +Build the assertion using the [credible-layer-starter](https://github.com/phylaxsystems/credible-layer-starter) repo: + +```bash +git clone --recurse-submodules https://github.com/phylaxsystems/credible-layer-starter /tmp/credible-layer-starter +cd /tmp/credible-layer-starter +forge build # or: pcl build +``` + +Submit the assertion's **creation bytecode** to the DA server: + +```bash +CREATION_BYTECODE=$(cat out/OwnableAssertion.a.sol/OwnableAssertion.json | \ + python3 -c "import sys,json; print(json.load(sys.stdin)['bytecode']['object'])") + +curl -s http://localhost:5001 -X POST -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"method\":\"da_submit_assertion\",\"params\":[\"$CREATION_BYTECODE\"],\"id\":1}" +``` + +Note the `id` from the response — that's the **assertion ID**. Then get the prover signature: + +```bash +ASSERTION_ID= + +DA_SIG=$(curl -s http://localhost:5001 -X POST -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"method\":\"da_get_assertion\",\"params\":[\"$ASSERTION_ID\"],\"id\":1}" | \ + python3 -c "import sys,json; print(json.load(sys.stdin)['result']['prover_signature'])") +``` + +Now register the assertion on the State Oracle: + +```bash +PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 +STATE_ORACLE=0x72ae2643518179cF01bcA3278a37ceAD408DE8b2 +OWNABLE_TARGET=
+ADMIN_VERIFIER=0x9f9F5Fd89ad648f2C000C954d8d9C87743243eC5 +DA_VERIFIER=0x422A3492e218383753D8006C7Bfa97815B44373F + +# Disable whitelist (required for devnet) +cast send $STATE_ORACLE "disableWhitelist()" \ + --private-key $PK --rpc-url http://localhost:1729 + +# Register the target contract as an assertion adopter +cast send $STATE_ORACLE "registerAssertionAdopter(address,address,bytes)" \ + $OWNABLE_TARGET $ADMIN_VERIFIER "0x" \ + --private-key $PK --rpc-url http://localhost:1729 + +# Add the assertion +cast send $STATE_ORACLE "addAssertion(address,bytes32,address,bytes,bytes)" \ + $OWNABLE_TARGET $ASSERTION_ID $DA_VERIFIER "0x" $DA_SIG \ + --private-key $PK --rpc-url http://localhost:1729 + +# Verify +cast call $STATE_ORACLE "hasAssertion(address,bytes32)(bool)" \ + $OWNABLE_TARGET $ASSERTION_ID --rpc-url http://localhost:1729 +# Should return: true +``` + +### Step 8: Start the assertion indexer and sidecar + +The sidecar discovers assertions through a GraphQL indexer that watches State Oracle events. + +Clone and start the [sidecar-indexer](https://github.com/phylaxsystems/sidecar-indexer): + +```bash +git clone https://github.com/phylaxsystems/sidecar-indexer /tmp/sidecar-indexer +cd /tmp/sidecar-indexer + +cat > .env << EOF +RPC_ENDPOINT=http://host.docker.internal:1729 +STATE_ORACLE_ADDRESS=0x72ae2643518179cF01bcA3278a37ceAD408DE8b2 +STATE_ORACLE_DEPLOYMENT_BLOCK=0 +FINALITY_CONFIRMATION=1 +GRAPHQL_SERVER_PORT=4350 +DB_HOST=db +DB_PORT=5432 +DB_NAME=squid +DB_USER=postgres +DB_PASS=postgres +EOF + +docker compose -f infra/local/docker-compose.yaml up --build -d +``` + +Wait ~15 seconds, then verify the indexer found the assertion: + +```bash +curl -s http://localhost:4350/graphql -X POST -H "Content-Type: application/json" \ + -d '{"query":"{ assertionAddeds { totalCount nodes { assertionId assertionAdopter } } }"}' +# Should show totalCount: 1 +``` + +Create `sidecar-config.json`: + +```json +{ + "chain": { "spec_id": "CANCUN", "chain_id": 65536999 }, + "credible": { + "assertion_gas_limit": 3000000, + "cache_capacity_bytes": 256000000, + "flush_every_ms": 5000, + "assertion_da_rpc_url": "http://host.docker.internal:5001", + "event_source_url": "http://host.docker.internal:4350/graphql", + "poll_interval": 1000, + "assertion_store_db_path": "/tmp/sidecar/assertion_store_database", + "transaction_observer_db_path": "/tmp/sidecar/transaction_observer_database", + "transaction_observer_endpoint": null, + "transaction_observer_auth_token": "", + "state_oracle": "0x72ae2643518179cF01bcA3278a37ceAD408DE8b2", + "state_oracle_deployment_block": 0, + "transaction_results_max_capacity": 1000, + "accepted_txs_ttl_ms": 600000, + "assertion_store_prune_config_interval_ms": 60000, + "assertion_store_prune_config_retention_blocks": 0 + }, + "transport": { "bind_addr": "0.0.0.0:50051", "health_bind_addr": "0.0.0.0:9547" }, + "state": { + "sources": [{ + "type": "eth-rpc", + "ws_url": "ws://host.docker.internal:1730", + "http_url": "http://host.docker.internal:1729" + }], + "minimum_state_diff": 100, + "sources_sync_timeout_ms": 1000, + "sources_monitoring_period_ms": 500 + } +} +``` + +**Important:** `chain_id` must match your L2 genesis chain ID (`65536999` for the default ethrex L2 genesis). + +Pull the sidecar Docker image and start it: + +```bash +docker pull ghcr.io/phylaxsystems/credible-sdk/sidecar:main + +docker run -d --name credible-sidecar \ + -p 50051:50051 -p 9547:9547 \ + -e RUST_LOG=info,sidecar::engine=debug \ + -v $(pwd)/sidecar-config.json:/etc/credible-sidecar/config.json:ro \ + ghcr.io/phylaxsystems/credible-sdk/sidecar:main \ + --config-file-path /etc/credible-sidecar/config.json +``` + +Wait ~15 seconds, then verify the sidecar is healthy and loaded the assertion: + +```bash +curl http://localhost:9547/health # Should return "OK" + +# Check logs for assertion loading (wait ~15s after start) +docker logs credible-sidecar 2>&1 | grep "trigger_recorder" +# Should show: triggers: {Call { trigger_selector: 0xf2fde38b }: {0x7ab4397a}} +# This means: transferOwnership(address) calls will trigger the assertion +``` + +### Step 9: Test — violating transaction is DROPPED + +Send a `transferOwnership` call. The sidecar detects the assertion violation and ethrex drops the transaction: + +```bash +PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 +OWNABLE_TARGET=
+ +# Check current owner +cast call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 + +# Try to transfer ownership (this should time out — tx never included!) +timeout 25 cast send $OWNABLE_TARGET "transferOwnership(address)" \ + 0x0000000000000000000000000000000000000001 \ + --private-key $PK --rpc-url http://localhost:1729 +# Expected: times out with no output (tx was dropped by Circuit Breaker) + +# Verify owner is UNCHANGED +cast call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 +# Should still be the original deployer address +``` + +Check sidecar logs to confirm the assertion caught it: + +```bash +docker logs credible-sidecar 2>&1 | grep "is_valid=false" +# Should show: Transaction processed ... is_valid=false +docker logs credible-sidecar 2>&1 | grep "assertion_failure_count" +# Should show: Transaction failed assertion validation ... failed_assertions=[...] +``` + +### Step 10: Test — valid transaction is INCLUDED + +Send a non-protected call (`doSomething()`). The sidecar allows it through: + +```bash +cast send $OWNABLE_TARGET "doSomething()" \ + --private-key $PK --rpc-url http://localhost:1729 +# Expected: status 1 (success), included in a block +``` + +Check sidecar logs: + +```bash +docker logs credible-sidecar 2>&1 | grep "is_valid=true" +# Should show: Transaction processed ... is_valid=true +``` + +### Cleanup + +Stop all services and remove containers and data: + +```bash +# Stop ethrex L1 and L2 +pkill -f ethrex + +# Remove Docker containers +docker rm -f credible-sidecar assertion-da + +# Stop and remove indexer (PostgreSQL + indexer + API) +cd /tmp/sidecar-indexer && docker compose -f infra/local/docker-compose.yaml down -v + +# Remove ethrex data directories +cd /crates/l2 +rm -rf dev_ethrex_l1 dev_ethrex_l2 + +# (Optional) Remove cloned repos +rm -rf /tmp/credible-layer-contracts /tmp/credible-layer-starter /tmp/sidecar-indexer /tmp/cb_compiled +``` + +--- + +## References + +- [Credible Layer Introduction](https://docs.phylax.systems/credible/credible-introduction) +- [Architecture Overview](https://docs.phylax.systems/credible/architecture-overview) +- [Assertion Enforcer](https://docs.phylax.systems/credible/assertion-enforcer) +- [Network Integration](https://docs.phylax.systems/credible/network-integration) +- [Linea/Besu Integration](https://docs.phylax.systems/credible/network-integrations/architecture-linea) +- [credible-std Library](https://github.com/phylaxsystems/credible-std) +- [Besu Plugin Reference](https://github.com/phylaxsystems/credible-layer-besu-plugin) +- [credible-sdk (sidecar source)](https://github.com/phylaxsystems/credible-sdk) +- [sidecar.proto](../../crates/l2/proto/sidecar.proto) +- [aeges.proto](../../crates/l2/proto/aeges.proto) From 2df484670e1232b1421e7528c2792560d27ed920 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 18:01:33 -0300 Subject: [PATCH 02/33] Rename Circuit Breaker to Credible Layer for consistency with Phylax naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Circuit Breaker was an informal name. Phylax's official product name is Credible Layer — used consistently across all their documentation, code, and repos. Rename all references: module directories, struct names, CLI flags, env vars, docs, and Makefile targets. --- cmd/ethrex/l2/initializers.rs | 15 ++++++- cmd/ethrex/l2/options.rs | 44 +++++++++---------- crates/l2/Cargo.toml | 2 +- crates/l2/Makefile | 16 +++---- crates/l2/build.rs | 2 +- .../OwnableTarget.sol | 2 +- .../README.md | 6 +-- .../TestOwnershipAssertion.sol | 2 +- crates/l2/l2.rs | 2 +- crates/l2/networking/rpc/rpc.rs | 5 ++- ...t_breaker_e2e.sh => credible_layer_e2e.sh} | 16 +++---- crates/l2/sequencer/block_producer.rs | 40 ++++++++--------- .../block_producer/payload_builder.rs | 18 ++++---- crates/l2/sequencer/configs.rs | 6 +-- .../aeges.rs | 8 ++-- .../client.rs | 40 ++++++++--------- .../errors.rs | 4 +- .../mock_sidecar/main.rs | 2 +- .../mod.rs | 8 ++-- crates/l2/sequencer/mod.rs | 2 +- docs/CLI.md | 12 ++--- .../{circuit_breaker.md => credible_layer.md} | 32 +++++++------- 22 files changed, 149 insertions(+), 135 deletions(-) rename crates/l2/contracts/src/{circuit_breaker => credible_layer}/OwnableTarget.sol (92%) rename crates/l2/contracts/src/{circuit_breaker => credible_layer}/README.md (90%) rename crates/l2/contracts/src/{circuit_breaker => credible_layer}/TestOwnershipAssertion.sol (96%) rename crates/l2/scripts/{circuit_breaker_e2e.sh => credible_layer_e2e.sh} (92%) rename crates/l2/sequencer/{circuit_breaker => credible_layer}/aeges.rs (96%) rename crates/l2/sequencer/{circuit_breaker => credible_layer}/client.rs (93%) rename crates/l2/sequencer/{circuit_breaker => credible_layer}/errors.rs (83%) rename crates/l2/sequencer/{circuit_breaker => credible_layer}/mock_sidecar/main.rs (99%) rename crates/l2/sequencer/{circuit_breaker => credible_layer}/mod.rs (73%) rename docs/l2/{circuit_breaker.md => credible_layer.md} (92%) diff --git a/cmd/ethrex/l2/initializers.rs b/cmd/ethrex/l2/initializers.rs index 366c313187b..73d88380098 100644 --- a/cmd/ethrex/l2/initializers.rs +++ b/cmd/ethrex/l2/initializers.rs @@ -53,6 +53,7 @@ fn init_rpc_api( l2_gas_limit: u64, ws_addr: Option, new_heads_sender: Option>, + mempool_filter: Option, ) { init_datadir(&opts.datadir); @@ -75,6 +76,7 @@ fn init_rpc_api( l2_gas_limit, l2_opts.sponsored_gas_limit, new_heads_sender, + mempool_filter, ); tracker.spawn(rpc_api); @@ -111,8 +113,8 @@ fn get_valid_delegation_addresses(l2_opts: &L2Options) -> Vec
{ async fn build_aeges_filter(aeges_url: Option) -> Option { use ethrex_common::types::Transaction as EthrexTx; use ethrex_crypto::NativeCrypto; - use ethrex_l2::sequencer::circuit_breaker::{AegesClient, aeges::AegesConfig}; - use ethrex_l2::sequencer::circuit_breaker::aeges_proto::{ + use ethrex_l2::sequencer::credible_layer::{AegesClient, aeges::AegesConfig}; + use ethrex_l2::sequencer::credible_layer::aeges_proto::{ AccessListEntry, Transaction as AegesTransaction, }; use ethrex_rlp::decode::RLPDecode; @@ -439,6 +441,14 @@ pub async fn init_l2( (None, None) }; + // Build the Aeges mempool pre-filter if configured. + let mempool_filter = build_aeges_filter( + opts.sequencer_opts + .credible_layer_opts + .credible_layer_aeges_url + .clone(), + ) + .await; init_rpc_api( &opts.node_opts, &opts, @@ -454,6 +464,7 @@ pub async fn init_l2( l2_gas_limit, ws_addr, new_heads_sender, + mempool_filter, ); // Initialize metrics if enabled diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index 54dea378dee..d638e25d22e 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -8,7 +8,7 @@ use ethrex_l2::sequencer::utils::resolve_aligned_network; use ethrex_l2::{ BasedConfig, BlockFetcherConfig, BlockProducerConfig, CommitterConfig, EthConfig, L1WatcherConfig, ProofCoordinatorConfig, SequencerConfig, StateUpdaterConfig, - sequencer::configs::{AdminConfig, AlignedConfig, CircuitBreakerConfig, MonitorConfig}, + sequencer::configs::{AdminConfig, AlignedConfig, CredibleLayerConfig, MonitorConfig}, }; use ethrex_l2_prover::{backend::BackendType, config::ProverConfig}; use ethrex_l2_rpc::signer::{LocalSigner, RemoteSigner, Signer}; @@ -124,7 +124,7 @@ pub struct SequencerOptions { #[clap(flatten)] pub state_updater_opts: StateUpdaterOptions, #[clap(flatten)] - pub circuit_breaker_opts: CircuitBreakerOptions, + pub credible_layer_opts: CredibleLayerOptions, #[arg( long = "validium", default_value = "false", @@ -296,10 +296,10 @@ impl TryFrom for SequencerConfig { start_at: opts.state_updater_opts.start_at, l2_head_check_rpc_url: opts.state_updater_opts.l2_head_check_rpc_url, }, - circuit_breaker: CircuitBreakerConfig { - sidecar_url: opts.circuit_breaker_opts.circuit_breaker_url, - aeges_url: opts.circuit_breaker_opts.circuit_breaker_aeges_url, - state_oracle_address: opts.circuit_breaker_opts.circuit_breaker_state_oracle, + credible_layer: CredibleLayerConfig { + sidecar_url: opts.credible_layer_opts.credible_layer_url, + aeges_url: opts.credible_layer_opts.credible_layer_aeges_url, + state_oracle_address: opts.credible_layer_opts.credible_layer_state_oracle, }, }) } @@ -336,7 +336,7 @@ impl SequencerOptions { self.state_updater_opts .populate_with_defaults(&defaults.state_updater_opts); // admin_opts contains only non-optional fields. - // circuit_breaker_opts contains only optional fields, nothing to populate. + // credible_layer_opts contains only optional fields, nothing to populate. } } @@ -1106,31 +1106,31 @@ impl Default for AdminOptions { } #[derive(Parser, Default, Debug)] -pub struct CircuitBreakerOptions { +pub struct CredibleLayerOptions { #[arg( - long = "circuit-breaker-url", + long = "credible-layer-url", value_name = "URL", - env = "ETHREX_CIRCUIT_BREAKER_URL", - help = "gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). When set, the circuit breaker integration is enabled.", - help_heading = "Circuit Breaker options" + env = "ETHREX_CREDIBLE_LAYER_URL", + help = "gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). When set, the credible layer integration is enabled.", + help_heading = "Credible Layer options" )] - pub circuit_breaker_url: Option, + pub credible_layer_url: Option, #[arg( - long = "circuit-breaker-aeges-url", + long = "credible-layer-aeges-url", value_name = "URL", - env = "ETHREX_CIRCUIT_BREAKER_AEGES_URL", + env = "ETHREX_CREDIBLE_LAYER_AEGES_URL", help = "gRPC endpoint for the Aeges mempool pre-filter service (e.g. http://localhost:8080). When set, Aeges pre-filtering is enabled.", - help_heading = "Circuit Breaker options" + help_heading = "Credible Layer options" )] - pub circuit_breaker_aeges_url: Option, + pub credible_layer_aeges_url: Option, #[arg( - long = "circuit-breaker-state-oracle", + long = "credible-layer-state-oracle", value_name = "ADDRESS", - env = "ETHREX_CIRCUIT_BREAKER_STATE_ORACLE", - help = "Address of the already-deployed State Oracle contract on L2. The State Oracle maps protected contracts to their active assertions and is required by the Credible Layer sidecar. Deploy it separately using the Phylax toolchain (see crates/l2/contracts/src/circuit_breaker/README.md).", - help_heading = "Circuit Breaker options" + env = "ETHREX_CREDIBLE_LAYER_STATE_ORACLE", + help = "Address of the already-deployed State Oracle contract on L2. The State Oracle maps protected contracts to their active assertions and is required by the Credible Layer sidecar. Deploy it separately using the Phylax toolchain (see crates/l2/contracts/src/credible_layer/README.md).", + help_heading = "Credible Layer options" )] - pub circuit_breaker_state_oracle: Option
, + pub credible_layer_state_oracle: Option
, } #[derive(Parser)] diff --git a/crates/l2/Cargo.toml b/crates/l2/Cargo.toml index 40f022befcf..623302f5128 100644 --- a/crates/l2/Cargo.toml +++ b/crates/l2/Cargo.toml @@ -72,7 +72,7 @@ path = "./l2.rs" [[bin]] name = "mock-sidecar" -path = "sequencer/circuit_breaker/mock_sidecar/main.rs" +path = "sequencer/credible_layer/mock_sidecar/main.rs" [lints.clippy] unwrap_used = "deny" diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 596ba8d93a4..8ddf9e00d4f 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -282,11 +282,11 @@ state-diff-test: validate-blobs: cargo test -p ethrex-test validate_blobs_match_genesis --release -# Circuit Breaker (Credible Layer) -# See docs/l2/circuit_breaker.md for full documentation. +# Credible Layer (Credible Layer) +# See docs/l2/credible_layer.md for full documentation. -init-circuit-breaker: ## 🛡️ Start the Circuit Breaker sidecar stack (assertion-da + sidecar) - @echo "Starting Circuit Breaker sidecar stack..." +init-credible-layer: ## 🛡️ Start the Credible Layer sidecar stack (assertion-da + sidecar) + @echo "Starting Credible Layer sidecar stack..." @echo "Make sure to set STATE_ORACLE_ADDRESS in sidecar-config.template.json" docker run -d --name assertion-da -p 5001:5001 \ -e DB_PATH=/data/assertions \ @@ -296,15 +296,15 @@ init-circuit-breaker: ## 🛡️ Start the Circuit Breaker sidecar stack (assert @echo "Assertion DA running on :5001" @echo "Start the sidecar manually: ./sidecar --config-file-path ./sidecar-config.template.json" -down-circuit-breaker: ## 🛑 Stop the Circuit Breaker sidecar stack +down-credible-layer: ## 🛑 Stop the Credible Layer sidecar stack docker stop assertion-da 2>/dev/null || true docker rm assertion-da 2>/dev/null || true -init-l2-circuit-breaker: ## 🛡️ Start L2 with Circuit Breaker enabled +init-l2-credible-layer: ## 🛡️ Start L2 with Credible Layer enabled cargo run --release --features l2 --manifest-path ../../Cargo.toml -- \ l2 \ - --circuit-breaker-url http://localhost:50051 \ - --circuit-breaker-aeges-url http://localhost:8080 \ + --credible-layer-url http://localhost:50051 \ + --credible-layer-aeges-url http://localhost:8080 \ --l2.ws-enabled \ --l2.ws-port 1730 \ 2>&1 | tee /tmp/l2_cb.log diff --git a/crates/l2/build.rs b/crates/l2/build.rs index 2367bc515b9..9e5159c4f2c 100644 --- a/crates/l2/build.rs +++ b/crates/l2/build.rs @@ -14,7 +14,7 @@ fn main() -> Result<(), Box> { Emitter::default().add_instructions(&git2)?.emit()?; } - // Compile Circuit Breaker protobuf definitions (client + server for mock sidecar) + // Compile Credible Layer protobuf definitions (client + server for mock sidecar) tonic_build::configure() .compile_protos( &["proto/sidecar.proto", "proto/aeges.proto"], diff --git a/crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol b/crates/l2/contracts/src/credible_layer/OwnableTarget.sol similarity index 92% rename from crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol rename to crates/l2/contracts/src/credible_layer/OwnableTarget.sol index c23f13f525c..bcdcfd94118 100644 --- a/crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol +++ b/crates/l2/contracts/src/credible_layer/OwnableTarget.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.27; /// @title OwnableTarget -/// @notice A simple Ownable contract used as a target for Circuit Breaker testing. +/// @notice A simple Ownable contract used as a target for Credible Layer testing. /// The TestOwnershipAssertion protects this contract by preventing ownership transfers. contract OwnableTarget { address public owner; diff --git a/crates/l2/contracts/src/circuit_breaker/README.md b/crates/l2/contracts/src/credible_layer/README.md similarity index 90% rename from crates/l2/contracts/src/circuit_breaker/README.md rename to crates/l2/contracts/src/credible_layer/README.md index 244bd657fd3..20af60587f2 100644 --- a/crates/l2/contracts/src/circuit_breaker/README.md +++ b/crates/l2/contracts/src/credible_layer/README.md @@ -1,6 +1,6 @@ -# Circuit Breaker Contracts +# Credible Layer Contracts -This directory contains example/demo contracts for the Circuit Breaker integration: +This directory contains example/demo contracts for the Credible Layer integration: - `OwnableTarget.sol` — a simple Ownable contract used as a demonstration target - `TestOwnershipAssertion.sol` — a test assertion that asserts ownership of `OwnableTarget` cannot change @@ -60,7 +60,7 @@ forge script script/DeployStateOracle.s.sol --rpc-url --broadcast ``` Once deployed, note the State Oracle address and pass it to ethrex via the -`--circuit-breaker-state-oracle` flag (see the [Circuit Breaker docs](../../../../docs/l2/circuit_breaker.md)). +`--credible-layer-state-oracle` flag (see the [Credible Layer docs](../../../../docs/l2/credible_layer.md)). ### References diff --git a/crates/l2/contracts/src/circuit_breaker/TestOwnershipAssertion.sol b/crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol similarity index 96% rename from crates/l2/contracts/src/circuit_breaker/TestOwnershipAssertion.sol rename to crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol index 4697bf38917..cce9a416c64 100644 --- a/crates/l2/contracts/src/circuit_breaker/TestOwnershipAssertion.sol +++ b/crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.27; /// @title TestOwnershipAssertion -/// @notice A trivial assertion for Circuit Breaker end-to-end testing. +/// @notice A trivial assertion for Credible Layer end-to-end testing. /// Protects OwnableTarget by asserting that ownership cannot change. /// /// This contract uses the credible-std library interfaces. diff --git a/crates/l2/l2.rs b/crates/l2/l2.rs index 1ec76869746..8925d8c534f 100644 --- a/crates/l2/l2.rs +++ b/crates/l2/l2.rs @@ -6,7 +6,7 @@ pub mod utils; pub use based::block_fetcher::BlockFetcher; pub use sequencer::configs::{ - BasedConfig, BlockFetcherConfig, BlockProducerConfig, CircuitBreakerConfig, CommitterConfig, + BasedConfig, BlockFetcherConfig, BlockProducerConfig, CredibleLayerConfig, CommitterConfig, EthConfig, L1WatcherConfig, ProofCoordinatorConfig, SequencerConfig, StateUpdaterConfig, }; pub use sequencer::start_l2; diff --git a/crates/l2/networking/rpc/rpc.rs b/crates/l2/networking/rpc/rpc.rs index ade070661ba..d6debe4f75c 100644 --- a/crates/l2/networking/rpc/rpc.rs +++ b/crates/l2/networking/rpc/rpc.rs @@ -124,6 +124,9 @@ pub async fn start_api( // here. The same sender clone should be given to the block producer so it // can publish headers after sealing each block. new_heads_sender: Option>, + // Optional mempool pre-filter (Aeges integration). When `Some`, every + // `eth_sendRawTransaction` call is checked before mempool admission. + mempool_filter: Option, ) -> Result<(), RpcErr> { // TODO: Refactor how filters are handled, // filters are used by the filters endpoints (eth_newFilter, eth_getFilterChanges, ...etc) @@ -160,7 +163,7 @@ pub async fn start_api( rollup_store, sponsored_gas_limit, new_heads_sender, - mempool_filter: None, + mempool_filter, }; // Periodically clean up the active filters for the filters endpoints. diff --git a/crates/l2/scripts/circuit_breaker_e2e.sh b/crates/l2/scripts/credible_layer_e2e.sh similarity index 92% rename from crates/l2/scripts/circuit_breaker_e2e.sh rename to crates/l2/scripts/credible_layer_e2e.sh index 91806a2c611..3427429947d 100755 --- a/crates/l2/scripts/circuit_breaker_e2e.sh +++ b/crates/l2/scripts/credible_layer_e2e.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# Circuit Breaker End-to-End Validation Script +# Credible Layer End-to-End Validation Script # -# This script validates the full Circuit Breaker integration: -# 1. Checks that ethrex L2 is running with Circuit Breaker enabled +# This script validates the full Credible Layer integration: +# 1. Checks that ethrex L2 is running with Credible Layer enabled # 2. Checks that the sidecar is running and healthy # 3. Deploys a test target contract (OwnableTarget) # 4. (Manual step) Deploys and registers a test assertion via pcl @@ -10,13 +10,13 @@ # 6. Sends a valid transaction → verifies it IS included # # Prerequisites: -# - ethrex L2 running with --circuit-breaker-url (see: make init-l2-circuit-breaker) -# - Credible Layer sidecar running (see: make init-circuit-breaker) +# - ethrex L2 running with --credible-layer-url (see: make init-l2-credible-layer) +# - Credible Layer sidecar running (see: make init-credible-layer) # - cast (from foundry) installed # - A funded account on L2 # # Usage: -# ./circuit_breaker_e2e.sh [L2_RPC_URL] [SIDECAR_HEALTH_URL] +# ./credible_layer_e2e.sh [L2_RPC_URL] [SIDECAR_HEALTH_URL] set -euo pipefail @@ -58,7 +58,7 @@ fi info "Deploying OwnableTarget contract..." -# OwnableTarget bytecode (compiled from contracts/src/circuit_breaker/OwnableTarget.sol) +# OwnableTarget bytecode (compiled from contracts/src/credible_layer/OwnableTarget.sol) # If you need to recompile: solc --bin OwnableTarget.sol # For now, we attempt to deploy using cast OWNABLE_TARGET_DEPLOY=$(cast send --create \ @@ -79,7 +79,7 @@ if [ -n "$OWNABLE_TARGET_DEPLOY" ]; then fi else info "Could not deploy OwnableTarget. You may need to deploy it manually." - info "Compile with: solc --bin crates/l2/contracts/src/circuit_breaker/OwnableTarget.sol" + info "Compile with: solc --bin crates/l2/contracts/src/credible_layer/OwnableTarget.sol" fi # ─── Step 4: Assertion registration (manual) ────────────────────────────────── diff --git a/crates/l2/sequencer/block_producer.rs b/crates/l2/sequencer/block_producer.rs index 5bc9ca7767a..ef6b39e953b 100644 --- a/crates/l2/sequencer/block_producer.rs +++ b/crates/l2/sequencer/block_producer.rs @@ -39,9 +39,9 @@ use serde_json::Value; use std::str::FromStr; use tokio::sync::broadcast; -use super::circuit_breaker::{ - CircuitBreakerClient, - client::CircuitBreakerConfig as ClientCircuitBreakerConfig, +use super::credible_layer::{ + CredibleLayerClient, + client::CredibleLayerConfig as ClientCredibleLayerConfig, sidecar_proto::{BlobExcessGasAndPrice, BlockEnv, CommitHead, NewIteration}, }; use super::errors::BlockProducerError; @@ -70,7 +70,7 @@ pub struct BlockProducer { block_gas_limit: u64, eth_client: EthClient, router_address: Address, - circuit_breaker: Option>, + credible_layer: Option>, /// Broadcast sender for new block header notifications to WS subscribers. new_heads_sender: Option>, } @@ -94,7 +94,7 @@ impl BlockProducer { sequencer_state: SequencerState, router_address: Address, l2_gas_limit: u64, - circuit_breaker_url: Option, + credible_layer_url: Option, new_heads_sender: Option>, ) -> Result { let BlockProducerConfig { @@ -121,18 +121,18 @@ impl BlockProducer { ); } - let circuit_breaker = if let Some(url) = circuit_breaker_url { - let cb_config = ClientCircuitBreakerConfig { + let credible_layer = if let Some(url) = credible_layer_url { + let cb_config = ClientCredibleLayerConfig { sidecar_url: url, ..Default::default() }; - match CircuitBreakerClient::connect(cb_config).await { + match CredibleLayerClient::connect(cb_config).await { Ok(client) => { - info!("Circuit Breaker sidecar connected"); + info!("Credible Layer sidecar connected"); Some(Arc::new(client)) } Err(e) => { - warn!("Failed to connect to Circuit Breaker sidecar: {e}. Proceeding without circuit breaker."); + warn!("Failed to connect to Credible Layer sidecar: {e}. Proceeding without credible layer."); None } } @@ -153,7 +153,7 @@ impl BlockProducer { block_gas_limit: l2_gas_limit, eth_client, router_address, - circuit_breaker, + credible_layer, new_heads_sender, }) } @@ -190,10 +190,10 @@ impl BlockProducer { }; let payload = create_payload(&args, &self.store, Bytes::new())?; - // Circuit Breaker: send NewIteration before building the block. + // Credible Layer: send NewIteration before building the block. // CommitHead is sent AFTER the block is stored (see below). // The sidecar flow per block: NewIteration → Transaction(s) → CommitHead - if let Some(cb) = &self.circuit_breaker { + if let Some(cb) = &self.credible_layer { let block_number_bytes = u64_to_u256_bytes(payload.header.number); let timestamp_bytes = u64_to_u256_bytes(payload.header.timestamp); let beneficiary_bytes = payload.header.coinbase.as_bytes().to_vec(); @@ -225,7 +225,7 @@ impl BlockProducer { parent_beacon_block_root, }; if let Err(e) = cb.send_new_iteration(new_iteration).await { - warn!("Failed to send NewIteration to circuit breaker: {e}"); + warn!("Failed to send NewIteration to credible layer: {e}"); } } @@ -239,7 +239,7 @@ impl BlockProducer { &mut self.privileged_nonces, self.block_gas_limit, registered_chains, - self.circuit_breaker.clone(), + self.credible_layer.clone(), ) .await?; info!( @@ -292,8 +292,8 @@ impl BlockProducer { // Make the new head be part of the canonical chain apply_fork_choice(&self.store, block_hash, block_hash, block_hash).await?; - // Circuit Breaker: send CommitHead AFTER block is stored (matches Besu plugin flow) - if let Some(cb) = &self.circuit_breaker { + // Credible Layer: send CommitHead AFTER block is stored (matches Besu plugin flow) + if let Some(cb) = &self.credible_layer { let last_tx_hash = self .store .get_block_by_hash(block_hash) @@ -312,7 +312,7 @@ impl BlockProducer { timestamp: u64_to_u256_bytes(block_header.timestamp), }; if let Err(e) = cb.send_commit_head(commit_head).await { - warn!("Failed to send CommitHead to circuit breaker: {e}"); + warn!("Failed to send CommitHead to credible layer: {e}"); } } @@ -428,7 +428,7 @@ impl BlockProducer { l2_gas_limit: u64, new_heads_sender: Option>, ) -> Result, BlockProducerError> { - let circuit_breaker_url = cfg.circuit_breaker.sidecar_url.clone(); + let credible_layer_url = cfg.credible_layer.sidecar_url.clone(); let block_producer = Self::new( &cfg.block_producer, cfg.eth.rpc_url, @@ -438,7 +438,7 @@ impl BlockProducer { sequencer_state, router_address, l2_gas_limit, - circuit_breaker_url, + credible_layer_url, new_heads_sender, ) .await?; diff --git a/crates/l2/sequencer/block_producer/payload_builder.rs b/crates/l2/sequencer/block_producer/payload_builder.rs index c1da635e604..e66faae935a 100644 --- a/crates/l2/sequencer/block_producer/payload_builder.rs +++ b/crates/l2/sequencer/block_producer/payload_builder.rs @@ -1,5 +1,5 @@ -use crate::sequencer::circuit_breaker::{ - CircuitBreakerClient, +use crate::sequencer::credible_layer::{ + CredibleLayerClient, sidecar_proto::{Transaction as SidecarTransaction, TransactionEnv, TxExecutionId}, }; use crate::sequencer::errors::BlockProducerError; @@ -39,7 +39,7 @@ pub async fn build_payload( privileged_nonces: &mut HashMap>, block_gas_limit: u64, registered_chains: Vec, - circuit_breaker: Option>, + credible_layer: Option>, ) -> Result { let since = Instant::now(); let gas_limit = payload.header.gas_limit; @@ -54,7 +54,7 @@ pub async fn build_payload( privileged_nonces, block_gas_limit, registered_chains, - circuit_breaker, + credible_layer, ) .await?; blockchain.finalize_payload(&mut context)?; @@ -106,7 +106,7 @@ pub async fn fill_transactions( privileged_nonces: &mut HashMap>, configured_block_gas_limit: u64, registered_chains: Vec, - circuit_breaker: Option>, + credible_layer: Option>, ) -> Result<(), BlockProducerError> { let mut privileged_tx_count = 0; let VMType::L2(fee_config) = context.vm.vm_type else { @@ -193,9 +193,9 @@ pub async fn fill_transactions( // TODO: maybe fetch hash too when filtering mempool so we don't have to compute it here (we can do this in the same refactor as adding timestamp) let tx_hash = head_tx.tx.hash(); - // Task 2.5: For non-privileged transactions, check with the circuit breaker sidecar. + // Task 2.5: For non-privileged transactions, check with the credible layer sidecar. // Privileged transactions (PrivilegedL2Transaction) bypass this check. - if let Some(cb) = &circuit_breaker { + if let Some(cb) = &credible_layer { if !head_tx.is_privileged() { let block_num_bytes = { let mut buf = [0u8; 32]; @@ -369,7 +369,7 @@ fn build_transaction_env(tx: &Transaction, sender: ethrex_common::Address) -> Tr .access_list() .iter() .map(|(addr, keys)| { - use super::super::circuit_breaker::sidecar_proto::AccessListItem; + use super::super::credible_layer::sidecar_proto::AccessListItem; AccessListItem { address: addr.as_bytes().to_vec(), storage_keys: keys.iter().map(|k| k.as_bytes().to_vec()).collect(), @@ -380,7 +380,7 @@ fn build_transaction_env(tx: &Transaction, sender: ethrex_common::Address) -> Tr let authorization_list = tx .authorization_list() .map(|list| { - use super::super::circuit_breaker::sidecar_proto::Authorization; + use super::super::credible_layer::sidecar_proto::Authorization; list.iter() .map(|auth| { let chain_id_bytes = auth.chain_id.to_big_endian(); diff --git a/crates/l2/sequencer/configs.rs b/crates/l2/sequencer/configs.rs index fb0518030da..15e8b0381a0 100644 --- a/crates/l2/sequencer/configs.rs +++ b/crates/l2/sequencer/configs.rs @@ -17,13 +17,13 @@ pub struct SequencerConfig { pub monitor: MonitorConfig, pub admin_server: AdminConfig, pub state_updater: StateUpdaterConfig, - pub circuit_breaker: CircuitBreakerConfig, + pub credible_layer: CredibleLayerConfig, } -/// Configuration for the Circuit Breaker sidecar integration. +/// Configuration for the Credible Layer sidecar integration. /// Both URLs are optional; if absent, the feature is disabled. #[derive(Clone, Debug, Default)] -pub struct CircuitBreakerConfig { +pub struct CredibleLayerConfig { /// gRPC endpoint for the Credible Layer Assertion Enforcer sidecar. pub sidecar_url: Option, /// gRPC endpoint for the Aeges mempool pre-filter service. diff --git a/crates/l2/sequencer/circuit_breaker/aeges.rs b/crates/l2/sequencer/credible_layer/aeges.rs similarity index 96% rename from crates/l2/sequencer/circuit_breaker/aeges.rs rename to crates/l2/sequencer/credible_layer/aeges.rs index 44c60156ae1..6a132c87119 100644 --- a/crates/l2/sequencer/circuit_breaker/aeges.rs +++ b/crates/l2/sequencer/credible_layer/aeges.rs @@ -8,7 +8,7 @@ use super::aeges_proto::{ aeges_service_client::AegesServiceClient, VerifyTransactionRequest, Transaction as AegesTransaction, }; -use super::errors::CircuitBreakerError; +use super::errors::CredibleLayerError; /// Configuration for the Aeges mempool pre-filter. #[derive(Debug, Clone)] @@ -40,11 +40,11 @@ pub struct AegesClient { impl AegesClient { /// Connect to the Aeges service. - pub async fn connect(config: AegesConfig) -> Result { + pub async fn connect(config: AegesConfig) -> Result { info!(url = %config.aeges_url, "Connecting to Aeges service"); let channel = Channel::from_shared(config.aeges_url.clone()) - .map_err(|e| CircuitBreakerError::Internal(format!("Invalid Aeges URL: {e}")))? + .map_err(|e| CredibleLayerError::Internal(format!("Invalid Aeges URL: {e}")))? .connect() .await?; @@ -103,7 +103,7 @@ mod tests { use std::time::Duration; use super::*; - use crate::sequencer::circuit_breaker::aeges_proto::{ + use crate::sequencer::credible_layer::aeges_proto::{ Transaction as AegesTransaction, VerifyTransactionRequest, VerifyTransactionResponse, }; diff --git a/crates/l2/sequencer/circuit_breaker/client.rs b/crates/l2/sequencer/credible_layer/client.rs similarity index 93% rename from crates/l2/sequencer/circuit_breaker/client.rs rename to crates/l2/sequencer/credible_layer/client.rs index 796bce9a35f..c3405946c55 100644 --- a/crates/l2/sequencer/circuit_breaker/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -6,16 +6,16 @@ use tokio::sync::{mpsc, Mutex}; use tonic::transport::Channel; use tracing::{debug, info, warn}; -use super::errors::CircuitBreakerError; +use super::errors::CredibleLayerError; use super::sidecar_proto::{ self, sidecar_transport_client::SidecarTransportClient, CommitHead, Event, GetTransactionRequest, NewIteration, ResultStatus, Transaction, TransactionResult, TxExecutionId, }; -/// Configuration for the Circuit Breaker gRPC client. +/// Configuration for the Credible Layer gRPC client. #[derive(Debug, Clone)] -pub struct CircuitBreakerConfig { +pub struct CredibleLayerConfig { /// gRPC endpoint URL for the sidecar (e.g., "http://localhost:50051") pub sidecar_url: String, /// Timeout for waiting for a transaction result from the sidecar @@ -24,7 +24,7 @@ pub struct CircuitBreakerConfig { pub poll_timeout: Duration, } -impl Default for CircuitBreakerConfig { +impl Default for CredibleLayerConfig { fn default() -> Self { Self { sidecar_url: "http://localhost:50051".to_string(), @@ -39,8 +39,8 @@ impl Default for CircuitBreakerConfig { /// Maintains a persistent bidirectional `StreamEvents` gRPC stream. Events are sent /// via an mpsc channel that feeds the stream. Transaction results are retrieved via /// the `GetTransaction` unary RPC. -pub struct CircuitBreakerClient { - config: CircuitBreakerConfig, +pub struct CredibleLayerClient { + config: CredibleLayerConfig, /// Sender side of the persistent StreamEvents stream event_sender: mpsc::Sender, /// Monotonically increasing event ID counter @@ -51,17 +51,17 @@ pub struct CircuitBreakerClient { grpc_client: Arc>>, } -impl CircuitBreakerClient { +impl CredibleLayerClient { /// Create a new client with lazy connection to the sidecar. /// Opens a persistent StreamEvents bidirectional stream in the background. - pub async fn connect(config: CircuitBreakerConfig) -> Result { + pub async fn connect(config: CredibleLayerConfig) -> Result { info!( url = %config.sidecar_url, - "Configuring Circuit Breaker sidecar client" + "Configuring Credible Layer sidecar client" ); let channel = Channel::from_shared(config.sidecar_url.clone()) - .map_err(|e| CircuitBreakerError::Internal(format!("Invalid URL: {e}")))? + .map_err(|e| CredibleLayerError::Internal(format!("Invalid URL: {e}")))? .connect_timeout(Duration::from_secs(5)) .timeout(Duration::from_secs(5)) .connect_lazy(); @@ -162,7 +162,7 @@ impl CircuitBreakerClient { } }); - info!("Circuit Breaker client ready (persistent stream opened)"); + info!("Credible Layer client ready (persistent stream opened)"); Ok(Self { config, @@ -187,7 +187,7 @@ impl CircuitBreakerClient { pub async fn send_commit_head( &self, commit_head: CommitHead, - ) -> Result<(), CircuitBreakerError> { + ) -> Result<(), CredibleLayerError> { let event = Event { event_id: self.next_event_id(), event: Some(sidecar_proto::event::Event::CommitHead(commit_head)), @@ -195,14 +195,14 @@ impl CircuitBreakerClient { self.event_sender .send(event) .await - .map_err(|_| CircuitBreakerError::StreamClosed) + .map_err(|_| CredibleLayerError::StreamClosed) } /// Send a NewIteration event (new block started) and increment the iteration ID. pub async fn send_new_iteration( &self, new_iteration: NewIteration, - ) -> Result<(), CircuitBreakerError> { + ) -> Result<(), CredibleLayerError> { self.iteration_id.fetch_add(1, Ordering::Relaxed); let event = Event { event_id: self.next_event_id(), @@ -211,7 +211,7 @@ impl CircuitBreakerClient { self.event_sender .send(event) .await - .map_err(|_| CircuitBreakerError::StreamClosed) + .map_err(|_| CredibleLayerError::StreamClosed) } /// Send a Transaction event and wait for the sidecar's verdict. @@ -321,25 +321,25 @@ mod tests { use std::time::Duration; use super::*; - use crate::sequencer::circuit_breaker::sidecar_proto::{ + use crate::sequencer::credible_layer::sidecar_proto::{ CommitHead, NewIteration, ResultStatus, TransactionResult, TxExecutionId, }; #[test] fn config_default_url_is_localhost_50051() { - let cfg = CircuitBreakerConfig::default(); + let cfg = CredibleLayerConfig::default(); assert_eq!(cfg.sidecar_url, "http://localhost:50051"); } #[test] fn config_default_result_timeout_is_500ms() { - let cfg = CircuitBreakerConfig::default(); + let cfg = CredibleLayerConfig::default(); assert_eq!(cfg.result_timeout, Duration::from_millis(500)); } #[test] fn config_default_poll_timeout_is_200ms() { - let cfg = CircuitBreakerConfig::default(); + let cfg = CredibleLayerConfig::default(); assert_eq!(cfg.poll_timeout, Duration::from_millis(200)); } @@ -405,7 +405,7 @@ mod tests { #[test] fn new_iteration_has_expected_iteration_id() { - use crate::sequencer::circuit_breaker::sidecar_proto::BlockEnv; + use crate::sequencer::credible_layer::sidecar_proto::BlockEnv; let ni = NewIteration { block_env: Some(BlockEnv { number: vec![0; 32], beneficiary: vec![0; 20], timestamp: vec![0; 32], gas_limit: 30_000_000, basefee: 1_000_000_000, difficulty: vec![0; 32], prevrandao: None, blob_excess_gas_and_price: None }), iteration_id: 42, parent_block_hash: None, parent_beacon_block_root: None, diff --git a/crates/l2/sequencer/circuit_breaker/errors.rs b/crates/l2/sequencer/credible_layer/errors.rs similarity index 83% rename from crates/l2/sequencer/circuit_breaker/errors.rs rename to crates/l2/sequencer/credible_layer/errors.rs index eea1aab76ba..be3d313580b 100644 --- a/crates/l2/sequencer/circuit_breaker/errors.rs +++ b/crates/l2/sequencer/credible_layer/errors.rs @@ -1,7 +1,7 @@ use tonic::Status; #[derive(Debug, thiserror::Error)] -pub enum CircuitBreakerError { +pub enum CredibleLayerError { #[error("gRPC transport error: {0}")] Transport(#[from] tonic::transport::Error), #[error("gRPC status error: {0}")] @@ -10,6 +10,6 @@ pub enum CircuitBreakerError { StreamClosed, #[error("Result timeout for tx {0}")] ResultTimeout(String), - #[error("Circuit Breaker error: {0}")] + #[error("Credible Layer error: {0}")] Internal(String), } diff --git a/crates/l2/sequencer/circuit_breaker/mock_sidecar/main.rs b/crates/l2/sequencer/credible_layer/mock_sidecar/main.rs similarity index 99% rename from crates/l2/sequencer/circuit_breaker/mock_sidecar/main.rs rename to crates/l2/sequencer/credible_layer/mock_sidecar/main.rs index 828426304c5..660a4b06fd4 100644 --- a/crates/l2/sequencer/circuit_breaker/mock_sidecar/main.rs +++ b/crates/l2/sequencer/credible_layer/mock_sidecar/main.rs @@ -1,4 +1,4 @@ -/// Mock Circuit Breaker Sidecar for end-to-end testing. +/// Mock Credible Layer Sidecar for end-to-end testing. /// /// Implements the sidecar.proto gRPC protocol and rejects any transaction /// that calls the `transferOwnership(address)` function selector (0xf2fde38b). diff --git a/crates/l2/sequencer/circuit_breaker/mod.rs b/crates/l2/sequencer/credible_layer/mod.rs similarity index 73% rename from crates/l2/sequencer/circuit_breaker/mod.rs rename to crates/l2/sequencer/credible_layer/mod.rs index 00ffea3272d..7163c3420b9 100644 --- a/crates/l2/sequencer/circuit_breaker/mod.rs +++ b/crates/l2/sequencer/credible_layer/mod.rs @@ -1,18 +1,18 @@ -/// Circuit Breaker integration with the Phylax Credible Layer. +/// Credible Layer integration with the Phylax Credible Layer. /// /// This module implements a gRPC client that communicates with the Credible Layer /// Assertion Enforcer sidecar during block building. Transactions that fail assertion /// validation are dropped before block inclusion. /// -/// The integration is opt-in via the `--circuit-breaker-url` CLI flag. +/// The integration is opt-in via the `--credible-layer-url` CLI flag. /// When disabled, there is zero overhead. pub mod client; pub mod aeges; pub mod errors; -pub use client::CircuitBreakerClient; +pub use client::CredibleLayerClient; pub use aeges::AegesClient; -pub use errors::CircuitBreakerError; +pub use errors::CredibleLayerError; /// Generated protobuf/gRPC types for sidecar.proto pub mod sidecar_proto { diff --git a/crates/l2/sequencer/mod.rs b/crates/l2/sequencer/mod.rs index ef718e39074..6e63b3d97a9 100644 --- a/crates/l2/sequencer/mod.rs +++ b/crates/l2/sequencer/mod.rs @@ -39,7 +39,7 @@ pub mod proof_coordinator; pub use ethrex_l2_common::sequencer_state::{SequencerState, SequencerStatus}; pub mod state_updater; -pub mod circuit_breaker; +pub mod credible_layer; pub mod configs; pub mod errors; pub mod setup; diff --git a/docs/CLI.md b/docs/CLI.md index 46fef4acb66..fef5acbf3a2 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -669,7 +669,7 @@ L2 options: [default: 0xffd790338a2798b648806fc8635ac7bf14af15425fed0c8f25bcc5febaa9b192] --l2.ws-enabled - Enable WebSocket RPC server for L2. Required by the Circuit Breaker sidecar. + Enable WebSocket RPC server for L2. Required by the Credible Layer sidecar. [env: ETHREX_L2_WS_ENABLED=] @@ -685,18 +685,18 @@ L2 options: [env: ETHREX_L2_WS_PORT=] [default: 1729] -Circuit Breaker options: - --circuit-breaker-url +Credible Layer options: + --credible-layer-url gRPC endpoint for the Credible Layer Assertion Enforcer sidecar. When set, enables transaction validation against assertions during block building. - [env: ETHREX_CIRCUIT_BREAKER_URL=] + [env: ETHREX_CREDIBLE_LAYER_URL=] - --circuit-breaker-aeges-url + --credible-layer-aeges-url gRPC endpoint for the Aeges mempool pre-filter service. When set, transactions are validated before mempool admission. - [env: ETHREX_CIRCUIT_BREAKER_AEGES_URL=] + [env: ETHREX_CREDIBLE_LAYER_AEGES_URL=] Monitor options: --no-monitor diff --git a/docs/l2/circuit_breaker.md b/docs/l2/credible_layer.md similarity index 92% rename from docs/l2/circuit_breaker.md rename to docs/l2/credible_layer.md index 0fb2561a873..b222ba93150 100644 --- a/docs/l2/circuit_breaker.md +++ b/docs/l2/credible_layer.md @@ -1,8 +1,8 @@ -# Circuit Breaker (Credible Layer Integration) +# Credible Layer Integration ## Overview -Circuit Breaker integrates ethrex L2 with [Phylax Systems' Credible Layer](https://docs.phylax.systems/credible/credible-introduction) — a pre-execution security infrastructure that validates transactions against on-chain assertions before block inclusion. Transactions that violate an assertion are silently dropped before they land on-chain. +Credible Layer integrates ethrex L2 with [Phylax Systems' Credible Layer](https://docs.phylax.systems/credible/credible-introduction) — a pre-execution security infrastructure that validates transactions against on-chain assertions before block inclusion. Transactions that violate an assertion are silently dropped before they land on-chain. ### Architecture @@ -76,7 +76,7 @@ Per block: - If `ASSERTION_FAILED`: skip the transaction - Otherwise: include it -Privileged transactions (L1→L2 deposits) bypass Circuit Breaker entirely. +Privileged transactions (L1→L2 deposits) bypass Credible Layer entirely. --- @@ -86,10 +86,10 @@ Privileged transactions (L1→L2 deposits) bypass Circuit Breaker entirely. | File | Role | |------|------| -| `crates/l2/sequencer/circuit_breaker/mod.rs` | Module root, proto imports | -| `crates/l2/sequencer/circuit_breaker/client.rs` | `CircuitBreakerClient` — gRPC client for sidecar | -| `crates/l2/sequencer/circuit_breaker/aeges.rs` | `AegesClient` — gRPC client for Aeges | -| `crates/l2/sequencer/circuit_breaker/errors.rs` | Error types | +| `crates/l2/sequencer/credible_layer/mod.rs` | Module root, proto imports | +| `crates/l2/sequencer/credible_layer/client.rs` | `CredibleLayerClient` — gRPC client for sidecar | +| `crates/l2/sequencer/credible_layer/aeges.rs` | `AegesClient` — gRPC client for Aeges | +| `crates/l2/sequencer/credible_layer/errors.rs` | Error types | | `crates/l2/proto/sidecar.proto` | Sidecar gRPC protocol definition | | `crates/l2/proto/aeges.proto` | Aeges gRPC protocol definition | | `crates/l2/sequencer/block_producer.rs` | Block producer — sends CommitHead + NewIteration | @@ -103,11 +103,11 @@ CLI flags: | Flag | Description | Default | |------|-------------|---------| -| `--circuit-breaker-url` | gRPC endpoint for the sidecar | (disabled) | -| `--circuit-breaker-aeges-url` | gRPC endpoint for Aeges pre-filter | (disabled) | -| `--circuit-breaker-state-oracle` | Address of the deployed State Oracle contract on L2 | (none) | +| `--credible-layer-url` | gRPC endpoint for the sidecar | (disabled) | +| `--credible-layer-aeges-url` | gRPC endpoint for Aeges pre-filter | (disabled) | +| `--credible-layer-state-oracle` | Address of the deployed State Oracle contract on L2 | (none) | -When `--circuit-breaker-url` is not set, Circuit Breaker is completely disabled with zero overhead. +When `--credible-layer-url` is not set, Credible Layer is completely disabled with zero overhead. ### Sidecar Requirements @@ -124,7 +124,7 @@ The `debug_traceBlockByNumber` with `prestateTracer` in diff mode is particularl ## How to Run (End-to-End) -This is a step-by-step guide to run the full Circuit Breaker stack locally, deploy an assertion, and verify that violating transactions are dropped. All steps have been tested and verified. +This is a step-by-step guide to run the full Credible Layer stack locally, deploy an assertion, and verify that violating transactions are dropped. All steps have been tested and verified. ### Prerequisites @@ -156,7 +156,7 @@ cast block-number --rpc-url http://localhost:8545 COMPILE_CONTRACTS=true make deploy-l1 ``` -### Step 3: Start ethrex L2 with Circuit Breaker +### Step 3: Start ethrex L2 with Credible Layer ```bash export $(cat ../../cmd/.env | xargs) @@ -173,7 +173,7 @@ RUST_LOG=info,ethrex_l2=debug ../../target/release/ethrex l2 \ --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d \ --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 \ --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d \ - --circuit-breaker-url http://localhost:50051 \ + --credible-layer-url http://localhost:50051 \ --l2.ws-enabled --l2.ws-port 1730 & # Verify (wait ~10s for L2 to start) @@ -231,7 +231,7 @@ docker run -d --name assertion-da -p 5001:5001 \ ### Step 6: Deploy OwnableTarget test contract ```bash -cd /crates/l2/contracts/src/circuit_breaker +cd /crates/l2/contracts/src/credible_layer solc --bin OwnableTarget.sol -o /tmp/cb_compiled --overwrite PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 @@ -413,7 +413,7 @@ cast call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 timeout 25 cast send $OWNABLE_TARGET "transferOwnership(address)" \ 0x0000000000000000000000000000000000000001 \ --private-key $PK --rpc-url http://localhost:1729 -# Expected: times out with no output (tx was dropped by Circuit Breaker) +# Expected: times out with no output (tx was dropped by Credible Layer) # Verify owner is UNCHANGED cast call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 From c8d011e2b5ba92be2da3645ef660726b106e8bc0 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 18:10:43 -0300 Subject: [PATCH 03/33] Replace ASCII architecture diagram with SVG in Credible Layer docs --- docs/l2/credible_layer.md | 34 +----- docs/l2/img/credible_layer_architecture.svg | 114 ++++++++++++++++++++ 2 files changed, 115 insertions(+), 33 deletions(-) create mode 100644 docs/l2/img/credible_layer_architecture.svg diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index b222ba93150..e532caa1cd9 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -6,39 +6,7 @@ Credible Layer integrates ethrex L2 with [Phylax Systems' Credible Layer](https: ### Architecture -``` - ethrex L2 Sequencer - ┌─────────────────────────────────────────┐ - │ │ -User ──tx──▶ RPC ──┼──▶ Aeges check ──▶ Mempool │ - │ │ │ - │ ▼ │ - │ Block Producer: fill_transactions() │ - │ │ │ - │ │ For each tx: │ - │ ├─ Send Transaction ──────────────────────▶ ┌────────────────────┐ - │ ├─ Await verdict ◀──────────────────────│ Credible Layer │ - │ ├─ ASSERTION_FAILED? Skip tx │ Sidecar │ - │ └─ Otherwise: include tx │ (Assertion Enforcer)│ - │ │ │ - │ On block sealed: │ Runs PhEVM to │ - │ └─ Send CommitHead ────────────────────────│ evaluate assertions│ - │ │ │ - │ WebSocket server (:1730) │ Tracks chain state │ - │ └─ eth_subscribe("newHeads") ◀────────────│ via WS + traces │ - │ │ └────────────────────┘ - │ debug_traceBlockByNumber │ │ - │ └─ prestateTracer (diff mode) ◀───────────────────┘ - │ │ - └─────────────────────────────────────────┘ - │ - On-chain (L2) - ┌─────────────────────────────────────────┐ - │ State Oracle Assertion DA │ - │ (registry of (bytecode storage) │ - │ assertions) │ - └─────────────────────────────────────────┘ -``` +![Credible Layer Architecture](img/credible_layer_architecture.svg) ### Key Concepts diff --git a/docs/l2/img/credible_layer_architecture.svg b/docs/l2/img/credible_layer_architecture.svg new file mode 100644 index 00000000000..403279f2a8c --- /dev/null +++ b/docs/l2/img/credible_layer_architecture.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + ethrex L2 Sequencer + + + User + + tx + + + RPC + + + Aeges check + + + Mempool + + + + + + + Block Producer: fill_transactions() + + + For each tx: + + + Send Transaction + + + + Await verdict + + + + ASSERTION_FAILED? + Skip tx + + + Otherwise: + include tx + + + On block sealed: + Send CommitHead + + + + + WebSocket server (:1730) + eth_subscribe("newHeads") + + + + + debug_traceBlockByNumber + prestateTracer (diff mode) + + + + + Credible Layer + Sidecar + (Assertion Enforcer) + + + + Runs PhEVM to + evaluate assertions + + Tracks chain state + via WS + traces + + + + + + On-chain (L2) + + + + + + + State Oracle + (registry of assertions) + + + + Assertion DA + (bytecode storage) + + + + gRPC + From 6b1628ec6ab4dcf72d0b5c8ddb7f0adc4b66a0f0 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 18:30:12 -0300 Subject: [PATCH 04/33] Add note about expected L1 dev mode payload_id errors on startup --- docs/l2/credible_layer.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index e532caa1cd9..1a1cfe79a63 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -113,11 +113,13 @@ rm -rf dev_ethrex_l1 dev_ethrex_l2 --authrpc.port 8551 --dev \ --datadir dev_ethrex_l1 & -# Verify -sleep 5 +# Verify (wait ~10s — initial "payload_id is None" errors are normal and resolve themselves) +sleep 10 cast block-number --rpc-url http://localhost:8545 ``` +> **Note:** You may see `ERROR Failed to produce block: payload_id is None in ForkChoiceResponse` in the first few seconds. This is a known L1 dev mode timing issue — the engine API needs a moment to initialize. The errors stop after a few blocks and block production continues normally. + ### Step 2: Deploy L2 contracts on L1 ```bash From ecce9b78f9fb69a2759a306314c6c8aa59f28d8e Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 18:36:00 -0300 Subject: [PATCH 05/33] Use pre-built binary for L2 contract deployment to avoid recompilation Build with COMPILE_CONTRACTS=true once in prerequisites, then use the binary directly in Step 2 instead of cargo run which triggers a rebuild. --- docs/l2/credible_layer.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index 1a1cfe79a63..efb2593a4d8 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -99,7 +99,10 @@ This is a step-by-step guide to run the full Credible Layer stack locally, deplo - [Foundry](https://book.getfoundry.sh/getting-started/installation) (`forge`, `cast`) - Docker (running) - Node.js >= 22 and `pnpm` (for the assertion indexer) -- ethrex built: `cargo build --release -p ethrex --features l2` +- ethrex built (including contract compilation): + ```bash + COMPILE_CONTRACTS=true cargo build --release -p ethrex --features l2 + ``` ### Step 1: Start ethrex L1 @@ -123,9 +126,20 @@ cast block-number --rpc-url http://localhost:8545 ### Step 2: Deploy L2 contracts on L1 ```bash -COMPILE_CONTRACTS=true make deploy-l1 +../../target/release/ethrex l2 deploy \ + --eth-rpc-url http://localhost:8545 \ + --private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 \ + --on-chain-proposer-owner 0x4417092b70a3e5f10dc504d0947dd256b965fc62 \ + --bridge-owner 0x4417092b70a3e5f10dc504d0947dd256b965fc62 \ + --bridge-owner-pk 0x941e103320615d394a55708be13e45994c7d93b932b064dbcb2b511fe3254e2e \ + --deposit-rich \ + --private-keys-file-path ../../fixtures/keys/private_keys_l1.txt \ + --genesis-l1-path ../../fixtures/genesis/l1.json \ + --genesis-l2-path ../../fixtures/genesis/l2.json ``` +This uses the pre-built binary directly (no recompilation). Addresses are written to `cmd/.env`. + ### Step 3: Start ethrex L2 with Credible Layer ```bash From 94ae06314fdcbfd63e04aa5cda32cfbd719b9bcf Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 19:06:39 -0300 Subject: [PATCH 06/33] Skip tx evaluation when sidecar stream is disconnected to avoid blocking block production. Adds a stream_connected flag that the background task sets/clears. When false, evaluate_transaction returns true immediately instead of polling GetTransaction (which would timeout and block the block producer for ~2s per tx). --- crates/l2/sequencer/credible_layer/client.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index c3405946c55..2d07a8c3bc1 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -49,6 +49,9 @@ pub struct CredibleLayerClient { iteration_id: AtomicU64, /// gRPC client for unary calls (GetTransaction) grpc_client: Arc>>, + /// Whether the StreamEvents stream is currently connected. + /// When false, evaluate_transaction skips immediately (permissive). + stream_connected: Arc, } impl CredibleLayerClient { @@ -67,6 +70,8 @@ impl CredibleLayerClient { .connect_lazy(); let mut client = SidecarTransportClient::new(channel.clone()); + let stream_connected = Arc::new(AtomicBool::new(false)); + let stream_connected_bg = stream_connected.clone(); // Create the event channel. The sender goes to the client, the receiver // is owned by the background stream task. @@ -84,6 +89,7 @@ impl CredibleLayerClient { match client.stream_events(grpc_stream).await { Ok(response) => { info!("StreamEvents stream connected to sidecar"); + stream_connected_bg.store(true, Ordering::Relaxed); let mut ack_stream = response.into_inner(); // Send an initial CommitHead (block 0) as the first event on @@ -158,6 +164,7 @@ impl CredibleLayerClient { debug!(%status, "StreamEvents connect failed, retrying in 5s"); } } + stream_connected_bg.store(false, Ordering::Relaxed); tokio::time::sleep(Duration::from_secs(5)).await; } }); @@ -170,6 +177,7 @@ impl CredibleLayerClient { event_id_counter: AtomicU64::new(1), iteration_id: AtomicU64::new(0), grpc_client: Arc::new(Mutex::new(SidecarTransportClient::new(channel))), + stream_connected, }) } @@ -219,6 +227,12 @@ impl CredibleLayerClient { /// Returns `true` if the transaction should be included, `false` if it should be dropped. /// On any error or timeout, returns `true` (permissive behavior). pub async fn evaluate_transaction(&self, transaction: Transaction) -> bool { + // Fast path: if the stream isn't connected, skip evaluation (permissive). + // This avoids blocking block production when the sidecar isn't running. + if !self.stream_connected.load(Ordering::Relaxed) { + return true; + } + let tx_exec_id = transaction.tx_execution_id.clone(); let tx_hash = tx_exec_id .as_ref() From f5da723de7e482a18e85114e1cc3ff5ed69bd9f4 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 19:32:18 -0300 Subject: [PATCH 07/33] Strip ANSI color codes from docker logs grep commands in the guide Docker logs contain ANSI escape sequences that break plain grep matching. Add sed pipe to strip them in all docker logs grep commands. --- docs/l2/credible_layer.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index efb2593a4d8..c125053d083 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -377,7 +377,7 @@ Wait ~15 seconds, then verify the sidecar is healthy and loaded the assertion: curl http://localhost:9547/health # Should return "OK" # Check logs for assertion loading (wait ~15s after start) -docker logs credible-sidecar 2>&1 | grep "trigger_recorder" +docker logs credible-sidecar 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep "trigger_recorder" # Should show: triggers: {Call { trigger_selector: 0xf2fde38b }: {0x7ab4397a}} # This means: transferOwnership(address) calls will trigger the assertion ``` @@ -407,9 +407,10 @@ cast call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 Check sidecar logs to confirm the assertion caught it: ```bash -docker logs credible-sidecar 2>&1 | grep "is_valid=false" +# Note: docker logs contain ANSI color codes, so pipe through sed to strip them +docker logs credible-sidecar 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep "is_valid=false" # Should show: Transaction processed ... is_valid=false -docker logs credible-sidecar 2>&1 | grep "assertion_failure_count" +docker logs credible-sidecar 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep "assertion_failure" # Should show: Transaction failed assertion validation ... failed_assertions=[...] ``` @@ -426,7 +427,7 @@ cast send $OWNABLE_TARGET "doSomething()" \ Check sidecar logs: ```bash -docker logs credible-sidecar 2>&1 | grep "is_valid=true" +docker logs credible-sidecar 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep "is_valid=true" # Should show: Transaction processed ... is_valid=true ``` From 488406c28d9601845cb38b14f994d95d6a6815f4 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 19:41:49 -0300 Subject: [PATCH 08/33] Refine Credible Layer integration: boolean gate flag, log levels, and doc improvements Add a --credible-layer boolean gate flag to CredibleLayerOptions (modeled after --aligned), so operators explicitly opt in before providing any --credible-layer-* sub-flags. The three sub-flags (--credible-layer-url, --credible-layer-aeges-url, --credible-layer-state-oracle) now carry requires = "credible_layer". The TryFrom mapping returns CredibleLayerConfig::default() when the gate flag is absent. Promote three log messages in client.rs from debug to info/warn: forwarding events is useful for operators to trace the event flow (info), while StreamEvents connect failures and channel closure are anomalies that deserve visibility (warn). The GetTransaction poll-attempt message stays at debug since it fires on every poll attempt. Update credible_layer.md: clarify the privileged-tx bypass is specific to this first version of the integration, expand Key Files descriptions to distinguish the sidecar client from the Aeges client by listing their RPCs, reflect the new --credible-layer gate flag in the Configuration table and the Step 3 startup command, simplify RUST_LOG to plain "info" since the important Credible Layer messages are now at INFO/WARN level, replace cast commands with rex equivalents (block-number, send -k, call, address -k), and remove cast from the Prerequisites list (only forge is now required). --- cmd/ethrex/l2/options.rs | 26 ++++++++-- crates/l2/sequencer/credible_layer/client.rs | 6 +-- docs/l2/credible_layer.md | 52 ++++++++++---------- 3 files changed, 51 insertions(+), 33 deletions(-) diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index d638e25d22e..674bf932af4 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -296,10 +296,14 @@ impl TryFrom for SequencerConfig { start_at: opts.state_updater_opts.start_at, l2_head_check_rpc_url: opts.state_updater_opts.l2_head_check_rpc_url, }, - credible_layer: CredibleLayerConfig { - sidecar_url: opts.credible_layer_opts.credible_layer_url, - aeges_url: opts.credible_layer_opts.credible_layer_aeges_url, - state_oracle_address: opts.credible_layer_opts.credible_layer_state_oracle, + credible_layer: if opts.credible_layer_opts.credible_layer { + CredibleLayerConfig { + sidecar_url: opts.credible_layer_opts.credible_layer_url, + aeges_url: opts.credible_layer_opts.credible_layer_aeges_url, + state_oracle_address: opts.credible_layer_opts.credible_layer_state_oracle, + } + } else { + CredibleLayerConfig::default() }, }) } @@ -1107,11 +1111,21 @@ impl Default for AdminOptions { #[derive(Parser, Default, Debug)] pub struct CredibleLayerOptions { + #[arg( + long = "credible-layer", + action = clap::ArgAction::SetTrue, + default_value = "false", + env = "ETHREX_CREDIBLE_LAYER", + help = "Enable the Credible Layer integration. Required before using any --credible-layer-* flags.", + help_heading = "Credible Layer options" + )] + pub credible_layer: bool, #[arg( long = "credible-layer-url", value_name = "URL", env = "ETHREX_CREDIBLE_LAYER_URL", - help = "gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). When set, the credible layer integration is enabled.", + requires = "credible_layer", + help = "gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051).", help_heading = "Credible Layer options" )] pub credible_layer_url: Option, @@ -1119,6 +1133,7 @@ pub struct CredibleLayerOptions { long = "credible-layer-aeges-url", value_name = "URL", env = "ETHREX_CREDIBLE_LAYER_AEGES_URL", + requires = "credible_layer", help = "gRPC endpoint for the Aeges mempool pre-filter service (e.g. http://localhost:8080). When set, Aeges pre-filtering is enabled.", help_heading = "Credible Layer options" )] @@ -1127,6 +1142,7 @@ pub struct CredibleLayerOptions { long = "credible-layer-state-oracle", value_name = "ADDRESS", env = "ETHREX_CREDIBLE_LAYER_STATE_ORACLE", + requires = "credible_layer", help = "Address of the already-deployed State Oracle contract on L2. The State Oracle maps protected contracts to their active assertions and is required by the Credible Layer sidecar. Deploy it separately using the Phylax toolchain (see crates/l2/contracts/src/credible_layer/README.md).", help_heading = "Credible Layer options" )] diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 2d07a8c3bc1..7558f045ee8 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -126,7 +126,7 @@ impl CredibleLayerClient { Some(sidecar_proto::event::Event::Reorg(_)) => "Reorg", None => "None", }; - debug!("Forwarding {event_type} event to gRPC stream (event_id={})", e.event_id); + info!("Forwarding {event_type} event to gRPC stream (event_id={})", e.event_id); if grpc_tx.send(e).await.is_err() { warn!("gRPC stream send failed, reconnecting"); break; @@ -134,7 +134,7 @@ impl CredibleLayerClient { } None => { // Main channel closed — client dropped - debug!("Event channel closed, stopping stream task"); + warn!("Event channel closed, stopping stream task"); return; } } @@ -161,7 +161,7 @@ impl CredibleLayerClient { } } Err(status) => { - debug!(%status, "StreamEvents connect failed, retrying in 5s"); + warn!(%status, "StreamEvents connect failed, retrying in 5s"); } } stream_connected_bg.store(false, Ordering::Relaxed); diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index c125053d083..ceca5bfa545 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -44,7 +44,7 @@ Per block: - If `ASSERTION_FAILED`: skip the transaction - Otherwise: include it -Privileged transactions (L1→L2 deposits) bypass Credible Layer entirely. +Privileged transactions (L1→L2 deposits) bypass the Credible Layer in this first version of the integration. --- @@ -55,8 +55,8 @@ Privileged transactions (L1→L2 deposits) bypass Credible Layer entirely. | File | Role | |------|------| | `crates/l2/sequencer/credible_layer/mod.rs` | Module root, proto imports | -| `crates/l2/sequencer/credible_layer/client.rs` | `CredibleLayerClient` — gRPC client for sidecar | -| `crates/l2/sequencer/credible_layer/aeges.rs` | `AegesClient` — gRPC client for Aeges | +| `crates/l2/sequencer/credible_layer/client.rs` | `CredibleLayerClient` — gRPC client for the sidecar; handles block building validation (StreamEvents, GetTransaction) | +| `crates/l2/sequencer/credible_layer/aeges.rs` | `AegesClient` — gRPC client for the Aeges mempool pre-filter; validates transactions before pool admission (VerifyTransaction) | | `crates/l2/sequencer/credible_layer/errors.rs` | Error types | | `crates/l2/proto/sidecar.proto` | Sidecar gRPC protocol definition | | `crates/l2/proto/aeges.proto` | Aeges gRPC protocol definition | @@ -71,11 +71,12 @@ CLI flags: | Flag | Description | Default | |------|-------------|---------| -| `--credible-layer-url` | gRPC endpoint for the sidecar | (disabled) | -| `--credible-layer-aeges-url` | gRPC endpoint for Aeges pre-filter | (disabled) | -| `--credible-layer-state-oracle` | Address of the deployed State Oracle contract on L2 | (none) | +| `--credible-layer` | Enable the Credible Layer integration (required gate flag) | `false` | +| `--credible-layer-url` | gRPC endpoint for the sidecar (requires `--credible-layer`) | (none) | +| `--credible-layer-aeges-url` | gRPC endpoint for Aeges pre-filter (requires `--credible-layer`) | (none) | +| `--credible-layer-state-oracle` | Address of the deployed State Oracle contract on L2 (requires `--credible-layer`) | (none) | -When `--credible-layer-url` is not set, Credible Layer is completely disabled with zero overhead. +When `--credible-layer` is not set, Credible Layer is completely disabled with zero overhead. ### Sidecar Requirements @@ -96,7 +97,7 @@ This is a step-by-step guide to run the full Credible Layer stack locally, deplo ### Prerequisites -- [Foundry](https://book.getfoundry.sh/getting-started/installation) (`forge`, `cast`) +- [Foundry](https://book.getfoundry.sh/getting-started/installation) (`forge`) - Docker (running) - Node.js >= 22 and `pnpm` (for the assertion indexer) - ethrex built (including contract compilation): @@ -118,7 +119,7 @@ rm -rf dev_ethrex_l1 dev_ethrex_l2 # Verify (wait ~10s — initial "payload_id is None" errors are normal and resolve themselves) sleep 10 -cast block-number --rpc-url http://localhost:8545 +rex block-number --rpc-url http://localhost:8545 ``` > **Note:** You may see `ERROR Failed to produce block: payload_id is None in ForkChoiceResponse` in the first few seconds. This is a known L1 dev mode timing issue — the engine API needs a moment to initialize. The errors stop after a few blocks and block production continues normally. @@ -145,7 +146,7 @@ This uses the pre-built binary directly (no recompilation). Addresses are writte ```bash export $(cat ../../cmd/.env | xargs) -RUST_LOG=info,ethrex_l2=debug ../../target/release/ethrex l2 \ +RUST_LOG=info ../../target/release/ethrex l2 \ --no-monitor \ --watcher.block-delay 0 \ --network ../../fixtures/genesis/l2.json \ @@ -157,11 +158,12 @@ RUST_LOG=info,ethrex_l2=debug ../../target/release/ethrex l2 \ --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d \ --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 \ --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d \ + --credible-layer \ --credible-layer-url http://localhost:50051 \ --l2.ws-enabled --l2.ws-port 1730 & # Verify (wait ~10s for L2 to start) -cast block-number --rpc-url http://localhost:1729 +rex block-number --rpc-url http://localhost:1729 ``` The L2 will log `StreamEvents connect failed, retrying in 5s` until the sidecar starts. This is expected — the L2 is permissive and keeps producing blocks. @@ -179,7 +181,7 @@ PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 STATE_ORACLE_MAX_ASSERTIONS_PER_AA=100 \ STATE_ORACLE_ASSERTION_TIMELOCK_BLOCKS=1 \ -STATE_ORACLE_ADMIN_ADDRESS=$(cast wallet address $PK) \ +STATE_ORACLE_ADMIN_ADDRESS=$(rex address -k $PK) \ DA_PROVER_ADDRESS=0xb0d60c09103F4a5c04EE8537A22ECD6a34382B36 \ DEPLOY_ADMIN_VERIFIER_OWNER=true \ DEPLOY_ADMIN_VERIFIER_WHITELIST=false \ @@ -266,21 +268,21 @@ ADMIN_VERIFIER=0x9f9F5Fd89ad648f2C000C954d8d9C87743243eC5 DA_VERIFIER=0x422A3492e218383753D8006C7Bfa97815B44373F # Disable whitelist (required for devnet) -cast send $STATE_ORACLE "disableWhitelist()" \ - --private-key $PK --rpc-url http://localhost:1729 +rex send $STATE_ORACLE "disableWhitelist()" \ + -k $PK --rpc-url http://localhost:1729 # Register the target contract as an assertion adopter -cast send $STATE_ORACLE "registerAssertionAdopter(address,address,bytes)" \ +rex send $STATE_ORACLE "registerAssertionAdopter(address,address,bytes)" \ $OWNABLE_TARGET $ADMIN_VERIFIER "0x" \ - --private-key $PK --rpc-url http://localhost:1729 + -k $PK --rpc-url http://localhost:1729 # Add the assertion -cast send $STATE_ORACLE "addAssertion(address,bytes32,address,bytes,bytes)" \ +rex send $STATE_ORACLE "addAssertion(address,bytes32,address,bytes,bytes)" \ $OWNABLE_TARGET $ASSERTION_ID $DA_VERIFIER "0x" $DA_SIG \ - --private-key $PK --rpc-url http://localhost:1729 + -k $PK --rpc-url http://localhost:1729 # Verify -cast call $STATE_ORACLE "hasAssertion(address,bytes32)(bool)" \ +rex call $STATE_ORACLE "hasAssertion(address,bytes32)(bool)" \ $OWNABLE_TARGET $ASSERTION_ID --rpc-url http://localhost:1729 # Should return: true ``` @@ -391,16 +393,16 @@ PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 OWNABLE_TARGET=
# Check current owner -cast call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 +rex call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 # Try to transfer ownership (this should time out — tx never included!) -timeout 25 cast send $OWNABLE_TARGET "transferOwnership(address)" \ +timeout 25 rex send $OWNABLE_TARGET "transferOwnership(address)" \ 0x0000000000000000000000000000000000000001 \ - --private-key $PK --rpc-url http://localhost:1729 + -k $PK --rpc-url http://localhost:1729 # Expected: times out with no output (tx was dropped by Credible Layer) # Verify owner is UNCHANGED -cast call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 +rex call $OWNABLE_TARGET "owner()" --rpc-url http://localhost:1729 # Should still be the original deployer address ``` @@ -419,8 +421,8 @@ docker logs credible-sidecar 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep "assertion_ Send a non-protected call (`doSomething()`). The sidecar allows it through: ```bash -cast send $OWNABLE_TARGET "doSomething()" \ - --private-key $PK --rpc-url http://localhost:1729 +rex send $OWNABLE_TARGET "doSomething()" \ + -k $PK --rpc-url http://localhost:1729 # Expected: status 1 (success), included in a block ``` From 423ab4dc6213c296d96054b2b677b30d125f0ab8 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 19:47:00 -0300 Subject: [PATCH 09/33] Replace cast send --create with rex deploy --contract-path in the guide rex deploy can compile and deploy Solidity directly, no need for manual solc compilation + cast send --create. --- docs/l2/credible_layer.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index ceca5bfa545..ba42136fb2e 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -217,16 +217,14 @@ docker run -d --name assertion-da -p 5001:5001 \ ### Step 6: Deploy OwnableTarget test contract ```bash -cd /crates/l2/contracts/src/credible_layer -solc --bin OwnableTarget.sol -o /tmp/cb_compiled --overwrite - PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 -BYTECODE=0x$(cat /tmp/cb_compiled/OwnableTarget.bin) -cast send --private-key $PK --rpc-url http://localhost:1729 --create "$BYTECODE" +rex deploy \ + --contract-path /crates/l2/contracts/src/credible_layer/OwnableTarget.sol \ + --private-key $PK --rpc-url http://localhost:1729 --print-address ``` -Note the `contractAddress` from the output. Example: `0x00c042c4d5d913277ce16611a2ce6e9003554ad5` +Note the contract address from the output. Example: `0x00c042c4d5d913277ce16611a2ce6e9003554ad5` ### Step 7: Upload assertion to DA and register on State Oracle From 5d8a1dd872b20f2ff00e2ee48d6f241dda0a9498 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 15 Apr 2026 19:47:41 -0300 Subject: [PATCH 10/33] Prefer pcl build over forge build for assertion compilation --- docs/l2/credible_layer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index ba42136fb2e..fb8fc6d4dc8 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -233,7 +233,7 @@ Build the assertion using the [credible-layer-starter](https://github.com/phylax ```bash git clone --recurse-submodules https://github.com/phylaxsystems/credible-layer-starter /tmp/credible-layer-starter cd /tmp/credible-layer-starter -forge build # or: pcl build +pcl build # or: forge build ``` Submit the assertion's **creation bytecode** to the DA server: From 7356d5b40d2ccad401e52b7104e6f8631c2f9353 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Thu, 16 Apr 2026 11:21:18 -0300 Subject: [PATCH 11/33] Remove Aeges mempool pre-filter integration (no public server or documentation exists) Deletes the Aeges gRPC client module (aeges.rs), its proto definition (aeges.proto), the --credible-layer-aeges-url CLI flag, the aeges_url field from CredibleLayerConfig, the MempoolFilter type and mempool_filter field from RpcApiContext, the build_aeges_filter function from initializers, and all related wiring in start_api. Updates docs/CLI.md, docs/l2/credible_layer.md, and the Makefile init-l2-credible-layer target accordingly. --- cmd/ethrex/l2/initializers.rs | 108 --------- cmd/ethrex/l2/options.rs | 10 - crates/l2/Makefile | 1 - crates/l2/build.rs | 2 +- crates/l2/networking/rpc/lib.rs | 3 +- crates/l2/networking/rpc/rpc.rs | 47 +--- crates/l2/proto/aeges.proto | 67 ------ crates/l2/sequencer/configs.rs | 4 +- crates/l2/sequencer/credible_layer/aeges.rs | 233 -------------------- crates/l2/sequencer/credible_layer/mod.rs | 7 - docs/CLI.md | 6 - docs/l2/credible_layer.md | 12 - 12 files changed, 4 insertions(+), 496 deletions(-) delete mode 100644 crates/l2/proto/aeges.proto delete mode 100644 crates/l2/sequencer/credible_layer/aeges.rs diff --git a/cmd/ethrex/l2/initializers.rs b/cmd/ethrex/l2/initializers.rs index 73d88380098..444274bddff 100644 --- a/cmd/ethrex/l2/initializers.rs +++ b/cmd/ethrex/l2/initializers.rs @@ -53,7 +53,6 @@ fn init_rpc_api( l2_gas_limit: u64, ws_addr: Option, new_heads_sender: Option>, - mempool_filter: Option, ) { init_datadir(&opts.datadir); @@ -76,7 +75,6 @@ fn init_rpc_api( l2_gas_limit, l2_opts.sponsored_gas_limit, new_heads_sender, - mempool_filter, ); tracker.spawn(rpc_api); @@ -105,103 +103,6 @@ fn get_valid_delegation_addresses(l2_opts: &L2Options) -> Vec
{ addresses } -/// Build an Aeges mempool pre-filter callback from a gRPC endpoint URL. -/// -/// Connects to the Aeges service and wraps the client in a `MempoolFilter` closure -/// that accepts raw transaction bytes and returns `true` if the transaction is allowed. -/// Returns `None` if no URL is provided or if the connection fails (permissive on error). -async fn build_aeges_filter(aeges_url: Option) -> Option { - use ethrex_common::types::Transaction as EthrexTx; - use ethrex_crypto::NativeCrypto; - use ethrex_l2::sequencer::credible_layer::{AegesClient, aeges::AegesConfig}; - use ethrex_l2::sequencer::credible_layer::aeges_proto::{ - AccessListEntry, Transaction as AegesTransaction, - }; - use ethrex_rlp::decode::RLPDecode; - use std::sync::Arc; - - let url = aeges_url?; - let config = AegesConfig { - aeges_url: url, - ..Default::default() - }; - match AegesClient::connect(config).await { - Ok(client) => { - tracing::info!("Aeges mempool pre-filter connected"); - let client = Arc::new(client); - let filter: ethrex_l2_rpc::MempoolFilter = Arc::new(move |raw: bytes::Bytes| { - let client = client.clone(); - Box::pin(async move { - // Decode the raw tx bytes into an ethrex Transaction. - let tx = match EthrexTx::decode_unfinished(&raw) - .map(|(tx, _)| tx) - .or_else(|_| { - // Try stripping the type prefix byte and decoding. - raw.first() - .filter(|&&b| b <= 0x7f) - .and_then(|_| { - EthrexTx::decode_unfinished(&raw[1..]) - .map(|(tx, _)| tx) - .ok() - }) - .ok_or(ethrex_rlp::error::RLPDecodeError::InvalidLength) - }) { - Ok(tx) => tx, - Err(_) => return true, // Permissive: admit if we can't decode - }; - - let tx_hash = tx.hash(); - let sender = tx.sender(&NativeCrypto).unwrap_or_default(); - let value_bytes = tx.value().to_big_endian(); - let to = match tx.to() { - ethrex_common::types::TxKind::Call(addr) => Some(addr.as_bytes().to_vec()), - ethrex_common::types::TxKind::Create => None, - }; - let access_list = tx - .access_list() - .iter() - .map(|(addr, keys)| AccessListEntry { - address: addr.as_bytes().to_vec(), - storage_keys: keys.iter().map(|k| k.as_bytes().to_vec()).collect(), - }) - .collect(); - #[allow(clippy::as_conversions)] - let aeges_tx = AegesTransaction { - hash: tx_hash.as_bytes().to_vec(), - sender: sender.as_bytes().to_vec(), - to, - value: value_bytes.to_vec(), - nonce: tx.nonce(), - r#type: u8::from(tx.tx_type()) as u32, - chain_id: tx.chain_id(), - payload: tx.data().to_vec(), - gas_limit: tx.gas_limit(), - gas_price: None, - max_fee_per_gas: tx.max_fee_per_gas(), - max_priority_fee_per_gas: tx.max_priority_fee(), - max_fee_per_blob_gas: None, - access_list, - versioned_hashes: tx - .blob_versioned_hashes() - .iter() - .map(|h| h.as_bytes().to_vec()) - .collect(), - code_delegation_list: vec![], - }; - client.verify_transaction(aeges_tx).await - }) - }); - Some(filter) - } - Err(e) => { - tracing::warn!( - "Failed to connect to Aeges service: {e}. Proceeding without pre-filter." - ); - None - } - } -} - pub async fn init_rollup_store(datadir: &Path) -> StoreRollup { #[cfg(feature = "l2-sql")] let engine_type = EngineTypeRollup::SQL; @@ -441,14 +342,6 @@ pub async fn init_l2( (None, None) }; - // Build the Aeges mempool pre-filter if configured. - let mempool_filter = build_aeges_filter( - opts.sequencer_opts - .credible_layer_opts - .credible_layer_aeges_url - .clone(), - ) - .await; init_rpc_api( &opts.node_opts, &opts, @@ -464,7 +357,6 @@ pub async fn init_l2( l2_gas_limit, ws_addr, new_heads_sender, - mempool_filter, ); // Initialize metrics if enabled diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index 674bf932af4..4a934f49dbe 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -299,7 +299,6 @@ impl TryFrom for SequencerConfig { credible_layer: if opts.credible_layer_opts.credible_layer { CredibleLayerConfig { sidecar_url: opts.credible_layer_opts.credible_layer_url, - aeges_url: opts.credible_layer_opts.credible_layer_aeges_url, state_oracle_address: opts.credible_layer_opts.credible_layer_state_oracle, } } else { @@ -1129,15 +1128,6 @@ pub struct CredibleLayerOptions { help_heading = "Credible Layer options" )] pub credible_layer_url: Option, - #[arg( - long = "credible-layer-aeges-url", - value_name = "URL", - env = "ETHREX_CREDIBLE_LAYER_AEGES_URL", - requires = "credible_layer", - help = "gRPC endpoint for the Aeges mempool pre-filter service (e.g. http://localhost:8080). When set, Aeges pre-filtering is enabled.", - help_heading = "Credible Layer options" - )] - pub credible_layer_aeges_url: Option, #[arg( long = "credible-layer-state-oracle", value_name = "ADDRESS", diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 8ddf9e00d4f..9f4a83f4ccc 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -304,7 +304,6 @@ init-l2-credible-layer: ## 🛡️ Start L2 with Credible Layer enabled cargo run --release --features l2 --manifest-path ../../Cargo.toml -- \ l2 \ --credible-layer-url http://localhost:50051 \ - --credible-layer-aeges-url http://localhost:8080 \ --l2.ws-enabled \ --l2.ws-port 1730 \ 2>&1 | tee /tmp/l2_cb.log diff --git a/crates/l2/build.rs b/crates/l2/build.rs index 9e5159c4f2c..3c7109f885f 100644 --- a/crates/l2/build.rs +++ b/crates/l2/build.rs @@ -17,7 +17,7 @@ fn main() -> Result<(), Box> { // Compile Credible Layer protobuf definitions (client + server for mock sidecar) tonic_build::configure() .compile_protos( - &["proto/sidecar.proto", "proto/aeges.proto"], + &["proto/sidecar.proto"], &["proto/"], )?; diff --git a/crates/l2/networking/rpc/lib.rs b/crates/l2/networking/rpc/lib.rs index d6d2fc8351c..6fde551f31d 100644 --- a/crates/l2/networking/rpc/lib.rs +++ b/crates/l2/networking/rpc/lib.rs @@ -4,6 +4,5 @@ mod rpc; pub mod signer; pub mod utils; -pub use ethrex_rpc::NEW_HEADS_CHANNEL_CAPACITY; -pub use rpc::start_api; +pub use rpc::{NEW_HEADS_CHANNEL_CAPACITY, start_api}; pub use tokio::sync::broadcast; diff --git a/crates/l2/networking/rpc/rpc.rs b/crates/l2/networking/rpc/rpc.rs index d6debe4f75c..26a5517b6cc 100644 --- a/crates/l2/networking/rpc/rpc.rs +++ b/crates/l2/networking/rpc/rpc.rs @@ -51,15 +51,7 @@ use secp256k1::SecretKey; /// A value of 128 handles bursts without blocking block production. pub const NEW_HEADS_CHANNEL_CAPACITY: usize = 128; -/// Async callback for mempool pre-filtering (Aeges integration). -/// -/// Receives the raw transaction bytes and returns `true` if the transaction -/// should be admitted to the mempool, `false` if it should be rejected. -/// On any error or timeout the implementation should return `true` (permissive). -pub type MempoolFilter = - Arc std::pin::Pin + Send>>) + Send + Sync>; - -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct RpcApiContext { pub l1_ctx: ethrex_rpc::RpcApiContext, pub valid_delegation_addresses: Vec
, @@ -69,16 +61,6 @@ pub struct RpcApiContext { /// Broadcast sender for new block header notifications (eth_subscribe "newHeads"). /// `None` when the WS server is disabled. pub new_heads_sender: Option>, - /// Mempool pre-filter callback (Aeges integration). `None` when not configured. - pub mempool_filter: Option, -} - -impl std::fmt::Debug for RpcApiContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RpcApiContext") - .field("sponsored_gas_limit", &self.sponsored_gas_limit) - .finish_non_exhaustive() - } } pub trait RpcHandler: Sized { @@ -100,7 +82,6 @@ pub const FILTER_DURATION: Duration = { } }; -#[expect(clippy::too_many_arguments)] pub async fn start_api( http_addr: SocketAddr, ws_addr: Option, @@ -124,9 +105,6 @@ pub async fn start_api( // here. The same sender clone should be given to the block producer so it // can publish headers after sealing each block. new_heads_sender: Option>, - // Optional mempool pre-filter (Aeges integration). When `Some`, every - // `eth_sendRawTransaction` call is checked before mempool admission. - mempool_filter: Option, ) -> Result<(), RpcErr> { // TODO: Refactor how filters are handled, // filters are used by the filters endpoints (eth_newFilter, eth_getFilterChanges, ...etc) @@ -163,7 +141,6 @@ pub async fn start_api( rollup_store, sponsored_gas_limit, new_heads_sender, - mempool_filter, }; // Periodically clean up the active filters for the filters endpoints. @@ -498,28 +475,6 @@ pub async fn map_eth_requests(req: &RpcRequest, context: RpcApiContext) -> Resul "EIP-4844 transactions are not supported in the L2".to_string(), )); } - // Task 3.2/3.3: Check with Aeges before admitting to mempool. - // Privileged transactions are added directly by the L1Watcher, never via - // eth_sendRawTransaction, so all transactions here are regular user txs. - if let Some(filter) = &context.mempool_filter { - // Extract the raw transaction bytes from the first param. - let raw_bytes = req - .params - .as_deref() - .and_then(|p| p.first()) - .and_then(|v| v.as_str()) - .map(|hex| { - let hex = hex.strip_prefix("0x").unwrap_or(hex); - hex::decode(hex).unwrap_or_default() - }) - .unwrap_or_default(); - if !filter(raw_bytes.into()).await { - debug!("Aeges pre-filter rejected transaction"); - return Err(RpcErr::InvalidEthrexL2Message( - "Transaction rejected by Aeges pre-filter".to_string(), - )); - } - } SendRawTransactionRequest::call(req, context.l1_ctx) .await .map_err(RpcErr::L1RpcErr) diff --git a/crates/l2/proto/aeges.proto b/crates/l2/proto/aeges.proto deleted file mode 100644 index 906f19d6d10..00000000000 --- a/crates/l2/proto/aeges.proto +++ /dev/null @@ -1,67 +0,0 @@ -syntax = "proto3"; - -package aeges.v1; - -// Aeges service for pre-execution transaction filtering. -service AegesService { - // Unary: plugin sends a single transaction, server responds with a - // denied/allowed verdict. - rpc VerifyTransaction(VerifyTransactionRequest) returns (VerifyTransactionResponse); -} - -// Mirrors the Besu Transaction interface fields. -message Transaction { - // Core fields. - bytes hash = 1; // 32 bytes - bytes sender = 2; // 20 bytes - optional bytes to = 3; // 20 bytes, absent for contract creation - bytes value = 4; // big-endian uint256 - uint64 nonce = 5; - uint32 type = 6; // TransactionType ordinal - optional uint64 chain_id = 7; - - // Payload. - bytes payload = 8; // calldata (calls) or init code (creates) - - // Gas. - uint64 gas_limit = 9; - optional uint64 gas_price = 10; // legacy/EIP-2930 - optional uint64 max_fee_per_gas = 11; // EIP-1559+ - optional uint64 max_priority_fee_per_gas = 12; // EIP-1559+ - optional uint64 max_fee_per_blob_gas = 13; // EIP-4844 - - // EIP-2930 access list. - repeated AccessListEntry access_list = 14; - - // EIP-4844 blob versioned hashes. - repeated bytes versioned_hashes = 15; // each 32 bytes - - // EIP-7702 code delegations. - repeated CodeDelegation code_delegation_list = 16; -} - -message AccessListEntry { - bytes address = 1; // 20 bytes - repeated bytes storage_keys = 2; // each 32 bytes -} - -message CodeDelegation { - uint64 chain_id = 1; - bytes address = 2; // 20 bytes - uint64 nonce = 3; - uint32 v = 4; // y-parity (0 or 1) - bytes r = 5; // 32 bytes - bytes s = 6; // 32 bytes -} - -message VerifyTransactionRequest { - // Client-provided correlation ID, echoed back in the response. - uint64 event_id = 1; - Transaction transaction = 2; -} - -message VerifyTransactionResponse { - // Echoed from the request. - uint64 event_id = 1; - bool denied = 2; -} diff --git a/crates/l2/sequencer/configs.rs b/crates/l2/sequencer/configs.rs index 15e8b0381a0..c36b3f5d4cb 100644 --- a/crates/l2/sequencer/configs.rs +++ b/crates/l2/sequencer/configs.rs @@ -21,13 +21,11 @@ pub struct SequencerConfig { } /// Configuration for the Credible Layer sidecar integration. -/// Both URLs are optional; if absent, the feature is disabled. +/// URL is optional; if absent, the feature is disabled. #[derive(Clone, Debug, Default)] pub struct CredibleLayerConfig { /// gRPC endpoint for the Credible Layer Assertion Enforcer sidecar. pub sidecar_url: Option, - /// gRPC endpoint for the Aeges mempool pre-filter service. - pub aeges_url: Option, /// Address of the already-deployed State Oracle contract on L2. /// Required by the Credible Layer sidecar for assertion registry lookups. /// Deploy the State Oracle separately using the Phylax toolchain before starting ethrex. diff --git a/crates/l2/sequencer/credible_layer/aeges.rs b/crates/l2/sequencer/credible_layer/aeges.rs deleted file mode 100644 index 6a132c87119..00000000000 --- a/crates/l2/sequencer/credible_layer/aeges.rs +++ /dev/null @@ -1,233 +0,0 @@ -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::Duration; - -use tonic::transport::Channel; -use tracing::{debug, info, warn}; - -use super::aeges_proto::{ - aeges_service_client::AegesServiceClient, VerifyTransactionRequest, - Transaction as AegesTransaction, -}; -use super::errors::CredibleLayerError; - -/// Configuration for the Aeges mempool pre-filter. -#[derive(Debug, Clone)] -pub struct AegesConfig { - /// gRPC endpoint URL for the Aeges service - pub aeges_url: String, - /// Timeout for the VerifyTransaction call - pub timeout: Duration, -} - -impl Default for AegesConfig { - fn default() -> Self { - Self { - aeges_url: "http://localhost:8080".to_string(), - timeout: Duration::from_millis(200), - } - } -} - -/// gRPC client for the Aeges mempool pre-filter service. -/// -/// Validates transactions before mempool admission via a simple unary RPC. -/// On any error or timeout, the transaction is admitted (permissive behavior). -pub struct AegesClient { - config: AegesConfig, - client: AegesServiceClient, - event_id_counter: AtomicU64, -} - -impl AegesClient { - /// Connect to the Aeges service. - pub async fn connect(config: AegesConfig) -> Result { - info!(url = %config.aeges_url, "Connecting to Aeges service"); - - let channel = Channel::from_shared(config.aeges_url.clone()) - .map_err(|e| CredibleLayerError::Internal(format!("Invalid Aeges URL: {e}")))? - .connect() - .await?; - - let client = AegesServiceClient::new(channel); - - info!("Connected to Aeges service"); - - Ok(Self { - config, - client, - event_id_counter: AtomicU64::new(1), - }) - } - - /// Verify a transaction with the Aeges service. - /// - /// Returns `true` if the transaction should be admitted to the mempool, - /// `false` if it should be rejected. - /// On any error or timeout, returns `true` (permissive behavior). - pub async fn verify_transaction(&self, transaction: AegesTransaction) -> bool { - let event_id = self.event_id_counter.fetch_add(1, Ordering::Relaxed); - - let request = VerifyTransactionRequest { - event_id, - transaction: Some(transaction), - }; - - let result = tokio::time::timeout(self.config.timeout, { - let mut client = self.client.clone(); - async move { client.verify_transaction(request).await } - }) - .await; - - match result { - Ok(Ok(response)) => { - let denied = response.into_inner().denied; - if denied { - debug!(event_id, "Aeges denied transaction"); - } - !denied - } - Ok(Err(status)) => { - warn!(%status, "Aeges VerifyTransaction failed, admitting tx (permissive)"); - true - } - Err(_) => { - warn!("Aeges VerifyTransaction timed out, admitting tx (permissive)"); - true - } - } - } -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use super::*; - use crate::sequencer::credible_layer::aeges_proto::{ - Transaction as AegesTransaction, VerifyTransactionRequest, VerifyTransactionResponse, - }; - - // ── AegesConfig defaults ───────────────────────────────────────────────── - - #[test] - fn config_default_url_is_localhost_8080() { - let cfg = AegesConfig::default(); - assert_eq!(cfg.aeges_url, "http://localhost:8080"); - } - - #[test] - fn config_default_timeout_is_200ms() { - let cfg = AegesConfig::default(); - assert_eq!(cfg.timeout, Duration::from_millis(200)); - } - - // ── VerifyTransactionResponse logic ───────────────────────────────────── - - /// Simulate the core result-mapping logic from `verify_transaction` without - /// needing a real gRPC connection. - fn admission_from_response(response: VerifyTransactionResponse) -> bool { - !response.denied - } - - #[test] - fn admitted_when_denied_is_false() { - let resp = VerifyTransactionResponse { - event_id: 1, - denied: false, - }; - assert!(admission_from_response(resp)); - } - - #[test] - fn rejected_when_denied_is_true() { - let resp = VerifyTransactionResponse { - event_id: 2, - denied: true, - }; - assert!(!admission_from_response(resp)); - } - - // ── AegesTransaction construction ──────────────────────────────────────── - - #[test] - fn aeges_transaction_fields_roundtrip() { - let sender = vec![0xaau8; 20]; - let tx_hash = vec![0xbbu8; 32]; - let value = vec![0u8; 32]; - - let tx = AegesTransaction { - hash: tx_hash.clone(), - sender: sender.clone(), - to: None, - value: value.clone(), - nonce: 5, - r#type: 2, - chain_id: Some(1), - payload: vec![], - gas_limit: 21_000, - gas_price: None, - max_fee_per_gas: Some(1_000_000_000), - max_priority_fee_per_gas: Some(100_000_000), - max_fee_per_blob_gas: None, - access_list: vec![], - versioned_hashes: vec![], - code_delegation_list: vec![], - }; - - assert_eq!(tx.hash, tx_hash); - assert_eq!(tx.sender, sender); - assert_eq!(tx.nonce, 5); - assert_eq!(tx.gas_limit, 21_000); - assert!(tx.to.is_none()); - } - - // ── VerifyTransactionRequest construction ──────────────────────────────── - - #[test] - fn verify_request_includes_event_id_and_transaction() { - let tx = AegesTransaction { - hash: vec![1u8; 32], - sender: vec![2u8; 20], - to: Some(vec![3u8; 20]), - value: vec![0u8; 32], - nonce: 0, - r#type: 0, - chain_id: None, - payload: vec![], - gas_limit: 21_000, - gas_price: Some(1_000_000_000), - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - max_fee_per_blob_gas: None, - access_list: vec![], - versioned_hashes: vec![], - code_delegation_list: vec![], - }; - - let req = VerifyTransactionRequest { - event_id: 99, - transaction: Some(tx), - }; - - assert_eq!(req.event_id, 99); - assert!(req.transaction.is_some()); - } - - // ── Permissive behavior on error ───────────────────────────────────────── - - #[test] - fn permissive_behavior_on_grpc_error() { - // Simulate the Err branch: gRPC call returned a status error. - // The match arm returns `true` (admit the tx). - let simulated_result: Result, tonic::Status>, tokio::time::error::Elapsed> = - Ok(Err(tonic::Status::internal("server error"))); - - let admitted = match simulated_result { - Ok(Ok(response)) => !response.into_inner().denied, - Ok(Err(_status)) => true, - Err(_) => true, - }; - - assert!(admitted, "should admit tx when gRPC returns an error status"); - } -} diff --git a/crates/l2/sequencer/credible_layer/mod.rs b/crates/l2/sequencer/credible_layer/mod.rs index 7163c3420b9..c8e28cdfb9a 100644 --- a/crates/l2/sequencer/credible_layer/mod.rs +++ b/crates/l2/sequencer/credible_layer/mod.rs @@ -7,19 +7,12 @@ /// The integration is opt-in via the `--credible-layer-url` CLI flag. /// When disabled, there is zero overhead. pub mod client; -pub mod aeges; pub mod errors; pub use client::CredibleLayerClient; -pub use aeges::AegesClient; pub use errors::CredibleLayerError; /// Generated protobuf/gRPC types for sidecar.proto pub mod sidecar_proto { tonic::include_proto!("sidecar.transport.v1"); } - -/// Generated protobuf/gRPC types for aeges.proto -pub mod aeges_proto { - tonic::include_proto!("aeges.v1"); -} diff --git a/docs/CLI.md b/docs/CLI.md index fef5acbf3a2..5a89fd7d9a2 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -692,12 +692,6 @@ Credible Layer options: [env: ETHREX_CREDIBLE_LAYER_URL=] - --credible-layer-aeges-url - gRPC endpoint for the Aeges mempool pre-filter service. - When set, transactions are validated before mempool admission. - - [env: ETHREX_CREDIBLE_LAYER_AEGES_URL=] - Monitor options: --no-monitor [env: ETHREX_NO_MONITOR=] diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index fb8fc6d4dc8..b178c8ef016 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -13,7 +13,6 @@ Credible Layer integrates ethrex L2 with [Phylax Systems' Credible Layer](https: - **Assertion**: A Solidity contract that defines a security invariant (e.g., "ownership of contract X must not change"). Written using the [`credible-std`](https://github.com/phylaxsystems/credible-std) library. - **Assertion Enforcer (Sidecar)**: A separate process that receives candidate transactions from the block builder, simulates them, runs applicable assertions through the PhEVM, and returns pass/fail verdicts. - **State Oracle**: An on-chain contract that maps protected contracts to their assertions. -- **Aeges**: An optional mempool pre-filter that rejects transactions before they enter the pool. - **Permissive on failure**: If the sidecar is unreachable or times out, transactions are included anyway. Liveness is prioritized over safety. ### Communication Protocol @@ -26,12 +25,6 @@ ethrex communicates with the sidecar via **gRPC** using the protocol defined in | `SubscribeResults` | Server stream | Receive transaction verdicts as they complete | | `GetTransaction` | Unary | Fallback polling for a single result | -The Aeges pre-filter uses [`aeges.proto`](../../crates/l2/proto/aeges.proto): - -| RPC | Type | Purpose | -|-----|------|---------| -| `VerifyTransaction` | Unary | Check if a transaction should be admitted to the mempool | - ### Block Building Flow Per block: @@ -56,13 +49,10 @@ Privileged transactions (L1→L2 deposits) bypass the Credible Layer in this fir |------|------| | `crates/l2/sequencer/credible_layer/mod.rs` | Module root, proto imports | | `crates/l2/sequencer/credible_layer/client.rs` | `CredibleLayerClient` — gRPC client for the sidecar; handles block building validation (StreamEvents, GetTransaction) | -| `crates/l2/sequencer/credible_layer/aeges.rs` | `AegesClient` — gRPC client for the Aeges mempool pre-filter; validates transactions before pool admission (VerifyTransaction) | | `crates/l2/sequencer/credible_layer/errors.rs` | Error types | | `crates/l2/proto/sidecar.proto` | Sidecar gRPC protocol definition | -| `crates/l2/proto/aeges.proto` | Aeges gRPC protocol definition | | `crates/l2/sequencer/block_producer.rs` | Block producer — sends CommitHead + NewIteration | | `crates/l2/sequencer/block_producer/payload_builder.rs` | Transaction selection — sends Transaction events | -| `crates/l2/networking/rpc/rpc.rs` | L2 RPC — WebSocket server + Aeges mempool filter | | `crates/networking/rpc/tracing.rs` | prestateTracer implementation | ### Configuration @@ -73,7 +63,6 @@ CLI flags: |------|-------------|---------| | `--credible-layer` | Enable the Credible Layer integration (required gate flag) | `false` | | `--credible-layer-url` | gRPC endpoint for the sidecar (requires `--credible-layer`) | (none) | -| `--credible-layer-aeges-url` | gRPC endpoint for Aeges pre-filter (requires `--credible-layer`) | (none) | | `--credible-layer-state-oracle` | Address of the deployed State Oracle contract on L2 (requires `--credible-layer`) | (none) | When `--credible-layer` is not set, Credible Layer is completely disabled with zero overhead. @@ -466,4 +455,3 @@ rm -rf /tmp/credible-layer-contracts /tmp/credible-layer-starter /tmp/sidecar-in - [Besu Plugin Reference](https://github.com/phylaxsystems/credible-layer-besu-plugin) - [credible-sdk (sidecar source)](https://github.com/phylaxsystems/credible-sdk) - [sidecar.proto](../../crates/l2/proto/sidecar.proto) -- [aeges.proto](../../crates/l2/proto/aeges.proto) From 9d8569bef6fcef5572848aa0d08a1228acce4572 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Thu, 16 Apr 2026 11:37:45 -0300 Subject: [PATCH 12/33] Move eth_subscribe/eth_unsubscribe WebSocket subscription support from L2 to L1 RPC crate. The subscription functionality (newHeads) is standard Ethereum behavior that belongs in the base execution client, not L2-specific code. The L1 RpcApiContext now carries a new_heads_sender field, and the subscription functions (handle_eth_subscribe, handle_eth_unsubscribe, generate_subscription_id, drain_subscriptions, build_subscription_notification) and the NEW_HEADS_CHANNEL_CAPACITY constant are defined in the L1 crate and exported from its lib.rs. The L1 handle_websocket is upgraded from a simple request-response loop to a select!-based loop that multiplexes incoming WS messages with subscription notification draining. The L2 WS handler is simplified to delegate eth_subscribe and eth_unsubscribe to L1's implementations (via context.l1_ctx), eliminating the duplicate code. The L2 lib.rs re-exports NEW_HEADS_CHANNEL_CAPACITY from ethrex_rpc for backward compatibility with existing callers (cmd/ethrex/l2/initializers.rs). The L2 tests that covered the subscription utilities are updated to reference the L1 implementations directly. --- cmd/ethrex/initializers.rs | 2 + crates/l2/networking/rpc/lib.rs | 3 +- crates/l2/networking/rpc/rpc.rs | 166 ++++------------------------ crates/networking/rpc/rpc.rs | 9 ++ crates/networking/rpc/test_utils.rs | 4 + 5 files changed, 38 insertions(+), 146 deletions(-) diff --git a/cmd/ethrex/initializers.rs b/cmd/ethrex/initializers.rs index 8a14fc23751..2e91f900cef 100644 --- a/cmd/ethrex/initializers.rs +++ b/cmd/ethrex/initializers.rs @@ -230,6 +230,8 @@ pub async fn init_rpc_api( opts.gas_limit, opts.extra_data.clone(), None, + #[cfg(feature = "eip-8025")] + proof_coordinator, ); tracker.spawn(rpc_api); diff --git a/crates/l2/networking/rpc/lib.rs b/crates/l2/networking/rpc/lib.rs index 6fde551f31d..d6d2fc8351c 100644 --- a/crates/l2/networking/rpc/lib.rs +++ b/crates/l2/networking/rpc/lib.rs @@ -4,5 +4,6 @@ mod rpc; pub mod signer; pub mod utils; -pub use rpc::{NEW_HEADS_CHANNEL_CAPACITY, start_api}; +pub use ethrex_rpc::NEW_HEADS_CHANNEL_CAPACITY; +pub use rpc::start_api; pub use tokio::sync::broadcast; diff --git a/crates/l2/networking/rpc/rpc.rs b/crates/l2/networking/rpc/rpc.rs index 26a5517b6cc..102c59ad278 100644 --- a/crates/l2/networking/rpc/rpc.rs +++ b/crates/l2/networking/rpc/rpc.rs @@ -39,7 +39,7 @@ use tokio::{ sync::{Mutex as TokioMutex, broadcast}, }; use tower_http::cors::CorsLayer; -use tracing::{debug, info, warn}; +use tracing::{debug, info}; use tracing_subscriber::{EnvFilter, Registry, reload}; use crate::l2::transaction::SponsoredTx; @@ -47,10 +47,6 @@ use ethrex_common::Address; use ethrex_storage_rollup::StoreRollup; use secp256k1::SecretKey; -/// Broadcast channel capacity for new block header notifications. -/// A value of 128 handles bursts without blocking block production. -pub const NEW_HEADS_CHANNEL_CAPACITY: usize = 128; - #[derive(Clone, Debug)] pub struct RpcApiContext { pub l1_ctx: ethrex_rpc::RpcApiContext, @@ -58,9 +54,6 @@ pub struct RpcApiContext { pub sponsor_pk: SecretKey, pub rollup_store: StoreRollup, pub sponsored_gas_limit: u64, - /// Broadcast sender for new block header notifications (eth_subscribe "newHeads"). - /// `None` when the WS server is disabled. - pub new_heads_sender: Option>, } pub trait RpcHandler: Sized { @@ -134,13 +127,14 @@ pub async fn start_api( log_filter_handler, gas_ceil: l2_gas_limit, block_worker_channel, - new_heads_sender: new_heads_sender.clone(), + new_heads_sender, + #[cfg(feature = "eip-8025")] + proof_coordinator: None, }, valid_delegation_addresses, sponsor_pk, rollup_store, sponsored_gas_limit, - new_heads_sender, }; // Periodically clean up the active filters for the filters endpoints. @@ -235,10 +229,9 @@ async fn handle_http_request( /// /// Supports eth_subscribe / eth_unsubscribe for "newHeads" in addition to /// regular JSON-RPC request-response calls that work the same as over HTTP. +/// Subscription functionality is provided by ethrex_rpc (L1 crate). async fn handle_websocket(mut socket: WebSocket, context: RpcApiContext) { // subscription_id -> broadcast::Receiver - // We store only one receiver per subscription ID; senders are cloned from - // context.new_heads_sender when a subscription is created. let mut subscriptions: HashMap> = HashMap::new(); // Channel for the write loop to receive outbound messages. let (out_tx, mut out_rx) = tokio::sync::mpsc::unbounded_channel::(); @@ -260,15 +253,15 @@ async fn handle_websocket(mut socket: WebSocket, context: RpcApiContext) { }; let response = handle_ws_request(&body, &context, &mut subscriptions, &out_tx).await; - if let Some(resp) = response { - if socket.send(Message::Text(resp.into())).await.is_err() { - break; - } + if let Some(resp) = response + && socket.send(Message::Text(resp.into())).await.is_err() + { + break; } } // Push subscription notifications for all active subscriptions. - _ = drain_subscriptions(&mut subscriptions, &out_tx) => {} + _ = ethrex_rpc::drain_subscriptions(&mut subscriptions, &out_tx) => {} // Send any pending outbound messages (subscription notifications). Some(msg) = out_rx.recv() => { @@ -280,7 +273,7 @@ async fn handle_websocket(mut socket: WebSocket, context: RpcApiContext) { } // Connection closed — subscriptions are dropped automatically when the - // HashMap goes out of scope (task 4.6). + // HashMap goes out of scope. } /// Process an incoming JSON-RPC request over WebSocket. @@ -308,12 +301,16 @@ async fn handle_ws_request( match req.method.as_str() { "eth_subscribe" => { - let result = handle_eth_subscribe(&req, context, subscriptions); + // Delegate to L1's implementation, which reads from context.l1_ctx.new_heads_sender. + let result = ethrex_rpc::handle_eth_subscribe(&req, &context.l1_ctx, subscriptions) + .map_err(RpcErr::L1RpcErr); let resp = ethrex_rpc::rpc_response(req.id, result).ok()?; Some(resp.to_string()) } "eth_unsubscribe" => { - let result = handle_eth_unsubscribe(&req, subscriptions); + // Delegate to L1's implementation. + let result = ethrex_rpc::handle_eth_unsubscribe(&req, subscriptions) + .map_err(RpcErr::L1RpcErr); let resp = ethrex_rpc::rpc_response(req.id, result).ok()?; Some(resp.to_string()) } @@ -325,130 +322,6 @@ async fn handle_ws_request( } } -/// Handle `eth_subscribe`. -/// -/// Only `"newHeads"` is supported (task 4.7). Returns a hex subscription ID -/// on success or an error for unsupported subscription types. -fn handle_eth_subscribe( - req: &RpcRequest, - context: &RpcApiContext, - subscriptions: &mut HashMap>, -) -> Result { - // params[0] must be the subscription type string. - let params = req.params.as_deref().unwrap_or(&[]); - let sub_type = params - .first() - .and_then(|v| v.as_str()) - .ok_or_else(|| RpcErr::L1RpcErr(ethrex_rpc::RpcErr::BadParams( - "eth_subscribe requires a subscription type parameter".to_string(), - )))?; - - match sub_type { - "newHeads" => { - let sender = context.new_heads_sender.as_ref().ok_or_else(|| { - RpcErr::L1RpcErr(ethrex_rpc::RpcErr::Internal( - "WebSocket server not enabled".to_string(), - )) - })?; - - // Generate a unique subscription ID. - let sub_id = generate_subscription_id(); - - // Subscribe to the broadcast channel. - let receiver = sender.subscribe(); - subscriptions.insert(sub_id.clone(), receiver); - - Ok(Value::String(sub_id)) - } - other => Err(RpcErr::L1RpcErr(ethrex_rpc::RpcErr::Internal(format!( - "Unsupported subscription type: {other}" - )))), - } -} - -/// Handle `eth_unsubscribe`. -/// -/// Returns `true` if the subscription existed and was removed, `false` otherwise. -fn handle_eth_unsubscribe( - req: &RpcRequest, - subscriptions: &mut HashMap>, -) -> Result { - let params = req.params.as_deref().unwrap_or(&[]); - let sub_id = params - .first() - .and_then(|v| v.as_str()) - .ok_or_else(|| RpcErr::L1RpcErr(ethrex_rpc::RpcErr::BadParams( - "eth_unsubscribe requires a subscription ID parameter".to_string(), - )))?; - - let removed = subscriptions.remove(sub_id).is_some(); - Ok(Value::Bool(removed)) -} - -/// Generate a unique hex-encoded subscription ID. -fn generate_subscription_id() -> String { - use std::sync::atomic::{AtomicU64, Ordering}; - static COUNTER: AtomicU64 = AtomicU64::new(1); - let id = COUNTER.fetch_add(1, Ordering::Relaxed); - format!("0x{id:016x}") -} - -/// Drain any buffered messages from active subscriptions and send them to -/// the outbound channel. This is called from the `select!` loop to ensure -/// subscription notifications are forwarded promptly. -/// -/// Returns immediately after draining whatever is currently buffered; -/// the future resolves to `()` so the caller can combine it with other arms. -async fn drain_subscriptions( - subscriptions: &mut HashMap>, - out_tx: &tokio::sync::mpsc::UnboundedSender, -) { - // Collect subscription IDs to avoid borrow conflicts while iterating. - let sub_ids: Vec = subscriptions.keys().cloned().collect(); - for sub_id in sub_ids { - let Some(receiver) = subscriptions.get_mut(&sub_id) else { - continue; - }; - loop { - match receiver.try_recv() { - Ok(header) => { - let notification = build_subscription_notification(&sub_id, header); - if out_tx.send(notification).is_err() { - // Channel closed — connection is shutting down. - return; - } - } - Err(broadcast::error::TryRecvError::Empty) => break, - Err(broadcast::error::TryRecvError::Closed) => { - // Sender was dropped. - subscriptions.remove(&sub_id); - break; - } - Err(broadcast::error::TryRecvError::Lagged(n)) => { - warn!("eth_subscribe newHeads: subscription {sub_id} lagged by {n} messages"); - // Continue to catch up. - } - } - } - } - // Yield so that the select! loop can check other arms. - tokio::task::yield_now().await; -} - -/// Build the standard Ethereum subscription notification envelope: -/// `{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x...","result":{...}}}` -fn build_subscription_notification(sub_id: &str, result: Value) -> String { - serde_json::json!({ - "jsonrpc": "2.0", - "method": "eth_subscription", - "params": { - "subscription": sub_id, - "result": result, - } - }) - .to_string() -} - /// Handle requests that can come from either clients or other users pub async fn map_http_requests(req: &RpcRequest, context: RpcApiContext) -> Result { match resolve_namespace(&req.method) { @@ -513,8 +386,11 @@ pub async fn map_l2_requests(req: &RpcRequest, context: RpcApiContext) -> Result mod tests { use std::collections::HashMap; + use ethrex_rpc::{ + NEW_HEADS_CHANNEL_CAPACITY, broadcast, build_subscription_notification, + generate_subscription_id, handle_eth_unsubscribe, + }; use serde_json::{Value, json}; - use tokio::sync::broadcast; use super::*; diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index a125a4a5be7..505a9f4c046 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -212,6 +212,10 @@ pub struct RpcApiContext { /// Broadcast sender for new block header notifications (eth_subscribe "newHeads"). /// `None` when the WS server is disabled or subscriptions are not needed. pub new_heads_sender: Option>, + /// EIP-8025 proof coordinator handle for sending proof requests. + #[cfg(feature = "eip-8025")] + pub proof_coordinator: + Option, } impl std::fmt::Debug for RpcApiContext { @@ -496,6 +500,9 @@ pub async fn start_api( gas_ceil: u64, extra_data: String, new_heads_sender: Option>, + #[cfg(feature = "eip-8025")] proof_coordinator: Option< + ethrex_blockchain::proof_coordinator::coordinator::CoordinatorHandle, + >, ) -> Result<(), RpcErr> { // TODO: Refactor how filters are handled, // filters are used by the filters endpoints (eth_newFilter, eth_getFilterChanges, ...etc) @@ -519,6 +526,8 @@ pub async fn start_api( gas_ceil, block_worker_channel, new_heads_sender, + #[cfg(feature = "eip-8025")] + proof_coordinator, }; // Periodically clean up the active filters for the filters endpoints. diff --git a/crates/networking/rpc/test_utils.rs b/crates/networking/rpc/test_utils.rs index 7c4956ee1cf..dd4a5e17810 100644 --- a/crates/networking/rpc/test_utils.rs +++ b/crates/networking/rpc/test_utils.rs @@ -260,6 +260,8 @@ pub async fn start_test_api() -> tokio::task::JoinHandle<()> { DEFAULT_BUILDER_GAS_CEIL, String::new(), None, + #[cfg(feature = "eip-8025")] + None, ) .await .unwrap() @@ -295,6 +297,8 @@ pub async fn default_context_with_storage(storage: Store) -> RpcApiContext { gas_ceil: DEFAULT_BUILDER_GAS_CEIL, block_worker_channel, new_heads_sender: None, + #[cfg(feature = "eip-8025")] + proof_coordinator: None, } } From 4d7f11cb90077ab41e068d4ad29d6bd538b87662 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Thu, 16 Apr 2026 12:29:31 -0300 Subject: [PATCH 13/33] Apply cargo fmt formatting fixes after rebase onto feat/l1/ws-subscriptions. --- crates/l2/networking/rpc/rpc.rs | 4 ++-- crates/networking/rpc/tracing.rs | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/l2/networking/rpc/rpc.rs b/crates/l2/networking/rpc/rpc.rs index 102c59ad278..132770f616e 100644 --- a/crates/l2/networking/rpc/rpc.rs +++ b/crates/l2/networking/rpc/rpc.rs @@ -309,8 +309,8 @@ async fn handle_ws_request( } "eth_unsubscribe" => { // Delegate to L1's implementation. - let result = ethrex_rpc::handle_eth_unsubscribe(&req, subscriptions) - .map_err(RpcErr::L1RpcErr); + let result = + ethrex_rpc::handle_eth_unsubscribe(&req, subscriptions).map_err(RpcErr::L1RpcErr); let resp = ethrex_rpc::rpc_response(req.id, result).ok()?; Some(resp.to_string()) } diff --git a/crates/networking/rpc/tracing.rs b/crates/networking/rpc/tracing.rs index 594a393720c..14650866735 100644 --- a/crates/networking/rpc/tracing.rs +++ b/crates/networking/rpc/tracing.rs @@ -142,18 +142,11 @@ impl RpcHandler for TraceTransactionRequest { }; let (pre_trace, pre_post) = context .blockchain - .trace_transaction_prestate( - self.tx_hash, - reexec, - timeout, - config.diff_mode, - ) + .trace_transaction_prestate(self.tx_hash, reexec, timeout, config.diff_mode) .await .map_err(|err| RpcErr::Internal(err.to_string()))?; if config.diff_mode { - Ok(serde_json::to_value( - pre_post.unwrap_or_default(), - )?) + Ok(serde_json::to_value(pre_post.unwrap_or_default())?) } else { Ok(serde_json::to_value(pre_trace)?) } From c6e68f8f03e68dfc5e28dee96ac19891f6f156d1 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Fri, 17 Apr 2026 13:21:25 -0300 Subject: [PATCH 14/33] Remove unused mock-sidecar binary and disable server codegen in tonic_build. The mock sidecar was a standalone dev tool for testing the gRPC protocol without the real Phylax sidecar, but it was never referenced by any Makefile target, script, test, or documentation. --- crates/l2/Cargo.toml | 4 - crates/l2/build.rs | 6 +- .../credible_layer/mock_sidecar/main.rs | 224 ------------------ 3 files changed, 4 insertions(+), 230 deletions(-) delete mode 100644 crates/l2/sequencer/credible_layer/mock_sidecar/main.rs diff --git a/crates/l2/Cargo.toml b/crates/l2/Cargo.toml index 623302f5128..f665445be58 100644 --- a/crates/l2/Cargo.toml +++ b/crates/l2/Cargo.toml @@ -70,10 +70,6 @@ hex = { workspace = true } [lib] path = "./l2.rs" -[[bin]] -name = "mock-sidecar" -path = "sequencer/credible_layer/mock_sidecar/main.rs" - [lints.clippy] unwrap_used = "deny" expect_used = "deny" diff --git a/crates/l2/build.rs b/crates/l2/build.rs index ff15efed516..400c81bd384 100644 --- a/crates/l2/build.rs +++ b/crates/l2/build.rs @@ -14,8 +14,10 @@ fn main() -> Result<(), Box> { Emitter::default().add_instructions(&git2)?.emit()?; } - // Compile Credible Layer protobuf definitions (client + server for mock sidecar) - tonic_build::configure().compile_protos(&["proto/sidecar.proto"], &["proto/"])?; + // Compile Credible Layer protobuf definitions (client only) + tonic_build::configure() + .build_server(false) + .compile_protos(&["proto/sidecar.proto"], &["proto/"])?; Ok(()) } diff --git a/crates/l2/sequencer/credible_layer/mock_sidecar/main.rs b/crates/l2/sequencer/credible_layer/mock_sidecar/main.rs deleted file mode 100644 index 7d9d31d849f..00000000000 --- a/crates/l2/sequencer/credible_layer/mock_sidecar/main.rs +++ /dev/null @@ -1,224 +0,0 @@ -/// Mock Credible Layer Sidecar for end-to-end testing. -/// -/// Implements the sidecar.proto gRPC protocol and rejects any transaction -/// that calls the `transferOwnership(address)` function selector (0xf2fde38b). -/// -/// Usage: -/// cargo run --bin mock-sidecar -/// -/// The mock listens on 0.0.0.0:50051 (same as the real sidecar). -use std::collections::HashMap; -use std::pin::Pin; -use std::sync::Arc; - -use tokio::sync::{Mutex, mpsc}; -use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream}; -use tonic::{Request, Response, Status, transport::Server}; - -// Include the generated protobuf code -pub mod sidecar_proto { - tonic::include_proto!("sidecar.transport.v1"); -} - -use sidecar_proto::{ - Event, GetTransactionRequest, GetTransactionResponse, GetTransactionsRequest, - GetTransactionsResponse, ResultStatus, StreamAck, SubscribeResultsRequest, TransactionResult, - get_transaction_response::Outcome, - sidecar_transport_server::{SidecarTransport, SidecarTransportServer}, -}; - -/// The `transferOwnership(address)` function selector -const TRANSFER_OWNERSHIP_SELECTOR: [u8; 4] = [0xf2, 0xfd, 0xe3, 0x8b]; - -/// Shared state: stores transaction results keyed by tx_hash -type ResultStore = Arc, TransactionResult>>>; - -pub struct MockSidecar { - results: ResultStore, -} - -impl MockSidecar { - fn new() -> Self { - Self { - results: Arc::new(Mutex::new(HashMap::new())), - } - } -} - -/// Evaluate a transaction: returns ASSERTION_FAILED if it calls transferOwnership, -/// SUCCESS otherwise. -fn evaluate_tx(event: &sidecar_proto::Transaction) -> ResultStatus { - let calldata = event - .tx_env - .as_ref() - .map(|env| &env.data[..]) - .unwrap_or(&[]); - - if calldata.len() >= 4 && calldata[..4] == TRANSFER_OWNERSHIP_SELECTOR { - ResultStatus::AssertionFailed - } else { - ResultStatus::Success - } -} - -#[tonic::async_trait] -impl SidecarTransport for MockSidecar { - type StreamEventsStream = - Pin> + Send + 'static>>; - type SubscribeResultsStream = - Pin> + Send + 'static>>; - - async fn stream_events( - &self, - request: Request>, - ) -> Result, Status> { - let mut stream = request.into_inner(); - let (tx, rx) = mpsc::channel(128); - let results = self.results.clone(); - - tokio::spawn(async move { - let mut events_processed: u64 = 0; - while let Some(Ok(event)) = stream.next().await { - let event_id = event.event_id; - events_processed += 1; - - match &event.event { - Some(sidecar_proto::event::Event::CommitHead(ch)) => { - eprintln!("[MOCK] CommitHead: n_transactions={}", ch.n_transactions); - } - Some(sidecar_proto::event::Event::NewIteration(ni)) => { - eprintln!("[MOCK] NewIteration: iteration_id={}", ni.iteration_id); - } - Some(sidecar_proto::event::Event::Transaction(t)) => { - let tx_hash = t - .tx_execution_id - .as_ref() - .map(|id| id.tx_hash.clone()) - .unwrap_or_default(); - let tx_hash_hex = hex::encode(&tx_hash); - - let status = evaluate_tx(t); - - let result = TransactionResult { - tx_execution_id: t.tx_execution_id.clone(), - status: status as i32, - gas_used: 21000, - error: String::new(), - }; - - // Store the result so GetTransaction can find it - { - let mut store = results.lock().await; - store.insert(tx_hash.clone(), result); - } - - match status { - ResultStatus::AssertionFailed => { - eprintln!( - "[MOCK] TX {}: ASSERTION_FAILED (transferOwnership)", - &tx_hash_hex[..std::cmp::min(16, tx_hash_hex.len())] - ); - } - _ => { - eprintln!( - "[MOCK] TX {}: SUCCESS", - &tx_hash_hex[..std::cmp::min(16, tx_hash_hex.len())] - ); - } - } - } - Some(sidecar_proto::event::Event::Reorg(_)) => { - eprintln!("[MOCK] ReorgEvent received"); - } - None => {} - } - - let ack = StreamAck { - success: true, - message: String::new(), - events_processed, - event_id, - }; - if tx.send(Ok(ack)).await.is_err() { - break; - } - } - }); - - Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) - } - - async fn subscribe_results( - &self, - _request: Request, - ) -> Result, Status> { - // Return an empty stream — results are delivered via GetTransaction - let (_tx, rx) = mpsc::channel(1); - Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) - } - - async fn get_transactions( - &self, - _request: Request, - ) -> Result, Status> { - Ok(Response::new(GetTransactionsResponse { - results: vec![], - not_found: vec![], - })) - } - - async fn get_transaction( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let tx_hash = req - .tx_execution_id - .as_ref() - .map(|id| id.tx_hash.clone()) - .unwrap_or_default(); - - // Look up stored result from stream_events processing - let store = self.results.lock().await; - if let Some(result) = store.get(&tx_hash) { - let status_name = match ResultStatus::try_from(result.status) { - Ok(ResultStatus::AssertionFailed) => "ASSERTION_FAILED", - Ok(ResultStatus::Success) => "SUCCESS", - _ => "OTHER", - }; - eprintln!( - "[MOCK] GetTransaction {}: returning {}", - &hex::encode(&tx_hash)[..std::cmp::min(16, tx_hash.len() * 2)], - status_name - ); - Ok(Response::new(GetTransactionResponse { - outcome: Some(Outcome::Result(result.clone())), - })) - } else { - // Not found yet — the stream_events call may not have completed - eprintln!( - "[MOCK] GetTransaction {}: NOT_FOUND", - &hex::encode(&tx_hash)[..std::cmp::min(16, tx_hash.len() * 2)] - ); - Ok(Response::new(GetTransactionResponse { - outcome: Some(Outcome::NotFound(tx_hash)), - })) - } - } -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - let addr = "0.0.0.0:50051".parse()?; - eprintln!("[MOCK SIDECAR] Starting on {addr}"); - eprintln!( - "[MOCK SIDECAR] Will reject transactions calling transferOwnership(address) [0xf2fde38b]" - ); - - Server::builder() - .add_service(SidecarTransportServer::new(MockSidecar::new())) - .serve(addr) - .await?; - - Ok(()) -} From f194988701ee2673a971f192705ccd9370906809 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Fri, 17 Apr 2026 15:00:06 -0300 Subject: [PATCH 15/33] =?UTF-8?q?Remove=20eip-8025=20proof=5Fcoordinator?= =?UTF-8?q?=20from=20L1=20RPC=20context=20=E2=80=94=20it=20was=20incorrect?= =?UTF-8?q?ly=20preserved=20during=20the=20merge=20and=20is=20unrelated=20?= =?UTF-8?q?to=20the=20Credible=20Layer=20integration.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/ethrex/initializers.rs | 2 -- crates/l2/networking/rpc/rpc.rs | 2 -- crates/networking/rpc/rpc.rs | 9 --------- crates/networking/rpc/test_utils.rs | 4 ---- 4 files changed, 17 deletions(-) diff --git a/cmd/ethrex/initializers.rs b/cmd/ethrex/initializers.rs index 8cd526cf50f..e37c4562a18 100644 --- a/cmd/ethrex/initializers.rs +++ b/cmd/ethrex/initializers.rs @@ -233,8 +233,6 @@ pub async fn init_rpc_api( log_filter_handler, opts.gas_limit, opts.extra_data.clone(), - #[cfg(feature = "eip-8025")] - proof_coordinator, ); tracker.spawn(rpc_api); diff --git a/crates/l2/networking/rpc/rpc.rs b/crates/l2/networking/rpc/rpc.rs index ad6f5ee0f50..76bad0e2b58 100644 --- a/crates/l2/networking/rpc/rpc.rs +++ b/crates/l2/networking/rpc/rpc.rs @@ -120,8 +120,6 @@ pub async fn start_api( gas_ceil: l2_gas_limit, block_worker_channel, ws: ws.clone(), - #[cfg(feature = "eip-8025")] - proof_coordinator: None, }, valid_delegation_addresses, sponsor_pk, diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index 77c7f20ce5b..9c8c27497fb 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -213,10 +213,6 @@ pub struct RpcApiContext { pub block_worker_channel: UnboundedSender, /// WebSocket configuration. `None` when the WS server is disabled. pub ws: Option, - /// EIP-8025 proof coordinator handle for sending proof requests. - #[cfg(feature = "eip-8025")] - pub proof_coordinator: - Option, } /// Configuration for the WebSocket RPC server. @@ -505,9 +501,6 @@ pub async fn start_api( log_filter_handler: Option>, gas_ceil: u64, extra_data: String, - #[cfg(feature = "eip-8025")] proof_coordinator: Option< - ethrex_blockchain::proof_coordinator::coordinator::CoordinatorHandle, - >, ) -> Result<(), RpcErr> { // TODO: Refactor how filters are handled, // filters are used by the filters endpoints (eth_newFilter, eth_getFilterChanges, ...etc) @@ -531,8 +524,6 @@ pub async fn start_api( gas_ceil, block_worker_channel, ws: ws.clone(), - #[cfg(feature = "eip-8025")] - proof_coordinator, }; // Periodically clean up the active filters for the filters endpoints. diff --git a/crates/networking/rpc/test_utils.rs b/crates/networking/rpc/test_utils.rs index 40b5a627bc9..3428efa4ddb 100644 --- a/crates/networking/rpc/test_utils.rs +++ b/crates/networking/rpc/test_utils.rs @@ -258,8 +258,6 @@ pub async fn start_test_api() -> tokio::task::JoinHandle<()> { None, DEFAULT_BUILDER_GAS_CEIL, String::new(), - #[cfg(feature = "eip-8025")] - None, ) .await .unwrap() @@ -295,8 +293,6 @@ pub async fn default_context_with_storage(storage: Store) -> RpcApiContext { gas_ceil: DEFAULT_BUILDER_GAS_CEIL, block_worker_channel, ws: None, - #[cfg(feature = "eip-8025")] - proof_coordinator: None, } } From 5ab6c450ae8ebf9951e0a7fca480d34074ebf9e9 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 14:40:41 -0300 Subject: [PATCH 16/33] Refactor Credible Layer integration: rewrite client as a GenServer actor, split transaction evaluation into pre/post execution phases, and simplify the block producer interface. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CredibleLayerClient is now a spawned actor using #[protocol]/#[actor] macros, replacing the previous Arc-wrapped struct. All protobuf conversion logic (BlockEnv, CommitHead, NewIteration, TransactionEnv) is encapsulated inside the actor — the block producer makes one-line calls instead of building protobuf messages inline. Transaction evaluation now follows the Besu plugin pattern: the tx event is sent to the sidecar before execution (fire-and-forget), and the verdict is polled after execution. If the sidecar rejects, the execution is undone using the existing undo_last_tx mechanism. Also removes unused state_oracle_address config/CLI flag, the e2e script, sidecar-config template, unused Makefile targets, and broken doc links. TestOwnershipAssertion.sol rewritten as a real credible-std assertion. --- cmd/ethrex/l2/options.rs | 10 - crates/l2/Makefile | 26 - .../l2/contracts/src/credible_layer/README.md | 52 +- .../credible_layer/TestOwnershipAssertion.sol | 63 +- crates/l2/scripts/credible_layer_e2e.sh | 150 ----- crates/l2/sequencer/block_producer.rs | 109 +--- .../block_producer/payload_builder.rs | 166 ++--- crates/l2/sequencer/configs.rs | 4 - crates/l2/sequencer/credible_layer/client.rs | 585 ++++++++---------- crates/l2/sequencer/credible_layer/mod.rs | 4 +- crates/l2/sequencer/mod.rs | 19 +- crates/l2/sidecar-config.template.json | 47 -- docs/CLI.md | 8 +- docs/l2/credible_layer.md | 13 +- 14 files changed, 381 insertions(+), 875 deletions(-) delete mode 100755 crates/l2/scripts/credible_layer_e2e.sh delete mode 100644 crates/l2/sidecar-config.template.json diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index 7db478f2fb0..0c58d2f140d 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -268,7 +268,6 @@ impl TryFrom for SequencerConfig { credible_layer: if opts.credible_layer_opts.credible_layer { CredibleLayerConfig { sidecar_url: opts.credible_layer_opts.credible_layer_url, - state_oracle_address: opts.credible_layer_opts.credible_layer_state_oracle, } } else { CredibleLayerConfig::default() @@ -1097,15 +1096,6 @@ pub struct CredibleLayerOptions { help_heading = "Credible Layer options" )] pub credible_layer_url: Option, - #[arg( - long = "credible-layer-state-oracle", - value_name = "ADDRESS", - env = "ETHREX_CREDIBLE_LAYER_STATE_ORACLE", - requires = "credible_layer", - help = "Address of the already-deployed State Oracle contract on L2. The State Oracle maps protected contracts to their active assertions and is required by the Credible Layer sidecar. Deploy it separately using the Phylax toolchain (see crates/l2/contracts/src/credible_layer/README.md).", - help_heading = "Credible Layer options" - )] - pub credible_layer_state_oracle: Option
, } #[derive(Parser)] diff --git a/crates/l2/Makefile b/crates/l2/Makefile index 9f4a83f4ccc..9fcb04c573a 100644 --- a/crates/l2/Makefile +++ b/crates/l2/Makefile @@ -281,29 +281,3 @@ state-diff-test: # - docs/developers/l2/state-reconstruction-blobs.md (step-by-step guide) validate-blobs: cargo test -p ethrex-test validate_blobs_match_genesis --release - -# Credible Layer (Credible Layer) -# See docs/l2/credible_layer.md for full documentation. - -init-credible-layer: ## 🛡️ Start the Credible Layer sidecar stack (assertion-da + sidecar) - @echo "Starting Credible Layer sidecar stack..." - @echo "Make sure to set STATE_ORACLE_ADDRESS in sidecar-config.template.json" - docker run -d --name assertion-da -p 5001:5001 \ - -e DB_PATH=/data/assertions \ - -e DA_LISTEN_ADDR=0.0.0.0:5001 \ - -e DA_CACHE_SIZE=1000000 \ - ghcr.io/phylaxsystems/credible-sdk/assertion-da-dev:sha-01b3374 || true - @echo "Assertion DA running on :5001" - @echo "Start the sidecar manually: ./sidecar --config-file-path ./sidecar-config.template.json" - -down-credible-layer: ## 🛑 Stop the Credible Layer sidecar stack - docker stop assertion-da 2>/dev/null || true - docker rm assertion-da 2>/dev/null || true - -init-l2-credible-layer: ## 🛡️ Start L2 with Credible Layer enabled - cargo run --release --features l2 --manifest-path ../../Cargo.toml -- \ - l2 \ - --credible-layer-url http://localhost:50051 \ - --l2.ws-enabled \ - --l2.ws-port 1730 \ - 2>&1 | tee /tmp/l2_cb.log diff --git a/crates/l2/contracts/src/credible_layer/README.md b/crates/l2/contracts/src/credible_layer/README.md index 20af60587f2..51f6726c562 100644 --- a/crates/l2/contracts/src/credible_layer/README.md +++ b/crates/l2/contracts/src/credible_layer/README.md @@ -11,59 +11,9 @@ The **State Oracle** is the on-chain registry that maps protected contracts to t assertions. It is maintained by Phylax Systems and must be deployed separately using the Phylax toolchain before starting the Credible Layer sidecar. -### Contract Source - -The State Oracle and its dependencies live in the `credible-layer-contracts` repository: - -> https://github.com/phylaxsystems/credible-layer-contracts - -The relevant contracts are: - -| Contract | Purpose | -|----------|---------| -| `StateOracle` | Core registry: maps protected contracts to assertions | -| `DAVerifierECDSA` | Verifies ECDSA-signed assertion DA payloads | -| `DAVerifierOnChain` | Verifies on-chain DA payloads | -| `AdminVerifierOwner` | Restricts assertion registration to contract owner | - -The `StateOracle` constructor signature is: - -```solidity -constructor(uint256 assertionTimelockBlocks) Ownable(msg.sender) -``` - -And initialization (called after proxy deployment): - -```solidity -function initialize( - address admin, - IAdminVerifier[] calldata _adminVerifiers, - IDAVerifier[] calldata _daVerifiers, - uint16 _maxAssertionsPerAA -) external -``` - -### Deploying the State Oracle - -Use the Phylax `pcl` CLI or the Foundry deployment scripts provided in -`credible-layer-contracts`: - -```bash -# Install pcl -brew tap phylaxsystems/pcl -brew install pcl - -# Or use Foundry scripts from the credible-layer-contracts repo -git clone https://github.com/phylaxsystems/credible-layer-contracts -cd credible-layer-contracts -forge script script/DeployStateOracle.s.sol --rpc-url --broadcast -``` - -Once deployed, note the State Oracle address and pass it to ethrex via the -`--credible-layer-state-oracle` flag (see the [Credible Layer docs](../../../../docs/l2/credible_layer.md)). +See the [Credible Layer docs](../../../../docs/l2/credible_layer.md) for deployment instructions. ### References - [Credible Layer Introduction](https://docs.phylax.systems/credible/credible-introduction) - [credible-layer-contracts](https://github.com/phylaxsystems/credible-layer-contracts) -- [credible-sdk (sidecar source)](https://github.com/phylaxsystems/credible-sdk) diff --git a/crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol b/crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol index cce9a416c64..7b870cdac69 100644 --- a/crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol +++ b/crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol @@ -1,52 +1,31 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -/// @title TestOwnershipAssertion -/// @notice A trivial assertion for Credible Layer end-to-end testing. -/// Protects OwnableTarget by asserting that ownership cannot change. -/// -/// This contract uses the credible-std library interfaces. -/// To compile and deploy, use the pcl CLI: -/// pcl apply --assertion TestOwnershipAssertion --adopter -/// -/// For local development without credible-std, this file serves as a reference -/// for the assertion logic. The actual deployment uses pcl which handles -/// compilation with the credible-std dependency. - -interface IPhEvm { - function forkPreTx() external; - function forkPostTx() external; - function getAssertionAdopter() external view returns (address); -} +import {Assertion} from "credible-std/Assertion.sol"; interface IOwnableTarget { function owner() external view returns (address); function transferOwnership(address newOwner) external; } -/// @dev In production, this would inherit from credible-std's Assertion base class. -/// The actual assertion contract deployed via pcl would look like: -/// -/// import {Assertion} from "credible-std/Assertion.sol"; -/// -/// contract TestOwnershipAssertion is Assertion { -/// function triggers() external view override { -/// registerCallTrigger( -/// this.assertOwnerUnchanged.selector, -/// IOwnableTarget.transferOwnership.selector -/// ); -/// } -/// -/// function assertOwnerUnchanged() external { -/// IOwnableTarget target = IOwnableTarget(ph.getAssertionAdopter()); -/// ph.forkPreTx(); -/// address ownerBefore = target.owner(); -/// ph.forkPostTx(); -/// address ownerAfter = target.owner(); -/// require(ownerBefore == ownerAfter, "ownership changed"); -/// } -/// } -contract TestOwnershipAssertion { - // This is a reference implementation. See the comment above for the - // actual credible-std version used with pcl. +/// @title TestOwnershipAssertion +/// @notice Protects OwnableTarget by asserting that ownership cannot change. +/// @dev Compile with `pcl build` (requires credible-std as a dependency). +/// See docs/l2/credible_layer.md for the full deployment workflow. +contract TestOwnershipAssertion is Assertion { + function triggers() external view override { + registerCallTrigger( + this.assertOwnerUnchanged.selector, + IOwnableTarget.transferOwnership.selector + ); + } + + function assertOwnerUnchanged() external { + IOwnableTarget target = IOwnableTarget(ph.getAssertionAdopter()); + ph.forkPreTx(); + address ownerBefore = target.owner(); + ph.forkPostTx(); + address ownerAfter = target.owner(); + require(ownerBefore == ownerAfter, "ownership changed"); + } } diff --git a/crates/l2/scripts/credible_layer_e2e.sh b/crates/l2/scripts/credible_layer_e2e.sh deleted file mode 100755 index 3427429947d..00000000000 --- a/crates/l2/scripts/credible_layer_e2e.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env bash -# Credible Layer End-to-End Validation Script -# -# This script validates the full Credible Layer integration: -# 1. Checks that ethrex L2 is running with Credible Layer enabled -# 2. Checks that the sidecar is running and healthy -# 3. Deploys a test target contract (OwnableTarget) -# 4. (Manual step) Deploys and registers a test assertion via pcl -# 5. Sends a violating transaction → verifies it's NOT included -# 6. Sends a valid transaction → verifies it IS included -# -# Prerequisites: -# - ethrex L2 running with --credible-layer-url (see: make init-l2-credible-layer) -# - Credible Layer sidecar running (see: make init-credible-layer) -# - cast (from foundry) installed -# - A funded account on L2 -# -# Usage: -# ./credible_layer_e2e.sh [L2_RPC_URL] [SIDECAR_HEALTH_URL] - -set -euo pipefail - -L2_RPC_URL="${1:-http://localhost:1729}" -SIDECAR_HEALTH_URL="${2:-http://localhost:9547/health}" -PRIVATE_KEY="${PRIVATE_KEY:-0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924}" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -pass() { echo -e "${GREEN}[PASS]${NC} $1"; } -fail() { echo -e "${RED}[FAIL]${NC} $1"; exit 1; } -info() { echo -e "${YELLOW}[INFO]${NC} $1"; } - -# ─── Step 1: Check ethrex L2 is running ─────────────────────────────────────── - -info "Checking ethrex L2 at ${L2_RPC_URL}..." -BLOCK_NUMBER=$(cast block-number --rpc-url "$L2_RPC_URL" 2>/dev/null || echo "UNREACHABLE") -if [ "$BLOCK_NUMBER" = "UNREACHABLE" ]; then - fail "ethrex L2 is not reachable at ${L2_RPC_URL}" -fi -pass "ethrex L2 is running. Current block: ${BLOCK_NUMBER}" - -# ─── Step 2: Check sidecar is running ───────────────────────────────────────── - -info "Checking sidecar health at ${SIDECAR_HEALTH_URL}..." -HEALTH=$(curl -s -o /dev/null -w "%{http_code}" "$SIDECAR_HEALTH_URL" 2>/dev/null || echo "000") -if [ "$HEALTH" = "200" ]; then - pass "Sidecar is healthy" -elif [ "$HEALTH" = "000" ]; then - info "Sidecar health endpoint not reachable (may be OK if running without health server)" -else - info "Sidecar health returned HTTP ${HEALTH}" -fi - -# ─── Step 3: Deploy OwnableTarget contract ──────────────────────────────────── - -info "Deploying OwnableTarget contract..." - -# OwnableTarget bytecode (compiled from contracts/src/credible_layer/OwnableTarget.sol) -# If you need to recompile: solc --bin OwnableTarget.sol -# For now, we attempt to deploy using cast -OWNABLE_TARGET_DEPLOY=$(cast send --create \ - --rpc-url "$L2_RPC_URL" \ - --private-key "$PRIVATE_KEY" \ - --json \ - "$(cat <<'SOLC_EOF' -0x608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102f8806100606000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063131a06801461004657806370a082311461006a578063f2fde38b14610088575b600080fd5b61004e6100a4565b604051808260001916815260200191505060405180910390f35b6100726100ae565b6040518082815260200191505060405180910390f35b6100a2600480360381019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506100b7565b005b6000602a905090565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610165576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602481526020018061029f6024913960400191505060405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156101eb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260348152602001806102c36034913960400191505060405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505056fe4f776e61626c655461726765743a2063616c6c6572206973206e6f7420746865206f776e65724f776e61626c655461726765743a206e6577206f776e657220697320746865207a65726f206164647265737300 -SOLC_EOF -)" 2>/dev/null) || true - -if [ -n "$OWNABLE_TARGET_DEPLOY" ]; then - CONTRACT_ADDRESS=$(echo "$OWNABLE_TARGET_DEPLOY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('contractAddress',''))" 2>/dev/null || echo "") - if [ -n "$CONTRACT_ADDRESS" ]; then - pass "OwnableTarget deployed at: ${CONTRACT_ADDRESS}" - else - info "Deploy transaction sent but contract address not parsed. Check logs." - fi -else - info "Could not deploy OwnableTarget. You may need to deploy it manually." - info "Compile with: solc --bin crates/l2/contracts/src/credible_layer/OwnableTarget.sol" -fi - -# ─── Step 4: Assertion registration (manual) ────────────────────────────────── - -echo "" -info "=== MANUAL STEP ===" -info "To complete the e2e test, you need to:" -info " 1. Deploy TestOwnershipAssertion using pcl:" -info " pcl apply --assertion TestOwnershipAssertion --adopter ${CONTRACT_ADDRESS:-}" -info " 2. Wait for the assertion timelock to expire" -info " 3. Then run this script again with the --validate flag" -echo "" - -# ─── Step 5 & 6: Validation (run with --validate after assertion is active) ── - -if [ "${3:-}" = "--validate" ] && [ -n "${CONTRACT_ADDRESS:-}" ]; then - info "Running validation..." - - # Get current block number - BLOCK_BEFORE=$(cast block-number --rpc-url "$L2_RPC_URL") - - # Send violating transaction: transferOwnership - info "Sending violating transaction (transferOwnership)..." - VIOLATING_TX=$(cast send "$CONTRACT_ADDRESS" \ - "transferOwnership(address)" \ - "0x0000000000000000000000000000000000000001" \ - --rpc-url "$L2_RPC_URL" \ - --private-key "$PRIVATE_KEY" \ - --json 2>/dev/null) || true - - sleep 5 # Wait for a block - - # Send valid transaction: doSomething - info "Sending valid transaction (doSomething)..." - VALID_TX=$(cast send "$CONTRACT_ADDRESS" \ - "doSomething()" \ - --rpc-url "$L2_RPC_URL" \ - --private-key "$PRIVATE_KEY" \ - --json 2>/dev/null) || true - - sleep 5 # Wait for a block - - # Check if valid tx was included - if [ -n "$VALID_TX" ]; then - VALID_TX_HASH=$(echo "$VALID_TX" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transactionHash',''))" 2>/dev/null || echo "") - if [ -n "$VALID_TX_HASH" ]; then - RECEIPT=$(cast receipt "$VALID_TX_HASH" --rpc-url "$L2_RPC_URL" --json 2>/dev/null || echo "") - if [ -n "$RECEIPT" ]; then - pass "Valid transaction was included in a block (tx: ${VALID_TX_HASH})" - else - fail "Valid transaction was NOT included (should have been)" - fi - fi - fi - - # Check owner hasn't changed (violating tx should have been dropped) - CURRENT_OWNER=$(cast call "$CONTRACT_ADDRESS" "owner()" --rpc-url "$L2_RPC_URL" 2>/dev/null || echo "") - info "Current owner: ${CURRENT_OWNER}" - info "If ownership hasn't changed, the violating transaction was successfully dropped." - - echo "" - pass "E2E validation complete. Check sidecar logs for assertion evaluation details." -else - info "Skipping validation. Run with '--validate' after assertion registration." -fi - -echo "" -info "Done." diff --git a/crates/l2/sequencer/block_producer.rs b/crates/l2/sequencer/block_producer.rs index 92f9228e17c..450b9279c8b 100644 --- a/crates/l2/sequencer/block_producer.rs +++ b/crates/l2/sequencer/block_producer.rs @@ -38,11 +38,8 @@ use ethrex_l2_common::sequencer_state::{SequencerState, SequencerStatus}; use std::str::FromStr; -use super::credible_layer::{ - CredibleLayerClient, - client::CredibleLayerConfig as ClientCredibleLayerConfig, - sidecar_proto::{BlobExcessGasAndPrice, BlockEnv, CommitHead, NewIteration}, -}; +use super::credible_layer::CredibleLayerClient; +use super::credible_layer::client::CredibleLayerProtocol; use super::errors::BlockProducerError; use ethrex_metrics::metrics; @@ -69,7 +66,7 @@ pub struct BlockProducer { block_gas_limit: u64, eth_client: EthClient, router_address: Address, - credible_layer: Option>, + credible_layer: Option>, /// Actor handle for sending new block headers to WS subscribers. subscription_manager: Option>, } @@ -93,7 +90,7 @@ impl BlockProducer { sequencer_state: SequencerState, router_address: Address, l2_gas_limit: u64, - credible_layer_url: Option, + credible_layer: Option>, subscription_manager: Option>, ) -> Result { let BlockProducerConfig { @@ -120,27 +117,6 @@ impl BlockProducer { ); } - let credible_layer = if let Some(url) = credible_layer_url { - let cb_config = ClientCredibleLayerConfig { - sidecar_url: url, - ..Default::default() - }; - match CredibleLayerClient::connect(cb_config).await { - Ok(client) => { - info!("Credible Layer sidecar connected"); - Some(Arc::new(client)) - } - Err(e) => { - warn!( - "Failed to connect to Credible Layer sidecar: {e}. Proceeding without credible layer." - ); - None - } - } - } else { - None - }; - Ok(Self { store, blockchain, @@ -192,42 +168,8 @@ impl BlockProducer { let payload = create_payload(&args, &self.store, Bytes::new())?; // Credible Layer: send NewIteration before building the block. - // CommitHead is sent AFTER the block is stored (see below). - // The sidecar flow per block: NewIteration → Transaction(s) → CommitHead - if let Some(cb) = &self.credible_layer { - let block_number_bytes = u64_to_u256_bytes(payload.header.number); - let timestamp_bytes = u64_to_u256_bytes(payload.header.timestamp); - let beneficiary_bytes = payload.header.coinbase.as_bytes().to_vec(); - let difficulty_bytes = payload.header.difficulty.to_big_endian().to_vec(); - let prevrandao = Some(payload.header.prev_randao.to_fixed_bytes().to_vec()); - let block_env = BlockEnv { - number: block_number_bytes, - beneficiary: beneficiary_bytes, - timestamp: timestamp_bytes, - gas_limit: payload.header.gas_limit, - basefee: payload.header.base_fee_per_gas.unwrap_or(0), - difficulty: difficulty_bytes, - prevrandao, - blob_excess_gas_and_price: Some(BlobExcessGasAndPrice { - excess_blob_gas: 0, - blob_gasprice: vec![0u8; 16], // u128 big-endian = 0 - }), - }; - let iteration_id = cb.current_iteration_id() + 1; - let parent_block_hash = Some(payload.header.parent_hash.to_fixed_bytes().to_vec()); - let parent_beacon_block_root = payload - .header - .parent_beacon_block_root - .map(|h| h.to_fixed_bytes().to_vec()); - let new_iteration = NewIteration { - block_env: Some(block_env), - iteration_id, - parent_block_hash, - parent_beacon_block_root, - }; - if let Err(e) = cb.send_new_iteration(new_iteration).await { - warn!("Failed to send NewIteration to credible layer: {e}"); - } + if let Some(ref cl) = self.credible_layer { + let _ = cl.new_iteration(payload.header.clone()); } let registered_chains = self.get_registered_l2_chain_ids().await?; @@ -293,33 +235,23 @@ impl BlockProducer { // Make the new head be part of the canonical chain apply_fork_choice(&self.store, block_hash, block_hash, block_hash).await?; - // Credible Layer: send CommitHead AFTER block is stored (matches Besu plugin flow) - if let Some(cb) = &self.credible_layer { + // Credible Layer: send CommitHead after block is stored + if let Some(ref cl) = self.credible_layer { let last_tx_hash = self .store .get_block_by_hash(block_hash) .await .ok() .flatten() - .and_then(|b| { - b.body - .transactions - .last() - .map(|tx| tx.hash().to_fixed_bytes().to_vec()) - }); + .and_then(|b| b.body.transactions.last().map(|tx| tx.hash())); #[allow(clippy::as_conversions)] - let commit_head = CommitHead { + let _ = cl.commit_head( + block_number, + block_hash, + block_header.timestamp, + transactions_count as u64, last_tx_hash, - n_transactions: transactions_count as u64, - block_number: u64_to_u256_bytes(block_number), - selected_iteration_id: cb.current_iteration_id(), - block_hash: Some(block_hash.to_fixed_bytes().to_vec()), - parent_beacon_block_root: None, - timestamp: u64_to_u256_bytes(block_header.timestamp), - }; - if let Err(e) = cb.send_commit_head(commit_head).await { - warn!("Failed to send CommitHead to credible layer: {e}"); - } + ); } // Notify all eth_subscribe("newHeads") subscribers. @@ -399,13 +331,6 @@ impl BlockProducer { } } -/// Encode a u64 as a 32-byte big-endian U256 for protobuf fields. -fn u64_to_u256_bytes(value: u64) -> Vec { - let mut buf = [0u8; 32]; - buf[24..].copy_from_slice(&value.to_be_bytes()); - buf.to_vec() -} - #[actor(protocol = BlockProducerProtocol)] impl BlockProducer { #[expect(clippy::too_many_arguments)] @@ -417,9 +342,9 @@ impl BlockProducer { sequencer_state: SequencerState, router_address: Address, l2_gas_limit: u64, + credible_layer: Option>, subscription_manager: Option>, ) -> Result, BlockProducerError> { - let credible_layer_url = cfg.credible_layer.sidecar_url.clone(); let block_producer = Self::new( &cfg.block_producer, cfg.eth.rpc_url, @@ -429,7 +354,7 @@ impl BlockProducer { sequencer_state, router_address, l2_gas_limit, - credible_layer_url, + credible_layer, subscription_manager, ) .await?; diff --git a/crates/l2/sequencer/block_producer/payload_builder.rs b/crates/l2/sequencer/block_producer/payload_builder.rs index e66faae935a..ab29d54f041 100644 --- a/crates/l2/sequencer/block_producer/payload_builder.rs +++ b/crates/l2/sequencer/block_producer/payload_builder.rs @@ -1,7 +1,5 @@ -use crate::sequencer::credible_layer::{ - CredibleLayerClient, - sidecar_proto::{Transaction as SidecarTransaction, TransactionEnv, TxExecutionId}, -}; +use crate::sequencer::credible_layer::CredibleLayerClient; +use crate::sequencer::credible_layer::client::CredibleLayerProtocol; use crate::sequencer::errors::BlockProducerError; use ethrex_blockchain::{ Blockchain, @@ -24,6 +22,7 @@ use ethrex_metrics::{ }; use ethrex_rlp::encode::RLPEncode; use ethrex_storage::Store; +use spawned_concurrency::tasks::ActorRef; use std::sync::Arc; use std::{collections::HashMap, ops::Div}; use tokio::time::Instant; @@ -39,7 +38,7 @@ pub async fn build_payload( privileged_nonces: &mut HashMap>, block_gas_limit: u64, registered_chains: Vec, - credible_layer: Option>, + credible_layer: Option>, ) -> Result { let since = Instant::now(); let gas_limit = payload.header.gas_limit; @@ -106,7 +105,7 @@ pub async fn fill_transactions( privileged_nonces: &mut HashMap>, configured_block_gas_limit: u64, registered_chains: Vec, - credible_layer: Option>, + credible_layer: Option>, ) -> Result<(), BlockProducerError> { let mut privileged_tx_count = 0; let VMType::L2(fee_config) = context.vm.vm_type else { @@ -193,41 +192,6 @@ pub async fn fill_transactions( // TODO: maybe fetch hash too when filtering mempool so we don't have to compute it here (we can do this in the same refactor as adding timestamp) let tx_hash = head_tx.tx.hash(); - // Task 2.5: For non-privileged transactions, check with the credible layer sidecar. - // Privileged transactions (PrivilegedL2Transaction) bypass this check. - if let Some(cb) = &credible_layer { - if !head_tx.is_privileged() { - let block_num_bytes = { - let mut buf = [0u8; 32]; - let n = context.block_number(); - buf[24..].copy_from_slice(&n.to_be_bytes()); - buf.to_vec() - }; - #[allow(clippy::as_conversions)] - let tx_index = context.payload.body.transactions.len() as u64; - let tx_execution_id = Some(TxExecutionId { - block_number: block_num_bytes, - iteration_id: cb.current_iteration_id(), - tx_hash: tx_hash.as_bytes().to_vec(), - index: tx_index, - }); - let sender = head_tx.tx.sender(); - let tx_as_inner: Transaction = head_tx.clone().into(); - let tx_env = build_transaction_env(&tx_as_inner, sender); - let sidecar_tx = SidecarTransaction { - tx_execution_id, - tx_env: Some(tx_env), - prev_tx_hash: None, - }; - if !cb.evaluate_transaction(sidecar_tx).await { - debug!("Circuit breaker rejected transaction: {tx_hash:#x}"); - txs.pop(); - blockchain.remove_transaction_from_pool(&tx_hash)?; - continue; - } - } - } - // Check whether the tx is replay-protected if head_tx.tx.protected() && !chain_config.is_eip155_activated(context.block_number()) { // Ignore replay protected tx & all txs from the sender @@ -251,6 +215,24 @@ pub async fn fill_transactions( continue; } + // Credible Layer: send transaction event to the sidecar before execution. + // TODO: Privileged transactions (L1->L2 deposits) currently bypass the Credible Layer + // check entirely. This should be revisited — the sidecar should be aware of all + // transactions for accurate state tracking, even if privileged txs are never dropped. + if let Some(ref cl) = credible_layer { + if !head_tx.is_privileged() { + #[allow(clippy::as_conversions)] + let tx_index = context.payload.body.transactions.len() as u64; + let _ = cl.send_transaction( + tx_hash, + context.block_number(), + tx_index, + head_tx.tx.sender(), + tx.clone(), + ); + } + } + // Set BAL index for this transaction (1-indexed per EIP-7928) #[allow(clippy::cast_possible_truncation, clippy::as_conversions)] let tx_index = (context.payload.body.transactions.len() + 1) as u16; @@ -279,6 +261,29 @@ pub async fn fill_transactions( } }; + // Credible Layer: poll for the sidecar's verdict after execution. + // If the sidecar rejected the transaction, undo execution and drop it. + if let Some(ref cl) = credible_layer { + if !head_tx.is_privileged() { + #[allow(clippy::as_conversions)] + let check_tx_index = context.payload.body.transactions.len() as u64; + let include = cl + .check_transaction(tx_hash, context.block_number(), check_tx_index) + .await + .unwrap_or(true); + if !include { + debug!("Credible layer rejected transaction: {tx_hash:#x}"); + txs.pop(); + context.vm.undo_last_tx()?; + context.remaining_gas = previous_remaining_gas; + context.block_value = previous_block_value; + context.cumulative_gas_spent = previous_cumulative_gas_spent; + blockchain.remove_transaction_from_pool(&tx_hash)?; + continue; + } + } + } + let l2_messages = get_block_l2_out_messages(std::slice::from_ref(&receipt), chain_id); let mut found_invalid_message = false; for msg in l2_messages { @@ -342,82 +347,3 @@ fn fetch_mempool_transactions( } Ok(plain_txs) } - -/// Build a `TransactionEnv` protobuf message from an ethrex transaction and its sender. -fn build_transaction_env(tx: &Transaction, sender: ethrex_common::Address) -> TransactionEnv { - use ethrex_common::types::TxKind; - - let transact_to = match tx.to() { - TxKind::Call(addr) => addr.as_bytes().to_vec(), - TxKind::Create => vec![], - }; - - let value_bytes = tx.value().to_big_endian(); - - let mut gas_price_bytes = [0u8; 16]; - // gas_price fits in u128 for all practical purposes - let gas_price_u128 = tx.gas_price().as_u128(); - gas_price_bytes.copy_from_slice(&gas_price_u128.to_be_bytes()); - - let gas_priority_fee = tx.max_priority_fee().map(|fee| { - let mut buf = [0u8; 16]; - buf[8..].copy_from_slice(&fee.to_be_bytes()); - buf.to_vec() - }); - - let access_list = tx - .access_list() - .iter() - .map(|(addr, keys)| { - use super::super::credible_layer::sidecar_proto::AccessListItem; - AccessListItem { - address: addr.as_bytes().to_vec(), - storage_keys: keys.iter().map(|k| k.as_bytes().to_vec()).collect(), - } - }) - .collect(); - - let authorization_list = tx - .authorization_list() - .map(|list| { - use super::super::credible_layer::sidecar_proto::Authorization; - list.iter() - .map(|auth| { - let chain_id_bytes = auth.chain_id.to_big_endian(); - let r_bytes = auth.r_signature.to_big_endian(); - let s_bytes = auth.s_signature.to_big_endian(); - Authorization { - chain_id: chain_id_bytes.to_vec(), - address: auth.address.as_bytes().to_vec(), - nonce: auth.nonce, - y_parity: auth.y_parity.as_u32(), - r: r_bytes.to_vec(), - s: s_bytes.to_vec(), - } - }) - .collect() - }) - .unwrap_or_default(); - - #[allow(clippy::as_conversions)] - TransactionEnv { - tx_type: u8::from(tx.tx_type()) as u32, - caller: sender.as_bytes().to_vec(), - gas_limit: tx.gas_limit(), - gas_price: gas_price_bytes.to_vec(), - transact_to, - value: value_bytes.to_vec(), - data: tx.data().to_vec(), - nonce: tx.nonce(), - chain_id: tx.chain_id(), - access_list, - gas_priority_fee, - blob_hashes: tx - .blob_versioned_hashes() - .iter() - .map(|h| h.as_bytes().to_vec()) - .collect(), - max_fee_per_blob_gas: vec![0u8; 16], - authorization_list, - } -} diff --git a/crates/l2/sequencer/configs.rs b/crates/l2/sequencer/configs.rs index c36b3f5d4cb..58ad1fa78df 100644 --- a/crates/l2/sequencer/configs.rs +++ b/crates/l2/sequencer/configs.rs @@ -26,10 +26,6 @@ pub struct SequencerConfig { pub struct CredibleLayerConfig { /// gRPC endpoint for the Credible Layer Assertion Enforcer sidecar. pub sidecar_url: Option, - /// Address of the already-deployed State Oracle contract on L2. - /// Required by the Credible Layer sidecar for assertion registry lookups. - /// Deploy the State Oracle separately using the Phylax toolchain before starting ethrex. - pub state_oracle_address: Option
, } // TODO: Move to blockchain/dev diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 261f5d8df5a..ab98b3d82db 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -1,79 +1,98 @@ use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; -use tokio::sync::{Mutex, mpsc}; +use ethrex_common::types::{BlockHeader, Transaction, TxKind}; +use ethrex_common::{Address, H256}; +use spawned_concurrency::{ + actor, + error::ActorError, + protocol, + tasks::{Actor, ActorRef, ActorStart as _, Context, Handler, Response}, +}; +use tokio::sync::mpsc; use tonic::transport::Channel; use tracing::{debug, info, warn}; -use super::errors::CredibleLayerError; use super::sidecar_proto::{ - self, CommitHead, Event, GetTransactionRequest, NewIteration, ResultStatus, Transaction, - TransactionResult, TxExecutionId, sidecar_transport_client::SidecarTransportClient, + self, AccessListItem, Authorization, BlobExcessGasAndPrice, BlockEnv, CommitHead, Event, + GetTransactionRequest, NewIteration, ResultStatus, Transaction as SidecarTransaction, + TransactionEnv, TxExecutionId, sidecar_transport_client::SidecarTransportClient, }; -/// Configuration for the Credible Layer gRPC client. -#[derive(Debug, Clone)] -pub struct CredibleLayerConfig { - /// gRPC endpoint URL for the sidecar (e.g., "http://localhost:50051") - pub sidecar_url: String, - /// Timeout for waiting for a transaction result from the sidecar - pub result_timeout: Duration, - /// Timeout for the GetTransaction fallback poll - pub poll_timeout: Duration, -} +use super::errors::CredibleLayerError; -impl Default for CredibleLayerConfig { - fn default() -> Self { - Self { - sidecar_url: "http://localhost:50051".to_string(), - result_timeout: Duration::from_millis(500), - poll_timeout: Duration::from_millis(200), - } - } +#[protocol] +pub trait CredibleLayerProtocol: Send + Sync { + /// Notify the sidecar that a new block iteration has started. + fn new_iteration(&self, header: BlockHeader) -> Result<(), ActorError>; + + /// Notify the sidecar that a block has been committed. + fn commit_head( + &self, + block_number: u64, + block_hash: H256, + timestamp: u64, + tx_count: u64, + last_tx_hash: Option, + ) -> Result<(), ActorError>; + + /// Send a transaction event to the sidecar (pre-execution, fire-and-forget). + fn send_transaction( + &self, + tx_hash: H256, + block_number: u64, + tx_index: u64, + sender: Address, + tx: Transaction, + ) -> Result<(), ActorError>; + + /// Poll the sidecar for a transaction verdict (post-execution). + /// Returns `true` if the transaction should be included, `false` if it should be dropped. + /// On any error or timeout, returns `true` (permissive — liveness over safety). + fn check_transaction(&self, tx_hash: H256, block_number: u64, tx_index: u64) -> Response; } -/// gRPC client for communicating with the Credible Layer Assertion Enforcer sidecar. +/// gRPC client actor for communicating with the Credible Layer Assertion Enforcer sidecar. /// -/// Maintains a persistent bidirectional `StreamEvents` gRPC stream. Events are sent -/// via an mpsc channel that feeds the stream. Transaction results are retrieved via -/// the `GetTransaction` unary RPC. +/// Maintains a persistent bidirectional `StreamEvents` gRPC stream via a background task. +/// Events are sent through an mpsc channel that feeds the stream. Transaction results +/// are retrieved via the `GetTransaction` unary RPC. pub struct CredibleLayerClient { - config: CredibleLayerConfig, /// Sender side of the persistent StreamEvents stream event_sender: mpsc::Sender, /// Monotonically increasing event ID counter - event_id_counter: AtomicU64, + event_id_counter: u64, /// Current iteration ID (incremented per block) - iteration_id: AtomicU64, + iteration_id: u64, /// gRPC client for unary calls (GetTransaction) - grpc_client: Arc>>, + grpc_client: SidecarTransportClient, /// Whether the StreamEvents stream is currently connected. - /// When false, evaluate_transaction skips immediately (permissive). + /// When false, send handlers skip immediately (permissive). stream_connected: Arc, } +#[actor(protocol = CredibleLayerProtocol)] impl CredibleLayerClient { - /// Create a new client with lazy connection to the sidecar. - /// Opens a persistent StreamEvents bidirectional stream in the background. - pub async fn connect(config: CredibleLayerConfig) -> Result { - info!( - url = %config.sidecar_url, - "Configuring Credible Layer sidecar client" - ); + /// Spawn the Credible Layer client actor. + pub async fn spawn(sidecar_url: String) -> Result, CredibleLayerError> { + let client = Self::new(sidecar_url).await?; + Ok(client.start()) + } + + async fn new(sidecar_url: String) -> Result { + info!(url = %sidecar_url, "Configuring Credible Layer sidecar client"); - let channel = Channel::from_shared(config.sidecar_url.clone()) + let channel = Channel::from_shared(sidecar_url) .map_err(|e| CredibleLayerError::Internal(format!("Invalid URL: {e}")))? .connect_timeout(Duration::from_secs(5)) .timeout(Duration::from_secs(5)) .connect_lazy(); - let mut client = SidecarTransportClient::new(channel.clone()); + let mut stream_client = SidecarTransportClient::new(channel.clone()); let stream_connected = Arc::new(AtomicBool::new(false)); let stream_connected_bg = stream_connected.clone(); - // Create the event channel. The sender goes to the client, the receiver - // is owned by the background stream task. let (event_tx, mut event_rx) = mpsc::channel::(256); // Background task: maintains a persistent StreamEvents connection. @@ -81,18 +100,17 @@ impl CredibleLayerClient { // Reconnects automatically if the connection drops. tokio::spawn(async move { loop { - // Create a new gRPC-side channel for each connection attempt let (grpc_tx, grpc_rx) = mpsc::channel::(64); let grpc_stream = tokio_stream::wrappers::ReceiverStream::new(grpc_rx); - match client.stream_events(grpc_stream).await { + match stream_client.stream_events(grpc_stream).await { Ok(response) => { info!("StreamEvents stream connected to sidecar"); stream_connected_bg.store(true, Ordering::Relaxed); let mut ack_stream = response.into_inner(); - // Send an initial CommitHead (block 0) as the first event on - // every new stream — the sidecar requires CommitHead first. + // Send an initial CommitHead (block 0) — the sidecar requires + // CommitHead as the first event on every new stream. let init_commit = Event { event_id: 0, event: Some(sidecar_proto::event::Event::CommitHead(CommitHead { @@ -114,31 +132,20 @@ impl CredibleLayerClient { // while also reading acks loop { tokio::select! { - // Read events from the main channel and forward to gRPC event = event_rx.recv() => { match event { Some(e) => { - let event_type = match &e.event { - Some(sidecar_proto::event::Event::CommitHead(_)) => "CommitHead", - Some(sidecar_proto::event::Event::NewIteration(_)) => "NewIteration", - Some(sidecar_proto::event::Event::Transaction(_)) => "Transaction", - Some(sidecar_proto::event::Event::Reorg(_)) => "Reorg", - None => "None", - }; - info!("Forwarding {event_type} event to gRPC stream (event_id={})", e.event_id); if grpc_tx.send(e).await.is_err() { warn!("gRPC stream send failed, reconnecting"); break; } } None => { - // Main channel closed — client dropped warn!("Event channel closed, stopping stream task"); return; } } } - // Read acks from sidecar ack = ack_stream.message() => { match ack { Ok(Some(a)) => { @@ -171,109 +178,162 @@ impl CredibleLayerClient { info!("Credible Layer client ready (persistent stream opened)"); Ok(Self { - config, event_sender: event_tx, - event_id_counter: AtomicU64::new(1), - iteration_id: AtomicU64::new(0), - grpc_client: Arc::new(Mutex::new(SidecarTransportClient::new(channel))), + event_id_counter: 1, + iteration_id: 0, + grpc_client: SidecarTransportClient::new(channel), stream_connected, }) } - /// Get the next event ID. - fn next_event_id(&self) -> u64 { - self.event_id_counter.fetch_add(1, Ordering::Relaxed) + fn next_event_id(&mut self) -> u64 { + let id = self.event_id_counter; + self.event_id_counter += 1; + id } - /// Get the current iteration ID. - pub fn current_iteration_id(&self) -> u64 { - self.iteration_id.load(Ordering::Relaxed) - } - - /// Send a CommitHead event (previous block finalized). - pub async fn send_commit_head( - &self, - commit_head: CommitHead, - ) -> Result<(), CredibleLayerError> { + async fn send_event(&mut self, event_payload: sidecar_proto::event::Event) { let event = Event { event_id: self.next_event_id(), - event: Some(sidecar_proto::event::Event::CommitHead(commit_head)), + event: Some(event_payload), }; - self.event_sender - .send(event) - .await - .map_err(|_| CredibleLayerError::StreamClosed) + if self.event_sender.send(event).await.is_err() { + warn!("Failed to send event: stream channel closed"); + } } - /// Send a NewIteration event (new block started) and increment the iteration ID. - pub async fn send_new_iteration( - &self, - new_iteration: NewIteration, - ) -> Result<(), CredibleLayerError> { - self.iteration_id.fetch_add(1, Ordering::Relaxed); - let event = Event { - event_id: self.next_event_id(), - event: Some(sidecar_proto::event::Event::NewIteration(new_iteration)), + #[send_handler] + async fn handle_new_iteration( + &mut self, + msg: credible_layer_protocol::NewIteration, + _ctx: &Context, + ) { + self.iteration_id += 1; + let header = &msg.header; + + let block_env = BlockEnv { + number: u64_to_u256_bytes(header.number), + beneficiary: header.coinbase.as_bytes().to_vec(), + timestamp: u64_to_u256_bytes(header.timestamp), + gas_limit: header.gas_limit, + basefee: header.base_fee_per_gas.unwrap_or(0), + difficulty: header.difficulty.to_big_endian().to_vec(), + prevrandao: Some(header.prev_randao.to_fixed_bytes().to_vec()), + blob_excess_gas_and_price: Some(BlobExcessGasAndPrice { + excess_blob_gas: 0, + blob_gasprice: vec![0u8; 16], + }), }; - self.event_sender - .send(event) - .await - .map_err(|_| CredibleLayerError::StreamClosed) + let new_iteration = NewIteration { + block_env: Some(block_env), + iteration_id: self.iteration_id, + parent_block_hash: Some(header.parent_hash.to_fixed_bytes().to_vec()), + parent_beacon_block_root: header + .parent_beacon_block_root + .map(|h| h.to_fixed_bytes().to_vec()), + }; + self.send_event(sidecar_proto::event::Event::NewIteration(new_iteration)) + .await; } - /// Send a Transaction event and wait for the sidecar's verdict. - /// - /// Returns `true` if the transaction should be included, `false` if it should be dropped. - /// On any error or timeout, returns `true` (permissive behavior). - pub async fn evaluate_transaction(&self, transaction: Transaction) -> bool { - // Fast path: if the stream isn't connected, skip evaluation (permissive). - // This avoids blocking block production when the sidecar isn't running. + #[send_handler] + async fn handle_commit_head( + &mut self, + msg: credible_layer_protocol::CommitHead, + _ctx: &Context, + ) { + let commit_head = CommitHead { + last_tx_hash: msg.last_tx_hash.map(|h| h.to_fixed_bytes().to_vec()), + n_transactions: msg.tx_count, + block_number: u64_to_u256_bytes(msg.block_number), + selected_iteration_id: self.iteration_id, + block_hash: Some(msg.block_hash.to_fixed_bytes().to_vec()), + parent_beacon_block_root: None, + timestamp: u64_to_u256_bytes(msg.timestamp), + }; + self.send_event(sidecar_proto::event::Event::CommitHead(commit_head)) + .await; + } + + #[send_handler] + async fn handle_send_transaction( + &mut self, + msg: credible_layer_protocol::SendTransaction, + _ctx: &Context, + ) { if !self.stream_connected.load(Ordering::Relaxed) { - return true; + return; } - - let tx_exec_id = transaction.tx_execution_id.clone(); - let tx_hash = tx_exec_id - .as_ref() - .map(|id| id.tx_hash.clone()) - .unwrap_or_default(); - let block_number = tx_exec_id - .as_ref() - .map(|id| id.block_number.clone()) - .unwrap_or_default(); - let index = tx_exec_id.as_ref().map(|id| id.index).unwrap_or(0); - - // Send the transaction event on the persistent stream - let event = Event { - event_id: self.next_event_id(), - event: Some(sidecar_proto::event::Event::Transaction(transaction)), + let tx_execution_id = Some(TxExecutionId { + block_number: u64_to_u256_bytes(msg.block_number), + iteration_id: self.iteration_id, + tx_hash: msg.tx_hash.as_bytes().to_vec(), + index: msg.tx_index, + }); + let tx_env = build_transaction_env(&msg.tx, msg.sender); + let sidecar_tx = SidecarTransaction { + tx_execution_id, + tx_env: Some(tx_env), + prev_tx_hash: None, }; - if self.event_sender.send(event).await.is_err() { - warn!("StreamEvents channel closed, including tx (permissive)"); + self.send_event(sidecar_proto::event::Event::Transaction(sidecar_tx)) + .await; + } + + #[request_handler] + async fn handle_check_transaction( + &mut self, + msg: credible_layer_protocol::CheckTransaction, + _ctx: &Context, + ) -> bool { + if !self.stream_connected.load(Ordering::Relaxed) { return true; } - // Poll for result with retries (sidecar evaluates async). - // The sidecar needs time to receive the tx event (via async stream), - // evaluate it, and make the result available. + let tx_exec_id = TxExecutionId { + block_number: u64_to_u256_bytes(msg.block_number), + iteration_id: self.iteration_id, + tx_hash: msg.tx_hash.as_bytes().to_vec(), + index: msg.tx_index, + }; + let poll_attempts = 10; let poll_interval = Duration::from_millis(200); + let poll_timeout = Duration::from_millis(200); + for attempt in 0..poll_attempts { tokio::time::sleep(poll_interval).await; - let result = self - .poll_transaction_result(&tx_hash, &block_number, index) - .await; - match result { - PollResult::Found(include) => return include, - PollResult::NotFound => { - debug!( - "GetTransaction poll attempt {}/{}: not found yet", - attempt + 1, - poll_attempts - ); - continue; + let request = GetTransactionRequest { + tx_execution_id: Some(tx_exec_id.clone()), + }; + match tokio::time::timeout(poll_timeout, self.grpc_client.get_transaction(request)) + .await + { + Ok(Ok(response)) => { + let inner = response.into_inner(); + match inner.outcome { + Some(sidecar_proto::get_transaction_response::Outcome::Result(result)) => { + return result.status() != ResultStatus::AssertionFailed; + } + Some(sidecar_proto::get_transaction_response::Outcome::NotFound(_)) => { + debug!( + "GetTransaction poll attempt {}/{}: not found yet", + attempt + 1, + poll_attempts + ); + continue; + } + None => continue, + } + } + Ok(Err(status)) => { + warn!(%status, "GetTransaction poll failed, including tx (permissive)"); + return true; + } + Err(_) => { + warn!("GetTransaction poll timed out, including tx (permissive)"); + return true; } - PollResult::Error => return true, // permissive } } warn!( @@ -281,200 +341,83 @@ impl CredibleLayerClient { ); true } - - /// Poll for a transaction result via GetTransaction unary RPC. - async fn poll_transaction_result( - &self, - tx_hash: &[u8], - block_number: &[u8], - index: u64, - ) -> PollResult { - let tx_exec_id = TxExecutionId { - block_number: block_number.to_vec(), - iteration_id: self.current_iteration_id(), - tx_hash: tx_hash.to_vec(), - index, - }; - - let request = GetTransactionRequest { - tx_execution_id: Some(tx_exec_id), - }; - - let poll_result = tokio::time::timeout(self.config.poll_timeout, async { - let mut client = self.grpc_client.lock().await; - client.get_transaction(request).await - }) - .await; - - match poll_result { - Ok(Ok(response)) => { - let inner = response.into_inner(); - match inner.outcome { - Some(sidecar_proto::get_transaction_response::Outcome::Result(result)) => { - PollResult::Found(!is_assertion_failed(&result)) - } - Some(sidecar_proto::get_transaction_response::Outcome::NotFound(_)) => { - PollResult::NotFound - } - None => PollResult::NotFound, - } - } - Ok(Err(status)) => { - warn!(%status, "GetTransaction poll failed, including tx (permissive)"); - PollResult::Error - } - Err(_) => { - warn!("GetTransaction poll timed out, including tx (permissive)"); - PollResult::Error - } - } - } } -enum PollResult { - Found(bool), // true = include, false = drop - NotFound, - Error, +/// Encode a u64 as a 32-byte big-endian U256 for protobuf fields. +fn u64_to_u256_bytes(value: u64) -> Vec { + let mut buf = [0u8; 32]; + buf[24..].copy_from_slice(&value.to_be_bytes()); + buf.to_vec() } -/// Check if a TransactionResult indicates assertion failure. -fn is_assertion_failed(result: &TransactionResult) -> bool { - result.status() == ResultStatus::AssertionFailed -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use super::*; - use crate::sequencer::credible_layer::sidecar_proto::{ - CommitHead, NewIteration, ResultStatus, TransactionResult, TxExecutionId, +/// Build a `TransactionEnv` protobuf message from an ethrex transaction and its sender. +fn build_transaction_env(tx: &Transaction, sender: Address) -> TransactionEnv { + let transact_to = match tx.to() { + TxKind::Call(addr) => addr.as_bytes().to_vec(), + TxKind::Create => vec![], }; - #[test] - fn config_default_url_is_localhost_50051() { - let cfg = CredibleLayerConfig::default(); - assert_eq!(cfg.sidecar_url, "http://localhost:50051"); - } - - #[test] - fn config_default_result_timeout_is_500ms() { - let cfg = CredibleLayerConfig::default(); - assert_eq!(cfg.result_timeout, Duration::from_millis(500)); - } - - #[test] - fn config_default_poll_timeout_is_200ms() { - let cfg = CredibleLayerConfig::default(); - assert_eq!(cfg.poll_timeout, Duration::from_millis(200)); - } + let value_bytes = tx.value().to_big_endian(); - fn make_result(status: ResultStatus) -> TransactionResult { - TransactionResult { - tx_execution_id: None, - status: status as i32, - gas_used: 0, - error: String::new(), - } - } + let mut gas_price_bytes = [0u8; 16]; + let gas_price_u128 = tx.gas_price().as_u128(); + gas_price_bytes.copy_from_slice(&gas_price_u128.to_be_bytes()); - #[test] - fn assertion_failed_returns_true_for_assertion_failed_status() { - assert!(is_assertion_failed(&make_result( - ResultStatus::AssertionFailed - ))); - } + let gas_priority_fee = tx.max_priority_fee().map(|fee| { + let mut buf = [0u8; 16]; + buf[8..].copy_from_slice(&fee.to_be_bytes()); + buf.to_vec() + }); - #[test] - fn assertion_failed_returns_false_for_success_status() { - assert!(!is_assertion_failed(&make_result(ResultStatus::Success))); - } - - #[test] - fn assertion_failed_returns_false_for_reverted_status() { - assert!(!is_assertion_failed(&make_result(ResultStatus::Reverted))); - } - - #[test] - fn assertion_failed_returns_false_for_halted_status() { - assert!(!is_assertion_failed(&make_result(ResultStatus::Halted))); - } - - #[test] - fn assertion_failed_returns_false_for_failed_status() { - assert!(!is_assertion_failed(&make_result(ResultStatus::Failed))); - } - - #[test] - fn assertion_failed_returns_false_for_unspecified_status() { - assert!(!is_assertion_failed(&make_result( - ResultStatus::Unspecified - ))); - } - - #[test] - fn commit_head_fields_are_set_correctly() { - let block_number: Vec = std::iter::repeat(0u8) - .take(31) - .chain(std::iter::once(42u8)) - .collect(); - let timestamp: Vec = std::iter::repeat(0u8) - .take(31) - .chain(std::iter::once(1u8)) - .collect(); - let ch = CommitHead { - last_tx_hash: None, - n_transactions: 5, - block_number: block_number.clone(), - selected_iteration_id: 3, - block_hash: None, - parent_beacon_block_root: None, - timestamp: timestamp.clone(), - }; - assert_eq!(ch.n_transactions, 5); - assert_eq!(ch.block_number, block_number); - assert_eq!(ch.selected_iteration_id, 3); - } - - #[test] - fn tx_execution_id_fields_are_set_correctly() { - let id = TxExecutionId { - block_number: vec![0; 32], - iteration_id: 7, - tx_hash: vec![0xab; 32], - index: 2, - }; - assert_eq!(id.iteration_id, 7); - assert_eq!(id.index, 2); - } - - #[test] - fn new_iteration_has_expected_iteration_id() { - use crate::sequencer::credible_layer::sidecar_proto::BlockEnv; - let ni = NewIteration { - block_env: Some(BlockEnv { - number: vec![0; 32], - beneficiary: vec![0; 20], - timestamp: vec![0; 32], - gas_limit: 30_000_000, - basefee: 1_000_000_000, - difficulty: vec![0; 32], - prevrandao: None, - blob_excess_gas_and_price: None, - }), - iteration_id: 42, - parent_block_hash: None, - parent_beacon_block_root: None, - }; - assert_eq!(ni.iteration_id, 42); - } - - #[test] - fn event_id_counter_increments() { - use std::sync::atomic::{AtomicU64, Ordering}; - let c = AtomicU64::new(1); - assert_eq!(c.fetch_add(1, Ordering::Relaxed), 1); - assert_eq!(c.fetch_add(1, Ordering::Relaxed), 2); - assert_eq!(c.fetch_add(1, Ordering::Relaxed), 3); + let access_list = tx + .access_list() + .iter() + .map(|(addr, keys)| AccessListItem { + address: addr.as_bytes().to_vec(), + storage_keys: keys.iter().map(|k| k.as_bytes().to_vec()).collect(), + }) + .collect(); + + let authorization_list = tx + .authorization_list() + .map(|list| { + list.iter() + .map(|auth| { + let chain_id_bytes = auth.chain_id.to_big_endian(); + let r_bytes = auth.r_signature.to_big_endian(); + let s_bytes = auth.s_signature.to_big_endian(); + Authorization { + chain_id: chain_id_bytes.to_vec(), + address: auth.address.as_bytes().to_vec(), + nonce: auth.nonce, + y_parity: auth.y_parity.as_u32(), + r: r_bytes.to_vec(), + s: s_bytes.to_vec(), + } + }) + .collect() + }) + .unwrap_or_default(); + + #[allow(clippy::as_conversions)] + TransactionEnv { + tx_type: u8::from(tx.tx_type()) as u32, + caller: sender.as_bytes().to_vec(), + gas_limit: tx.gas_limit(), + gas_price: gas_price_bytes.to_vec(), + transact_to, + value: value_bytes.to_vec(), + data: tx.data().to_vec(), + nonce: tx.nonce(), + chain_id: tx.chain_id(), + access_list, + gas_priority_fee, + blob_hashes: tx + .blob_versioned_hashes() + .iter() + .map(|h| h.as_bytes().to_vec()) + .collect(), + max_fee_per_blob_gas: vec![0u8; 16], + authorization_list, } } diff --git a/crates/l2/sequencer/credible_layer/mod.rs b/crates/l2/sequencer/credible_layer/mod.rs index c8e28cdfb9a..d180697e2aa 100644 --- a/crates/l2/sequencer/credible_layer/mod.rs +++ b/crates/l2/sequencer/credible_layer/mod.rs @@ -1,10 +1,10 @@ /// Credible Layer integration with the Phylax Credible Layer. /// -/// This module implements a gRPC client that communicates with the Credible Layer +/// This module implements a gRPC client actor that communicates with the Credible Layer /// Assertion Enforcer sidecar during block building. Transactions that fail assertion /// validation are dropped before block inclusion. /// -/// The integration is opt-in via the `--credible-layer-url` CLI flag. +/// The integration is opt-in via the `--credible-layer` CLI flag. /// When disabled, there is zero overhead. pub mod client; pub mod errors; diff --git a/crates/l2/sequencer/mod.rs b/crates/l2/sequencer/mod.rs index 6861db1a974..99239f28b64 100644 --- a/crates/l2/sequencer/mod.rs +++ b/crates/l2/sequencer/mod.rs @@ -23,7 +23,7 @@ use reqwest::Url; use spawned_concurrency::tasks::ActorRef; use std::pin::Pin; use tokio_util::sync::CancellationToken; -use tracing::{error, info}; +use tracing::{error, info, warn}; use utils::get_needed_proof_types; mod admin_server; @@ -155,6 +155,22 @@ pub async fn start_l2( .inspect_err(|err| { error!("Error starting L1 Proof Sender: {err}"); }); + let credible_layer = if let Some(url) = cfg.credible_layer.sidecar_url.clone() { + match credible_layer::CredibleLayerClient::spawn(url).await { + Ok(actor_ref) => { + info!("Credible Layer sidecar client started"); + Some(actor_ref) + } + Err(e) => { + warn!( + "Failed to start Credible Layer client: {e}. Proceeding without credible layer." + ); + None + } + } + } else { + None + }; let block_producer = BlockProducer::spawn( store.clone(), rollup_store.clone(), @@ -163,6 +179,7 @@ pub async fn start_l2( shared_state.clone(), cfg.l1_watcher.router_address, l2_gas_limit, + credible_layer, subscription_manager, ) .await diff --git a/crates/l2/sidecar-config.template.json b/crates/l2/sidecar-config.template.json deleted file mode 100644 index 9974538a653..00000000000 --- a/crates/l2/sidecar-config.template.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "chain": { - "spec_id": "CANCUN", - "chain_id": 1729 - }, - "credible": { - "assertion_gas_limit": 3000000, - "cache_capacity_bytes": 256000000, - "flush_every_ms": 5000, - "assertion_da_rpc_url": "http://127.0.0.1:5001", - "event_source_url": "http://127.0.0.1:4350/graphql", - "poll_interval": 1000, - "assertion_store_db_path": ".local/sidecar/assertion_store_database", - "transaction_observer_db_path": ".local/sidecar/transaction_observer_database", - "transaction_observer_endpoint": null, - "transaction_observer_auth_token": "", - "transaction_observer_endpoint_rps_max": 60, - "transaction_observer_poll_interval_ms": 1000, - "transaction_observer_backoff_initial_ms": 1000, - "transaction_observer_backoff_max_ms": 5000, - "transaction_observer_backoff_multiplier": 2, - "aeges_url": "http://127.0.0.1:8080", - "state_oracle": "0x__STATE_ORACLE_ADDRESS__", - "state_oracle_deployment_block": 0, - "transaction_results_max_capacity": 1000, - "accepted_txs_ttl_ms": 600000, - "assertion_store_prune_config_interval_ms": 60000, - "assertion_store_prune_config_retention_blocks": 0 - }, - "transport": { - "bind_addr": "0.0.0.0:50051", - "health_bind_addr": "0.0.0.0:9547" - }, - "state": { - "sources": [ - { - "type": "eth-rpc", - "ws_url": "ws://127.0.0.1:1730", - "http_url": "http://127.0.0.1:1729" - } - ], - "minimum_state_diff": 100, - "sources_sync_timeout_ms": 1000, - "sources_monitoring_period_ms": 500, - "enable_parallel_sources": false - } -} diff --git a/docs/CLI.md b/docs/CLI.md index 5a89fd7d9a2..9b2e30df2fa 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -686,9 +686,13 @@ L2 options: [default: 1729] Credible Layer options: + --credible-layer + Enable the Credible Layer integration. Required before using any --credible-layer-* flags. + + [env: ETHREX_CREDIBLE_LAYER=] + --credible-layer-url - gRPC endpoint for the Credible Layer Assertion Enforcer sidecar. - When set, enables transaction validation against assertions during block building. + gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). [env: ETHREX_CREDIBLE_LAYER_URL=] diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index 1c03cfcbc8c..fa0f2bb0c2a 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -32,10 +32,11 @@ Per block: 1. **CommitHead** → sidecar (previous block finalized, with block hash and tx count) 2. **NewIteration** → sidecar (new block env: number, timestamp, coinbase, gas limit, basefee) 3. For each candidate transaction: - - **Transaction** → sidecar (tx data: type, caller, gas, to, value, data, nonce, ...) - - ← **TransactionResult** (status: SUCCESS, ASSERTION_FAILED, etc.) - - If `ASSERTION_FAILED`: skip the transaction - - Otherwise: include it + - **Transaction** → sidecar (pre-execution: tx data sent via StreamEvents) + - Execute the transaction in the local EVM + - ← **GetTransaction** poll (post-execution: fetch the sidecar's verdict) + - If `ASSERTION_FAILED`: undo execution and drop the transaction + - Otherwise: keep it in the block Privileged transactions (L1→L2 deposits) bypass the Credible Layer in this first version of the integration. @@ -48,7 +49,7 @@ Privileged transactions (L1→L2 deposits) bypass the Credible Layer in this fir | File | Role | |------|------| | `crates/l2/sequencer/credible_layer/mod.rs` | Module root, proto imports | -| `crates/l2/sequencer/credible_layer/client.rs` | `CredibleLayerClient` — gRPC client for the sidecar; handles block building validation (StreamEvents, GetTransaction) | +| `crates/l2/sequencer/credible_layer/client.rs` | `CredibleLayerClient` — GenServer actor wrapping the gRPC sidecar client (StreamEvents, GetTransaction) | | `crates/l2/sequencer/credible_layer/errors.rs` | Error types | | `crates/l2/proto/sidecar.proto` | Sidecar gRPC protocol definition | | `crates/l2/sequencer/block_producer.rs` | Block producer — sends CommitHead + NewIteration | @@ -63,7 +64,6 @@ CLI flags: |------|-------------|---------| | `--credible-layer` | Enable the Credible Layer integration (required gate flag) | `false` | | `--credible-layer-url` | gRPC endpoint for the sidecar (requires `--credible-layer`) | (none) | -| `--credible-layer-state-oracle` | Address of the deployed State Oracle contract on L2 (requires `--credible-layer`) | (none) | When `--credible-layer` is not set, Credible Layer is completely disabled with zero overhead. @@ -453,5 +453,4 @@ rm -rf /tmp/credible-layer-contracts /tmp/credible-layer-starter /tmp/sidecar-in - [Linea/Besu Integration](https://docs.phylax.systems/credible/network-integrations/architecture-linea) - [credible-std Library](https://github.com/phylaxsystems/credible-std) - [Besu Plugin Reference](https://github.com/phylaxsystems/credible-layer-besu-plugin) -- [credible-sdk (sidecar source)](https://github.com/phylaxsystems/credible-sdk) - [sidecar.proto](../../crates/l2/proto/sidecar.proto) From 7810bf5e45ec11c3e7ae965a516b2e6f78525fb2 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 14:45:46 -0300 Subject: [PATCH 17/33] Simplify Credible Layer CLI to a single --credible-layer-url flag (passing it enables the integration, removing the separate --credible-layer boolean gate) and replace #[allow(clippy::as_conversions)] with proper try_into conversions that return errors instead of panicking. --- cmd/ethrex/l2/options.rs | 21 +++---------------- crates/l2/sequencer/block_producer.rs | 6 ++++-- .../block_producer/payload_builder.rs | 18 ++++++++++++---- crates/l2/sequencer/credible_layer/client.rs | 3 +-- docs/CLI.md | 6 +----- docs/l2/credible_layer.md | 6 ++---- 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index 0c58d2f140d..79f6c6d444c 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -265,12 +265,8 @@ impl TryFrom for SequencerConfig { start_at: opts.state_updater_opts.start_at, l2_head_check_rpc_url: opts.state_updater_opts.l2_head_check_rpc_url, }, - credible_layer: if opts.credible_layer_opts.credible_layer { - CredibleLayerConfig { - sidecar_url: opts.credible_layer_opts.credible_layer_url, - } - } else { - CredibleLayerConfig::default() + credible_layer: CredibleLayerConfig { + sidecar_url: opts.credible_layer_opts.credible_layer_url, }, }) } @@ -307,7 +303,6 @@ impl SequencerOptions { self.state_updater_opts .populate_with_defaults(&defaults.state_updater_opts); // admin_opts contains only non-optional fields. - // credible_layer_opts contains only optional fields, nothing to populate. } } @@ -1078,21 +1073,11 @@ impl Default for AdminOptions { #[derive(Parser, Default, Debug)] pub struct CredibleLayerOptions { - #[arg( - long = "credible-layer", - action = clap::ArgAction::SetTrue, - default_value = "false", - env = "ETHREX_CREDIBLE_LAYER", - help = "Enable the Credible Layer integration. Required before using any --credible-layer-* flags.", - help_heading = "Credible Layer options" - )] - pub credible_layer: bool, #[arg( long = "credible-layer-url", value_name = "URL", env = "ETHREX_CREDIBLE_LAYER_URL", - requires = "credible_layer", - help = "gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051).", + help = "gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). Passing this flag enables the integration.", help_heading = "Credible Layer options" )] pub credible_layer_url: Option, diff --git a/crates/l2/sequencer/block_producer.rs b/crates/l2/sequencer/block_producer.rs index 450b9279c8b..de120515128 100644 --- a/crates/l2/sequencer/block_producer.rs +++ b/crates/l2/sequencer/block_producer.rs @@ -244,12 +244,14 @@ impl BlockProducer { .ok() .flatten() .and_then(|b| b.body.transactions.last().map(|tx| tx.hash())); - #[allow(clippy::as_conversions)] + let tx_count: u64 = transactions_count + .try_into() + .map_err(|_| BlockProducerError::Custom("tx count overflow".into()))?; let _ = cl.commit_head( block_number, block_hash, block_header.timestamp, - transactions_count as u64, + tx_count, last_tx_hash, ); } diff --git a/crates/l2/sequencer/block_producer/payload_builder.rs b/crates/l2/sequencer/block_producer/payload_builder.rs index ab29d54f041..37b30aac364 100644 --- a/crates/l2/sequencer/block_producer/payload_builder.rs +++ b/crates/l2/sequencer/block_producer/payload_builder.rs @@ -221,8 +221,13 @@ pub async fn fill_transactions( // transactions for accurate state tracking, even if privileged txs are never dropped. if let Some(ref cl) = credible_layer { if !head_tx.is_privileged() { - #[allow(clippy::as_conversions)] - let tx_index = context.payload.body.transactions.len() as u64; + let tx_index: u64 = context + .payload + .body + .transactions + .len() + .try_into() + .map_err(|_| BlockProducerError::Custom("tx index overflow".into()))?; let _ = cl.send_transaction( tx_hash, context.block_number(), @@ -265,8 +270,13 @@ pub async fn fill_transactions( // If the sidecar rejected the transaction, undo execution and drop it. if let Some(ref cl) = credible_layer { if !head_tx.is_privileged() { - #[allow(clippy::as_conversions)] - let check_tx_index = context.payload.body.transactions.len() as u64; + let check_tx_index: u64 = context + .payload + .body + .transactions + .len() + .try_into() + .map_err(|_| BlockProducerError::Custom("tx index overflow".into()))?; let include = cl .check_transaction(tx_hash, context.block_number(), check_tx_index) .await diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index ab98b3d82db..5f1d08bfea0 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -399,9 +399,8 @@ fn build_transaction_env(tx: &Transaction, sender: Address) -> TransactionEnv { }) .unwrap_or_default(); - #[allow(clippy::as_conversions)] TransactionEnv { - tx_type: u8::from(tx.tx_type()) as u32, + tx_type: u32::from(u8::from(tx.tx_type())), caller: sender.as_bytes().to_vec(), gas_limit: tx.gas_limit(), gas_price: gas_price_bytes.to_vec(), diff --git a/docs/CLI.md b/docs/CLI.md index 9b2e30df2fa..8a1add1640a 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -686,13 +686,9 @@ L2 options: [default: 1729] Credible Layer options: - --credible-layer - Enable the Credible Layer integration. Required before using any --credible-layer-* flags. - - [env: ETHREX_CREDIBLE_LAYER=] - --credible-layer-url gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). + Passing this flag enables the integration. [env: ETHREX_CREDIBLE_LAYER_URL=] diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index fa0f2bb0c2a..9dfe99e068e 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -62,10 +62,9 @@ CLI flags: | Flag | Description | Default | |------|-------------|---------| -| `--credible-layer` | Enable the Credible Layer integration (required gate flag) | `false` | -| `--credible-layer-url` | gRPC endpoint for the sidecar (requires `--credible-layer`) | (none) | +| `--credible-layer-url` | gRPC endpoint for the sidecar. Passing this flag enables the integration. | (none) | -When `--credible-layer` is not set, Credible Layer is completely disabled with zero overhead. +When `--credible-layer-url` is not set, Credible Layer is completely disabled with zero overhead. ### Sidecar Requirements @@ -147,7 +146,6 @@ RUST_LOG=info ../../target/release/ethrex l2 \ --block-producer.coinbase-address 0x0007a881CD95B1484fca47615B64803dad620C8d \ --committer.l1-private-key 0x385c546456b6a603a1cfcaa9ec9494ba4832da08dd6bcf4de9a71e4a01b74924 \ --proof-coordinator.l1-private-key 0x39725efee3fb28614de3bacaffe4cc4bd8c436257e2c8bb887c4b5c4be45e76d \ - --credible-layer \ --credible-layer-url http://localhost:50051 \ --ws.enabled --ws.port 1730 & From b128f0cd02e6f5445c7fc709a7833632f512c76f Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 15:28:23 -0300 Subject: [PATCH 18/33] =?UTF-8?q?Replace=20raw=20tokio::spawn=20in=20Credi?= =?UTF-8?q?bleLayerClient=20with=20proper=20spawned=5Fconcurrency=20primit?= =?UTF-8?q?ives:=20connection=20is=20established=20in=20#[started],=20the?= =?UTF-8?q?=20ack=20stream=20is=20bridged=20into=20actor=20messages=20via?= =?UTF-8?q?=20spawn=5Flistener,=20and=20reconnection=20on=20failure=20is?= =?UTF-8?q?=20scheduled=20with=20send=5Fafter.=20This=20ties=20the=20backg?= =?UTF-8?q?round=20stream=20lifecycle=20to=20the=20actor=20=E2=80=94=20whe?= =?UTF-8?q?n=20the=20actor=20stops,=20the=20stream=20listener=20is=20autom?= =?UTF-8?q?atically=20cancelled.=20The=20Arc=20for=20tracking?= =?UTF-8?q?=20connection=20state=20is=20replaced=20by=20a=20plain=20bool?= =?UTF-8?q?=20in=20the=20actor's=20owned=20state.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/l2/sequencer/credible_layer/client.rs | 273 +++++++++++-------- 1 file changed, 164 insertions(+), 109 deletions(-) diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 5f1d08bfea0..3e1cb02ea49 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use ethrex_common::types::{BlockHeader, Transaction, TxKind}; @@ -8,20 +6,22 @@ use spawned_concurrency::{ actor, error::ActorError, protocol, - tasks::{Actor, ActorRef, ActorStart as _, Context, Handler, Response}, + tasks::{ + Actor, ActorRef, ActorStart as _, Context, Handler, Response, send_after, spawn_listener, + }, }; use tokio::sync::mpsc; +use tokio_stream::StreamExt; use tonic::transport::Channel; use tracing::{debug, info, warn}; +use super::errors::CredibleLayerError; use super::sidecar_proto::{ self, AccessListItem, Authorization, BlobExcessGasAndPrice, BlockEnv, CommitHead, Event, GetTransactionRequest, NewIteration, ResultStatus, Transaction as SidecarTransaction, TransactionEnv, TxExecutionId, sidecar_transport_client::SidecarTransportClient, }; -use super::errors::CredibleLayerError; - #[protocol] pub trait CredibleLayerProtocol: Send + Sync { /// Notify the sidecar that a new block iteration has started. @@ -51,16 +51,31 @@ pub trait CredibleLayerProtocol: Send + Sync { /// Returns `true` if the transaction should be included, `false` if it should be dropped. /// On any error or timeout, returns `true` (permissive — liveness over safety). fn check_transaction(&self, tx_hash: H256, block_number: u64, tx_index: u64) -> Response; + + /// Attempt to (re)connect to the sidecar. Scheduled by `send_after` on failure. + fn reconnect(&self) -> Result<(), ActorError>; + + /// Process a stream ack from the sidecar. When `disconnected` is true, + /// the stream has ended and a reconnect is scheduled. + fn handle_stream_ack( + &self, + disconnected: bool, + success: bool, + event_id: u64, + message: String, + ) -> Result<(), ActorError>; } /// gRPC client actor for communicating with the Credible Layer Assertion Enforcer sidecar. /// -/// Maintains a persistent bidirectional `StreamEvents` gRPC stream via a background task. -/// Events are sent through an mpsc channel that feeds the stream. Transaction results -/// are retrieved via the `GetTransaction` unary RPC. +/// Maintains a persistent bidirectional `StreamEvents` gRPC stream. Connection is +/// established in `#[started]` and ack messages are bridged into the actor via +/// `spawn_listener`. Reconnection on failure is scheduled with `send_after`. pub struct CredibleLayerClient { - /// Sender side of the persistent StreamEvents stream - event_sender: mpsc::Sender, + /// gRPC channel (lazy — shared between stream and unary clients) + channel: Channel, + /// Sender for forwarding events to the active gRPC stream. None when disconnected. + event_sender: Option>, /// Monotonically increasing event ID counter event_id_counter: u64, /// Current iteration ID (incremented per block) @@ -68,19 +83,18 @@ pub struct CredibleLayerClient { /// gRPC client for unary calls (GetTransaction) grpc_client: SidecarTransportClient, /// Whether the StreamEvents stream is currently connected. - /// When false, send handlers skip immediately (permissive). - stream_connected: Arc, + connected: bool, } #[actor(protocol = CredibleLayerProtocol)] impl CredibleLayerClient { /// Spawn the Credible Layer client actor. pub async fn spawn(sidecar_url: String) -> Result, CredibleLayerError> { - let client = Self::new(sidecar_url).await?; + let client = Self::new(sidecar_url)?; Ok(client.start()) } - async fn new(sidecar_url: String) -> Result { + fn new(sidecar_url: String) -> Result { info!(url = %sidecar_url, "Configuring Credible Layer sidecar client"); let channel = Channel::from_shared(sidecar_url) @@ -89,116 +103,157 @@ impl CredibleLayerClient { .timeout(Duration::from_secs(5)) .connect_lazy(); - let mut stream_client = SidecarTransportClient::new(channel.clone()); - let stream_connected = Arc::new(AtomicBool::new(false)); - let stream_connected_bg = stream_connected.clone(); - - let (event_tx, mut event_rx) = mpsc::channel::(256); - - // Background task: maintains a persistent StreamEvents connection. - // Reads events from the mpsc channel and forwards them to the gRPC stream. - // Reconnects automatically if the connection drops. - tokio::spawn(async move { - loop { - let (grpc_tx, grpc_rx) = mpsc::channel::(64); - let grpc_stream = tokio_stream::wrappers::ReceiverStream::new(grpc_rx); - - match stream_client.stream_events(grpc_stream).await { - Ok(response) => { - info!("StreamEvents stream connected to sidecar"); - stream_connected_bg.store(true, Ordering::Relaxed); - let mut ack_stream = response.into_inner(); - - // Send an initial CommitHead (block 0) — the sidecar requires - // CommitHead as the first event on every new stream. - let init_commit = Event { - event_id: 0, - event: Some(sidecar_proto::event::Event::CommitHead(CommitHead { - last_tx_hash: None, - n_transactions: 0, - block_number: vec![0u8; 32], - selected_iteration_id: 0, - block_hash: Some(vec![0u8; 32]), - parent_beacon_block_root: None, - timestamp: vec![0u8; 32], - })), - }; - if grpc_tx.send(init_commit).await.is_err() { - warn!("Failed to send initial CommitHead"); - continue; - } - - // Forward events from the main channel to the gRPC stream - // while also reading acks - loop { - tokio::select! { - event = event_rx.recv() => { - match event { - Some(e) => { - if grpc_tx.send(e).await.is_err() { - warn!("gRPC stream send failed, reconnecting"); - break; - } - } - None => { - warn!("Event channel closed, stopping stream task"); - return; - } - } - } - ack = ack_stream.message() => { - match ack { - Ok(Some(a)) => { - if !a.success { - warn!(event_id = a.event_id, msg = %a.message, "Sidecar rejected event"); - } - } - Ok(None) => { - info!("StreamEvents ack stream ended, reconnecting"); - break; - } - Err(status) => { - warn!(%status, "StreamEvents ack error, reconnecting"); - break; - } - } - } - } - } - } - Err(status) => { - warn!(%status, "StreamEvents connect failed, retrying in 5s"); - } - } - stream_connected_bg.store(false, Ordering::Relaxed); - tokio::time::sleep(Duration::from_secs(5)).await; - } - }); - - info!("Credible Layer client ready (persistent stream opened)"); - Ok(Self { - event_sender: event_tx, + grpc_client: SidecarTransportClient::new(channel.clone()), + channel, + event_sender: None, event_id_counter: 1, iteration_id: 0, - grpc_client: SidecarTransportClient::new(channel), - stream_connected, + connected: false, }) } + /// Attempt to establish a bidirectional StreamEvents connection with the sidecar. + /// On success, stores the event sender and spawns an ack listener. + /// On failure, schedules a retry via `send_after`. + async fn try_connect(&mut self, ctx: &Context) { + let mut stream_client = SidecarTransportClient::new(self.channel.clone()); + let (tx, rx) = mpsc::channel::(64); + let grpc_stream = tokio_stream::wrappers::ReceiverStream::new(rx); + + match stream_client.stream_events(grpc_stream).await { + Ok(response) => { + info!("StreamEvents stream connected to sidecar"); + + // The sidecar requires CommitHead as the first event on every new stream. + let init_commit = Event { + event_id: 0, + event: Some(sidecar_proto::event::Event::CommitHead(CommitHead { + last_tx_hash: None, + n_transactions: 0, + block_number: vec![0u8; 32], + selected_iteration_id: 0, + block_hash: Some(vec![0u8; 32]), + parent_beacon_block_root: None, + timestamp: vec![0u8; 32], + })), + }; + if tx.send(init_commit).await.is_err() { + warn!("Failed to send initial CommitHead, scheduling reconnect"); + send_after( + Duration::from_secs(5), + ctx.clone(), + credible_layer_protocol::Reconnect, + ); + return; + } + + self.event_sender = Some(tx); + self.connected = true; + + // Bridge the ack stream into actor messages via spawn_listener. + // When the stream ends or errors, a final "disconnected" message is sent. + let ack_stream = response.into_inner(); + let mapped = ack_stream + .map(|result| match result { + Ok(ack) => credible_layer_protocol::HandleStreamAck { + disconnected: false, + success: ack.success, + event_id: ack.event_id, + message: ack.message, + }, + Err(status) => credible_layer_protocol::HandleStreamAck { + disconnected: true, + success: false, + event_id: 0, + message: status.to_string(), + }, + }) + .chain(tokio_stream::once( + credible_layer_protocol::HandleStreamAck { + disconnected: true, + success: false, + event_id: 0, + message: "ack stream ended".into(), + }, + )); + + spawn_listener(ctx.clone(), mapped); + } + Err(status) => { + warn!(%status, "StreamEvents connect failed, retrying in 5s"); + send_after( + Duration::from_secs(5), + ctx.clone(), + credible_layer_protocol::Reconnect, + ); + } + } + } + fn next_event_id(&mut self) -> u64 { let id = self.event_id_counter; self.event_id_counter += 1; id } + /// Send an event on the active gRPC stream. If the channel is closed, + /// marks the connection as disconnected. async fn send_event(&mut self, event_payload: sidecar_proto::event::Event) { let event = Event { event_id: self.next_event_id(), event: Some(event_payload), }; - if self.event_sender.send(event).await.is_err() { - warn!("Failed to send event: stream channel closed"); + if let Some(ref sender) = self.event_sender { + if sender.send(event).await.is_err() { + warn!("Event channel closed, marking disconnected"); + self.connected = false; + self.event_sender = None; + } + } + } + + #[started] + async fn started(&mut self, ctx: &Context) { + self.try_connect(ctx).await; + } + + #[send_handler] + async fn handle_reconnect( + &mut self, + _msg: credible_layer_protocol::Reconnect, + ctx: &Context, + ) { + if self.connected { + return; + } + self.try_connect(ctx).await; + } + + #[send_handler] + async fn handle_handle_stream_ack( + &mut self, + msg: credible_layer_protocol::HandleStreamAck, + ctx: &Context, + ) { + if msg.disconnected { + if !self.connected { + return; + } + info!("Sidecar stream disconnected: {}", msg.message); + self.connected = false; + self.event_sender = None; + send_after( + Duration::from_secs(5), + ctx.clone(), + credible_layer_protocol::Reconnect, + ); + } else if !msg.success { + warn!( + event_id = msg.event_id, + msg = %msg.message, + "Sidecar rejected event" + ); } } @@ -261,7 +316,7 @@ impl CredibleLayerClient { msg: credible_layer_protocol::SendTransaction, _ctx: &Context, ) { - if !self.stream_connected.load(Ordering::Relaxed) { + if !self.connected { return; } let tx_execution_id = Some(TxExecutionId { @@ -286,7 +341,7 @@ impl CredibleLayerClient { msg: credible_layer_protocol::CheckTransaction, _ctx: &Context, ) -> bool { - if !self.stream_connected.load(Ordering::Relaxed) { + if !self.connected { return true; } From 9a2cac6ca055b492c75932148290da49621bd100 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 15:59:04 -0300 Subject: [PATCH 19/33] Rename event_sender to stream_tx (it feeds the gRPC StreamEvents stream, not actor events) and rename handle_stream_ack protocol method to stream_ack to avoid the auto-generated handle_handle_stream_ack double prefix. Clarify the channel field comment. --- crates/l2/sequencer/credible_layer/client.rs | 40 ++++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 3e1cb02ea49..6ed9f002291 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -57,7 +57,7 @@ pub trait CredibleLayerProtocol: Send + Sync { /// Process a stream ack from the sidecar. When `disconnected` is true, /// the stream has ended and a reconnect is scheduled. - fn handle_stream_ack( + fn stream_ack( &self, disconnected: bool, success: bool, @@ -72,10 +72,10 @@ pub trait CredibleLayerProtocol: Send + Sync { /// established in `#[started]` and ack messages are bridged into the actor via /// `spawn_listener`. Reconnection on failure is scheduled with `send_after`. pub struct CredibleLayerClient { - /// gRPC channel (lazy — shared between stream and unary clients) + /// gRPC channel (lazy). Used to create new stream/unary clients. channel: Channel, - /// Sender for forwarding events to the active gRPC stream. None when disconnected. - event_sender: Option>, + /// Feeds events into the gRPC StreamEvents bidirectional stream. None when disconnected. + stream_tx: Option>, /// Monotonically increasing event ID counter event_id_counter: u64, /// Current iteration ID (incremented per block) @@ -106,7 +106,7 @@ impl CredibleLayerClient { Ok(Self { grpc_client: SidecarTransportClient::new(channel.clone()), channel, - event_sender: None, + stream_tx: None, event_id_counter: 1, iteration_id: 0, connected: false, @@ -148,7 +148,7 @@ impl CredibleLayerClient { return; } - self.event_sender = Some(tx); + self.stream_tx = Some(tx); self.connected = true; // Bridge the ack stream into actor messages via spawn_listener. @@ -156,27 +156,25 @@ impl CredibleLayerClient { let ack_stream = response.into_inner(); let mapped = ack_stream .map(|result| match result { - Ok(ack) => credible_layer_protocol::HandleStreamAck { + Ok(ack) => credible_layer_protocol::StreamAck { disconnected: false, success: ack.success, event_id: ack.event_id, message: ack.message, }, - Err(status) => credible_layer_protocol::HandleStreamAck { + Err(status) => credible_layer_protocol::StreamAck { disconnected: true, success: false, event_id: 0, message: status.to_string(), }, }) - .chain(tokio_stream::once( - credible_layer_protocol::HandleStreamAck { - disconnected: true, - success: false, - event_id: 0, - message: "ack stream ended".into(), - }, - )); + .chain(tokio_stream::once(credible_layer_protocol::StreamAck { + disconnected: true, + success: false, + event_id: 0, + message: "ack stream ended".into(), + })); spawn_listener(ctx.clone(), mapped); } @@ -204,11 +202,11 @@ impl CredibleLayerClient { event_id: self.next_event_id(), event: Some(event_payload), }; - if let Some(ref sender) = self.event_sender { + if let Some(ref sender) = self.stream_tx { if sender.send(event).await.is_err() { warn!("Event channel closed, marking disconnected"); self.connected = false; - self.event_sender = None; + self.stream_tx = None; } } } @@ -231,9 +229,9 @@ impl CredibleLayerClient { } #[send_handler] - async fn handle_handle_stream_ack( + async fn handle_stream_ack( &mut self, - msg: credible_layer_protocol::HandleStreamAck, + msg: credible_layer_protocol::StreamAck, ctx: &Context, ) { if msg.disconnected { @@ -242,7 +240,7 @@ impl CredibleLayerClient { } info!("Sidecar stream disconnected: {}", msg.message); self.connected = false; - self.event_sender = None; + self.stream_tx = None; send_after( Duration::from_secs(5), ctx.clone(), From b4777c496f6d89a83ea567dd68649f69729315f8 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 16:05:11 -0300 Subject: [PATCH 20/33] =?UTF-8?q?Remove=20redundant=20channel=20field=20fr?= =?UTF-8?q?om=20CredibleLayerClient=20=E2=80=94=20the=20grpc=5Fclient=20al?= =?UTF-8?q?ready=20wraps=20the=20channel=20internally=20and=20tonic=20clie?= =?UTF-8?q?nts=20are=20Clone,=20so=20try=5Fconnect=20just=20clones=20grpc?= =?UTF-8?q?=5Fclient=20for=20new=20stream=20connections.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/l2/sequencer/credible_layer/client.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 6ed9f002291..090b22dfa88 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -72,15 +72,13 @@ pub trait CredibleLayerProtocol: Send + Sync { /// established in `#[started]` and ack messages are bridged into the actor via /// `spawn_listener`. Reconnection on failure is scheduled with `send_after`. pub struct CredibleLayerClient { - /// gRPC channel (lazy). Used to create new stream/unary clients. - channel: Channel, /// Feeds events into the gRPC StreamEvents bidirectional stream. None when disconnected. stream_tx: Option>, /// Monotonically increasing event ID counter event_id_counter: u64, /// Current iteration ID (incremented per block) iteration_id: u64, - /// gRPC client for unary calls (GetTransaction) + /// gRPC client — used for unary GetTransaction calls and cloned for stream connections. grpc_client: SidecarTransportClient, /// Whether the StreamEvents stream is currently connected. connected: bool, @@ -104,8 +102,7 @@ impl CredibleLayerClient { .connect_lazy(); Ok(Self { - grpc_client: SidecarTransportClient::new(channel.clone()), - channel, + grpc_client: SidecarTransportClient::new(channel), stream_tx: None, event_id_counter: 1, iteration_id: 0, @@ -117,7 +114,7 @@ impl CredibleLayerClient { /// On success, stores the event sender and spawns an ack listener. /// On failure, schedules a retry via `send_after`. async fn try_connect(&mut self, ctx: &Context) { - let mut stream_client = SidecarTransportClient::new(self.channel.clone()); + let mut stream_client = self.grpc_client.clone(); let (tx, rx) = mpsc::channel::(64); let grpc_stream = tokio_stream::wrappers::ReceiverStream::new(rx); From 12e6b0e22fc86c77d894a1f48e5b7b9be8243387 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 16:25:39 -0300 Subject: [PATCH 21/33] =?UTF-8?q?Rename=20fields=20and=20methods=20for=20c?= =?UTF-8?q?larity:=20stream=5Ftx=20->=20sidecar=5Ftx=20(write=20handle=20t?= =?UTF-8?q?o=20sidecar),=20grpc=5Fclient=20->=20sidecar=5Fclient,=20connec?= =?UTF-8?q?ted=20->=20stream=5Fconnected,=20try=5Fconnect=20->=20open=5Fev?= =?UTF-8?q?ent=5Fstream.=20Remove=20unnecessary=20clone=20of=20sidecar=5Fc?= =?UTF-8?q?lient=20when=20opening=20the=20stream=20=E2=80=94=20use=20self.?= =?UTF-8?q?sidecar=5Fclient=20directly=20since=20the=20stream=20lives=20in?= =?UTF-8?q?dependently=20once=20established.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/l2/sequencer/credible_layer/client.rs | 54 ++++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 090b22dfa88..4cf2a1091d4 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -72,16 +72,17 @@ pub trait CredibleLayerProtocol: Send + Sync { /// established in `#[started]` and ack messages are bridged into the actor via /// `spawn_listener`. Reconnection on failure is scheduled with `send_after`. pub struct CredibleLayerClient { - /// Feeds events into the gRPC StreamEvents bidirectional stream. None when disconnected. - stream_tx: Option>, + /// Write handle for the gRPC StreamEvents bidirectional stream (Rust equivalent + /// of Java gRPC's `StreamObserver.onNext()`). None when disconnected. + sidecar_tx: Option>, /// Monotonically increasing event ID counter event_id_counter: u64, /// Current iteration ID (incremented per block) iteration_id: u64, - /// gRPC client — used for unary GetTransaction calls and cloned for stream connections. - grpc_client: SidecarTransportClient, - /// Whether the StreamEvents stream is currently connected. - connected: bool, + /// gRPC client for sidecar RPCs (StreamEvents, GetTransaction). + sidecar_client: SidecarTransportClient, + /// Whether the StreamEvents stream is currently active. + stream_connected: bool, } #[actor(protocol = CredibleLayerProtocol)] @@ -102,25 +103,24 @@ impl CredibleLayerClient { .connect_lazy(); Ok(Self { - grpc_client: SidecarTransportClient::new(channel), - stream_tx: None, + sidecar_client: SidecarTransportClient::new(channel), + sidecar_tx: None, event_id_counter: 1, iteration_id: 0, - connected: false, + stream_connected: false, }) } /// Attempt to establish a bidirectional StreamEvents connection with the sidecar. /// On success, stores the event sender and spawns an ack listener. /// On failure, schedules a retry via `send_after`. - async fn try_connect(&mut self, ctx: &Context) { - let mut stream_client = self.grpc_client.clone(); + async fn open_event_stream(&mut self, ctx: &Context) { let (tx, rx) = mpsc::channel::(64); let grpc_stream = tokio_stream::wrappers::ReceiverStream::new(rx); - match stream_client.stream_events(grpc_stream).await { + match self.sidecar_client.stream_events(grpc_stream).await { Ok(response) => { - info!("StreamEvents stream connected to sidecar"); + info!("StreamEvents stream stream_connected to sidecar"); // The sidecar requires CommitHead as the first event on every new stream. let init_commit = Event { @@ -145,8 +145,8 @@ impl CredibleLayerClient { return; } - self.stream_tx = Some(tx); - self.connected = true; + self.sidecar_tx = Some(tx); + self.stream_connected = true; // Bridge the ack stream into actor messages via spawn_listener. // When the stream ends or errors, a final "disconnected" message is sent. @@ -199,18 +199,18 @@ impl CredibleLayerClient { event_id: self.next_event_id(), event: Some(event_payload), }; - if let Some(ref sender) = self.stream_tx { + if let Some(ref sender) = self.sidecar_tx { if sender.send(event).await.is_err() { warn!("Event channel closed, marking disconnected"); - self.connected = false; - self.stream_tx = None; + self.stream_connected = false; + self.sidecar_tx = None; } } } #[started] async fn started(&mut self, ctx: &Context) { - self.try_connect(ctx).await; + self.open_event_stream(ctx).await; } #[send_handler] @@ -219,10 +219,10 @@ impl CredibleLayerClient { _msg: credible_layer_protocol::Reconnect, ctx: &Context, ) { - if self.connected { + if self.stream_connected { return; } - self.try_connect(ctx).await; + self.open_event_stream(ctx).await; } #[send_handler] @@ -232,12 +232,12 @@ impl CredibleLayerClient { ctx: &Context, ) { if msg.disconnected { - if !self.connected { + if !self.stream_connected { return; } info!("Sidecar stream disconnected: {}", msg.message); - self.connected = false; - self.stream_tx = None; + self.stream_connected = false; + self.sidecar_tx = None; send_after( Duration::from_secs(5), ctx.clone(), @@ -311,7 +311,7 @@ impl CredibleLayerClient { msg: credible_layer_protocol::SendTransaction, _ctx: &Context, ) { - if !self.connected { + if !self.stream_connected { return; } let tx_execution_id = Some(TxExecutionId { @@ -336,7 +336,7 @@ impl CredibleLayerClient { msg: credible_layer_protocol::CheckTransaction, _ctx: &Context, ) -> bool { - if !self.connected { + if !self.stream_connected { return true; } @@ -356,7 +356,7 @@ impl CredibleLayerClient { let request = GetTransactionRequest { tx_execution_id: Some(tx_exec_id.clone()), }; - match tokio::time::timeout(poll_timeout, self.grpc_client.get_transaction(request)) + match tokio::time::timeout(poll_timeout, self.sidecar_client.get_transaction(request)) .await { Ok(Ok(response)) => { From 320e93d66ed2204d992688f0f262c345d4d42bba Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 16:47:27 -0300 Subject: [PATCH 22/33] =?UTF-8?q?Move=20TransactionEnv=20conversion=20to?= =?UTF-8?q?=20From<(Transaction,=20Address)>=20impl=20in=20mod.rs,=20next?= =?UTF-8?q?=20to=20the=20proto=20type=20definitions.=20Removes=20build=5Ft?= =?UTF-8?q?ransaction=5Fenv=20from=20client.rs=20=E2=80=94=20the=20handler?= =?UTF-8?q?=20now=20just=20calls=20(tx,=20sender).into().?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/l2/sequencer/credible_layer/client.rs | 81 ++------------------ crates/l2/sequencer/credible_layer/mod.rs | 78 +++++++++++++++++++ 2 files changed, 83 insertions(+), 76 deletions(-) diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 4cf2a1091d4..7b02ce7d7ef 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use ethrex_common::types::{BlockHeader, Transaction, TxKind}; +use ethrex_common::types::{BlockHeader, Transaction}; use ethrex_common::{Address, H256}; use spawned_concurrency::{ actor, @@ -17,9 +17,9 @@ use tracing::{debug, info, warn}; use super::errors::CredibleLayerError; use super::sidecar_proto::{ - self, AccessListItem, Authorization, BlobExcessGasAndPrice, BlockEnv, CommitHead, Event, - GetTransactionRequest, NewIteration, ResultStatus, Transaction as SidecarTransaction, - TransactionEnv, TxExecutionId, sidecar_transport_client::SidecarTransportClient, + self, BlobExcessGasAndPrice, BlockEnv, CommitHead, Event, GetTransactionRequest, NewIteration, + ResultStatus, Transaction as SidecarTransaction, TxExecutionId, + sidecar_transport_client::SidecarTransportClient, }; #[protocol] @@ -320,7 +320,7 @@ impl CredibleLayerClient { tx_hash: msg.tx_hash.as_bytes().to_vec(), index: msg.tx_index, }); - let tx_env = build_transaction_env(&msg.tx, msg.sender); + let tx_env = (msg.tx, msg.sender).into(); let sidecar_tx = SidecarTransaction { tx_execution_id, tx_env: Some(tx_env), @@ -399,74 +399,3 @@ fn u64_to_u256_bytes(value: u64) -> Vec { buf[24..].copy_from_slice(&value.to_be_bytes()); buf.to_vec() } - -/// Build a `TransactionEnv` protobuf message from an ethrex transaction and its sender. -fn build_transaction_env(tx: &Transaction, sender: Address) -> TransactionEnv { - let transact_to = match tx.to() { - TxKind::Call(addr) => addr.as_bytes().to_vec(), - TxKind::Create => vec![], - }; - - let value_bytes = tx.value().to_big_endian(); - - let mut gas_price_bytes = [0u8; 16]; - let gas_price_u128 = tx.gas_price().as_u128(); - gas_price_bytes.copy_from_slice(&gas_price_u128.to_be_bytes()); - - let gas_priority_fee = tx.max_priority_fee().map(|fee| { - let mut buf = [0u8; 16]; - buf[8..].copy_from_slice(&fee.to_be_bytes()); - buf.to_vec() - }); - - let access_list = tx - .access_list() - .iter() - .map(|(addr, keys)| AccessListItem { - address: addr.as_bytes().to_vec(), - storage_keys: keys.iter().map(|k| k.as_bytes().to_vec()).collect(), - }) - .collect(); - - let authorization_list = tx - .authorization_list() - .map(|list| { - list.iter() - .map(|auth| { - let chain_id_bytes = auth.chain_id.to_big_endian(); - let r_bytes = auth.r_signature.to_big_endian(); - let s_bytes = auth.s_signature.to_big_endian(); - Authorization { - chain_id: chain_id_bytes.to_vec(), - address: auth.address.as_bytes().to_vec(), - nonce: auth.nonce, - y_parity: auth.y_parity.as_u32(), - r: r_bytes.to_vec(), - s: s_bytes.to_vec(), - } - }) - .collect() - }) - .unwrap_or_default(); - - TransactionEnv { - tx_type: u32::from(u8::from(tx.tx_type())), - caller: sender.as_bytes().to_vec(), - gas_limit: tx.gas_limit(), - gas_price: gas_price_bytes.to_vec(), - transact_to, - value: value_bytes.to_vec(), - data: tx.data().to_vec(), - nonce: tx.nonce(), - chain_id: tx.chain_id(), - access_list, - gas_priority_fee, - blob_hashes: tx - .blob_versioned_hashes() - .iter() - .map(|h| h.as_bytes().to_vec()) - .collect(), - max_fee_per_blob_gas: vec![0u8; 16], - authorization_list, - } -} diff --git a/crates/l2/sequencer/credible_layer/mod.rs b/crates/l2/sequencer/credible_layer/mod.rs index d180697e2aa..1093e4a32e6 100644 --- a/crates/l2/sequencer/credible_layer/mod.rs +++ b/crates/l2/sequencer/credible_layer/mod.rs @@ -16,3 +16,81 @@ pub use errors::CredibleLayerError; pub mod sidecar_proto { tonic::include_proto!("sidecar.transport.v1"); } + +// Conversions from ethrex types to sidecar protobuf types. + +impl From<(ethrex_common::types::Transaction, ethrex_common::Address)> + for sidecar_proto::TransactionEnv +{ + fn from((tx, sender): (ethrex_common::types::Transaction, ethrex_common::Address)) -> Self { + use ethrex_common::types::TxKind; + + let transact_to = match tx.to() { + TxKind::Call(addr) => addr.as_bytes().to_vec(), + TxKind::Create => vec![], + }; + + let value_bytes = tx.value().to_big_endian(); + + let mut gas_price_bytes = [0u8; 16]; + let gas_price_u128 = tx.gas_price().as_u128(); + gas_price_bytes.copy_from_slice(&gas_price_u128.to_be_bytes()); + + let gas_priority_fee = tx.max_priority_fee().map(|fee| { + let mut buf = [0u8; 16]; + buf[8..].copy_from_slice(&fee.to_be_bytes()); + buf.to_vec() + }); + + let access_list = tx + .access_list() + .iter() + .map(|(addr, keys)| sidecar_proto::AccessListItem { + address: addr.as_bytes().to_vec(), + storage_keys: keys.iter().map(|k| k.as_bytes().to_vec()).collect(), + }) + .collect(); + + let authorization_list = tx + .authorization_list() + .map(|list| { + list.iter() + .map(|auth| { + let chain_id_bytes = auth.chain_id.to_big_endian(); + let r_bytes = auth.r_signature.to_big_endian(); + let s_bytes = auth.s_signature.to_big_endian(); + sidecar_proto::Authorization { + chain_id: chain_id_bytes.to_vec(), + address: auth.address.as_bytes().to_vec(), + nonce: auth.nonce, + y_parity: auth.y_parity.as_u32(), + r: r_bytes.to_vec(), + s: s_bytes.to_vec(), + } + }) + .collect() + }) + .unwrap_or_default(); + + Self { + tx_type: u32::from(u8::from(tx.tx_type())), + caller: sender.as_bytes().to_vec(), + gas_limit: tx.gas_limit(), + gas_price: gas_price_bytes.to_vec(), + transact_to, + value: value_bytes.to_vec(), + data: tx.data().to_vec(), + nonce: tx.nonce(), + chain_id: tx.chain_id(), + access_list, + gas_priority_fee, + blob_hashes: tx + .blob_versioned_hashes() + .iter() + .map(|h| h.as_bytes().to_vec()) + .collect(), + max_fee_per_blob_gas: vec![0u8; 16], + authorization_list, + } + } +} From 271cbb31a0829ec5a1a1082150bd2868bf74b6fe Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 17:10:38 -0300 Subject: [PATCH 23/33] Fix module doc redundancy, use short type paths in From impl, make BlockProducer::new sync (no async needed since CL actor is spawned externally), and get last_tx_hash from the block before store_block consumes it instead of re-fetching from the store. --- crates/l2/sequencer/block_producer.rs | 13 +++---------- crates/l2/sequencer/credible_layer/mod.rs | 19 +++++++++---------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/crates/l2/sequencer/block_producer.rs b/crates/l2/sequencer/block_producer.rs index de120515128..32810060164 100644 --- a/crates/l2/sequencer/block_producer.rs +++ b/crates/l2/sequencer/block_producer.rs @@ -81,7 +81,7 @@ pub struct BlockProducerHealth { impl BlockProducer { #[expect(clippy::too_many_arguments)] - pub async fn new( + pub fn new( config: &BlockProducerConfig, l1_rpc_url: Vec, store: Store, @@ -215,6 +215,7 @@ impl BlockProducer { .ok_or(ChainError::ParentStateNotFound)?; let transactions_count = block.body.transactions.len(); + let last_tx_hash = block.body.transactions.last().map(|tx| tx.hash()); let block_number = block.header.number; let block_hash = block.hash(); // Save the header for newHeads notifications before block is moved into store_block. @@ -237,13 +238,6 @@ impl BlockProducer { // Credible Layer: send CommitHead after block is stored if let Some(ref cl) = self.credible_layer { - let last_tx_hash = self - .store - .get_block_by_hash(block_hash) - .await - .ok() - .flatten() - .and_then(|b| b.body.transactions.last().map(|tx| tx.hash())); let tx_count: u64 = transactions_count .try_into() .map_err(|_| BlockProducerError::Custom("tx count overflow".into()))?; @@ -358,8 +352,7 @@ impl BlockProducer { l2_gas_limit, credible_layer, subscription_manager, - ) - .await?; + )?; let actor_ref = block_producer.start_with_backend(Backend::Blocking); Ok(actor_ref) } diff --git a/crates/l2/sequencer/credible_layer/mod.rs b/crates/l2/sequencer/credible_layer/mod.rs index 1093e4a32e6..ba460d1cf09 100644 --- a/crates/l2/sequencer/credible_layer/mod.rs +++ b/crates/l2/sequencer/credible_layer/mod.rs @@ -1,10 +1,10 @@ -/// Credible Layer integration with the Phylax Credible Layer. +/// Credible Layer integration with the Phylax Assertion Enforcer sidecar. /// -/// This module implements a gRPC client actor that communicates with the Credible Layer -/// Assertion Enforcer sidecar during block building. Transactions that fail assertion -/// validation are dropped before block inclusion. +/// This module implements a gRPC client actor that communicates with the sidecar +/// during block building. Transactions that fail assertion validation are dropped +/// before block inclusion. /// -/// The integration is opt-in via the `--credible-layer` CLI flag. +/// The integration is opt-in via the `--credible-layer-url` CLI flag. /// When disabled, there is zero overhead. pub mod client; pub mod errors; @@ -19,12 +19,11 @@ pub mod sidecar_proto { // Conversions from ethrex types to sidecar protobuf types. -impl From<(ethrex_common::types::Transaction, ethrex_common::Address)> - for sidecar_proto::TransactionEnv -{ - fn from((tx, sender): (ethrex_common::types::Transaction, ethrex_common::Address)) -> Self { - use ethrex_common::types::TxKind; +use ethrex_common::Address; +use ethrex_common::types::{Transaction, TxKind}; +impl From<(Transaction, Address)> for sidecar_proto::TransactionEnv { + fn from((tx, sender): (Transaction, Address)) -> Self { let transact_to = match tx.to() { TxKind::Call(addr) => addr.as_bytes().to_vec(), TxKind::Create => vec![], From d84231e1bf6d1ed5ab1c8de95b5fcb11e2b47cab Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 17:12:10 -0300 Subject: [PATCH 24/33] Remove redundant module doc title line --- crates/l2/sequencer/credible_layer/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/l2/sequencer/credible_layer/mod.rs b/crates/l2/sequencer/credible_layer/mod.rs index ba460d1cf09..3131b068ba7 100644 --- a/crates/l2/sequencer/credible_layer/mod.rs +++ b/crates/l2/sequencer/credible_layer/mod.rs @@ -1,5 +1,3 @@ -/// Credible Layer integration with the Phylax Assertion Enforcer sidecar. -/// /// This module implements a gRPC client actor that communicates with the sidecar /// during block building. Transactions that fail assertion validation are dropped /// before block inclusion. From 84a9afc5721dd29fb8ccdfcc507016d886804dd4 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 17:23:09 -0300 Subject: [PATCH 25/33] Regenerate ethrex l2 help section in CLI.md from actual --help output and update sp1/risc0 guest program Cargo.lock files to fix CI check-cargo-lock. --- crates/guest-program/bin/risc0/Cargo.lock | 1 + crates/guest-program/bin/sp1/Cargo.lock | 1 + docs/CLI.md | 242 +++++++++++++--------- 3 files changed, 147 insertions(+), 97 deletions(-) diff --git a/crates/guest-program/bin/risc0/Cargo.lock b/crates/guest-program/bin/risc0/Cargo.lock index 1665d67ca3f..3eb9ceb19c3 100644 --- a/crates/guest-program/bin/risc0/Cargo.lock +++ b/crates/guest-program/bin/risc0/Cargo.lock @@ -1108,6 +1108,7 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp", + "hex", "rayon", "rustc-hash", "serde", diff --git a/crates/guest-program/bin/sp1/Cargo.lock b/crates/guest-program/bin/sp1/Cargo.lock index e6530927eac..4ca066fa512 100644 --- a/crates/guest-program/bin/sp1/Cargo.lock +++ b/crates/guest-program/bin/sp1/Cargo.lock @@ -918,6 +918,7 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp", + "hex", "rayon", "rustc-hash", "serde", diff --git a/docs/CLI.md b/docs/CLI.md index 8a1add1640a..08122900ce2 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -247,31 +247,31 @@ Commands: Options: --osaka-activation-time Block timestamp at which the Osaka fork is activated on L1. If not set, it will assume Osaka is already active. - + [env: ETHREX_OSAKA_ACTIVATION_TIME=] -t, --tick-rate time in ms between two ticks - + [default: 1000] --batch-widget-height - + -h, --help Print help (see a summary with '-h') Node options: --network - Alternatively, the name of a known network can be provided instead to use its preset genesis file and include its preset bootnodes. The networks currently supported include holesky, sepolia, hoodi and mainnet. If not specified, defaults to mainnet. - + Alternatively, the name of a known network can be provided instead to use its preset genesis file and include its preset bootnodes. The networks currently supported include sepolia, hoodi and mainnet. If not specified, defaults to mainnet. + [env: ETHREX_NETWORK=] --datadir - If the datadir is the word `memory`, ethrex will use the `InMemory Engine`. - + Base directory for the database. For public networks a subdirectory named after the network is appended (e.g. ~/.local/share/ethrex/mainnet). If the value is `memory`, the InMemory Engine is used instead. + [env: ETHREX_DATADIR=] - [default: "/home/runner/.local/share/ethrex"] + [default: ~/.local/share/ethrex] --force Delete the database without confirmation. @@ -286,147 +286,184 @@ Node options: --metrics Enable metrics collection and exposition - + [env: ETHREX_METRICS=] --dev If set it will be considered as `true`. If `--network` is not specified, it will default to a custom local devnet. The Binary has to be built with the `dev` feature enabled. - + [env: ETHREX_DEV=] --log.level Possible values: info, debug, trace, warn, error - + [env: ETHREX_LOG_LEVEL=] [default: INFO] --log.color Possible values: auto, always, never - + [env: ETHREX_LOG_COLOR=] [default: auto] + --no-migrate + Do not migrate an existing database to the network-specific subdirectory. + + [env: ETHREX_NO_MIGRATE=] + + --log.dir + Directory to store log files. + + [env: ETHREX_LOG_DIR=] + --mempool.maxsize Maximum size of the mempool in number of transactions - + [env: ETHREX_MEMPOOL_MAX_SIZE=] [default: 10000] + --precompute-witnesses + Once synced, computes execution witnesses upon receiving newPayload messages and stores them in local storage + + [env: ETHREX_PRECOMPUTE_WITNESSES=] + P2P options: --bootnodes ... Comma separated enode URLs for P2P discovery bootstrap. - + [env: ETHREX_BOOTNODES=] --syncmode Can be either "full" or "snap" with "snap" as default value. - + [env: ETHREX_SYNCMODE=] [default: snap] --p2p.disabled - [env: ETHREX_P2P_DISABLED=] --p2p.addr
The address to bind P2P sockets to. Defaults to the local IP. Use 0.0.0.0 (IPv4) or :: (IPv6) to listen on all interfaces. See also --nat.extip to announce a different external address. - + [env: ETHREX_P2P_ADDR=] --nat.extip The IP address advertised to other nodes via discovery and ENR. Use this when the node is behind NAT and --p2p.addr is a private/unspecified address. Defaults to the value of --p2p.addr (or the auto-detected local IP if neither is set). - + [env: ETHREX_P2P_NAT_EXTIP=] --p2p.port TCP port for the P2P protocol. - + [env: ETHREX_P2P_PORT=] [default: 30303] --discovery.port UDP port for P2P discovery. - + [env: ETHREX_P2P_DISCOVERY_PORT=] [default: 30303] + --p2p.discv4 + Enable discv4 discovery. + + [default: true] + [possible values: true, false] + + --p2p.discv5 + Enable discv5 discovery. + + [default: true] + [possible values: true, false] + --p2p.tx-broadcasting-interval Transaction Broadcasting Time Interval (ms) for batching transactions before broadcasting them. - + [env: ETHREX_P2P_TX_BROADCASTING_INTERVAL=] [default: 1000] - --target.peers + --p2p.target-peers Max amount of connected peers. - + [env: ETHREX_P2P_TARGET_PEERS=] [default: 100] + --p2p.lookup-interval + Initial Lookup Time Interval (ms) to trigger each Discovery lookup message and RLPx connection attempt. + + [env: ETHREX_P2P_LOOKUP_INTERVAL=] + [default: 100] + RPC options: --http.addr
Listening address for the http rpc server. - + [env: ETHREX_HTTP_ADDR=] [default: 0.0.0.0] --http.port Listening port for the http rpc server. - + [env: ETHREX_HTTP_PORT=] [default: 8545] --ws.enabled Enable websocket rpc server. Disabled by default. - + [env: ETHREX_ENABLE_WS=] --ws.addr
Listening address for the websocket rpc server. - + [env: ETHREX_WS_ADDR=] [default: 0.0.0.0] --ws.port Listening port for the websocket rpc server. - + [env: ETHREX_WS_PORT=] [default: 8546] --authrpc.addr
Listening address for the authenticated rpc server. - + [env: ETHREX_AUTHRPC_ADDR=] [default: 127.0.0.1] --authrpc.port Listening port for the authenticated rpc server. - + [env: ETHREX_AUTHRPC_PORT=] [default: 8551] --authrpc.jwtsecret Receives the jwt secret used for authenticated rpc requests. - + [env: ETHREX_AUTHRPC_JWTSECRET_PATH=] [default: jwt.hex] Block building options: --builder.extra-data Block extra data message. - + [env: ETHREX_BUILDER_EXTRA_DATA=] - [default: "ethrex 9.0.0"] + [default: ~/.local/share/ethrex] --builder.gas-limit Target block gas limit. - + [env: ETHREX_BUILDER_GAS_LIMIT=] [default: 60000000] + --builder.max-blobs + EIP-7872: Maximum blobs per block for local building. Minimum of 1. Defaults to protocol max. + + [env: ETHREX_BUILDER_MAX_BLOBS=] + Eth options: --eth.rpc-url ... List of rpc urls to use. - + [env: ETHREX_ETH_RPC_URL=] --eth.maximum-allowed-max-fee-per-gas @@ -459,7 +496,7 @@ L1 Watcher options: --watcher.watch-interval How often the L1 watcher checks for new blocks in milliseconds. - + [env: ETHREX_WATCHER_WATCH_INTERVAL=] [default: 12000] @@ -469,10 +506,19 @@ L1 Watcher options: --watcher.block-delay Number of blocks the L1 watcher waits before trusting an L1 block. - + [env: ETHREX_WATCHER_BLOCK_DELAY=] [default: 10] + --l1.router-address
+ [env: ETHREX_WATCHER_ROUTER_ADDRESS=] + + --watcher.l2-rpcs ... + [env: ETHREX_WATCHER_L2_RPCS=] + + --watcher.l2-chain-ids ... + [env: ETHREX_WATCHER_L2_CHAIN_IDS=] + Block producer options: --watcher.l1-fee-update-interval-ms
[env: ETHREX_WATCHER_L1_FEE_UPDATE_INTERVAL_MS=] @@ -480,7 +526,7 @@ Block producer options: --block-producer.block-time How often does the sequencer produce new blocks to the L1 in milliseconds. - + [env: ETHREX_BLOCK_PRODUCER_BLOCK_TIME=] [default: 5000] @@ -495,7 +541,7 @@ Block producer options: --block-producer.operator-fee-per-gas Fee that the operator will receive for each unit of gas consumed in a block. - + [env: ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS=] --block-producer.l1-fee-vault-address
@@ -509,36 +555,39 @@ Proposer options: L1 Committer options: --committer.l1-private-key Private key of a funded account that the sequencer will use to send commit txs to the L1. - + [env: ETHREX_COMMITTER_L1_PRIVATE_KEY=] --committer.remote-signer-url URL of a Web3Signer-compatible server to remote sign instead of a local private key. - + [env: ETHREX_COMMITTER_REMOTE_SIGNER_URL=] --committer.remote-signer-public-key Public key to request the remote signature from. - + [env: ETHREX_COMMITTER_REMOTE_SIGNER_PUBLIC_KEY=] --l1.on-chain-proposer-address
[env: ETHREX_COMMITTER_ON_CHAIN_PROPOSER_ADDRESS=] + --l1.timelock-address
+ [env: ETHREX_TIMELOCK_ADDRESS=] + --committer.commit-time How often does the sequencer commit new blocks to the L1 in milliseconds. - + [env: ETHREX_COMMITTER_COMMIT_TIME=] [default: 60000] --committer.batch-gas-limit Maximum gas limit for the batch - + [env: ETHREX_COMMITTER_BATCH_GAS_LIMIT=] --committer.first-wake-up-time Time to wait before the sequencer seals a batch when started. After committing the first batch, `committer.commit-time` will be used. - + [env: ETHREX_COMMITTER_FIRST_WAKE_UP_TIME=] --committer.arbitrary-base-blob-gas-price @@ -547,34 +596,34 @@ L1 Committer options: Proof coordinator options: --proof-coordinator.l1-private-key - Private key of a funded account that the sequencer will use to send verify txs to the L1. Has to be a different account than --committer-l1-private-key. - + Private key of of a funded account that the sequencer will use to send verify txs to the L1. Has to be a different account than --committer-l1-private-key. + [env: ETHREX_PROOF_COORDINATOR_L1_PRIVATE_KEY=] --proof-coordinator.tdx-private-key - Private key of a funded account that the TDX tool will use to send the tdx attestation to L1. - + Private key of of a funded account that the TDX tool that will use to send the tdx attestation to L1. + [env: ETHREX_PROOF_COORDINATOR_TDX_PRIVATE_KEY=] --proof-coordinator.qpl-tool-path Path to the QPL tool that will be used to generate TDX quotes. - + [env: ETHREX_PROOF_COORDINATOR_QPL_TOOL_PATH=] [default: ./tee/contracts/automata-dcap-qpl/automata-dcap-qpl-tool/target/release/automata-dcap-qpl-tool] --proof-coordinator.remote-signer-url URL of a Web3Signer-compatible server to remote sign instead of a local private key. - + [env: ETHREX_PROOF_COORDINATOR_REMOTE_SIGNER_URL=] --proof-coordinator.remote-signer-public-key Public key to request the remote signature from. - + [env: ETHREX_PROOF_COORDINATOR_REMOTE_SIGNER_PUBLIC_KEY=] --proof-coordinator.addr Set it to 0.0.0.0 to allow connections from other machines. - + [env: ETHREX_PROOF_COORDINATOR_LISTEN_ADDRESS=] [default: 127.0.0.1] @@ -584,18 +633,17 @@ Proof coordinator options: --proof-coordinator.send-interval How often does the proof coordinator send proofs to the L1 in milliseconds. - + [env: ETHREX_PROOF_COORDINATOR_SEND_INTERVAL=] [default: 5000] -Based options: - --state-updater.sequencer-registry
- [env: ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY=] - - --state-updater.check-interval - [env: ETHREX_STATE_UPDATER_CHECK_INTERVAL=] - [default: 1000] + --proof-coordinator.prover-timeout + Timeout in milliseconds before a batch assignment to a prover is considered stale. + + [env: ETHREX_PROOF_COORDINATOR_PROVER_TIMEOUT=] + [default: 600000] +Based options: --block-fetcher.fetch_interval_ms [env: ETHREX_BLOCK_FETCHER_FETCH_INTERVAL_MS=] [default: 5000] @@ -604,6 +652,13 @@ Based options: [env: ETHREX_BLOCK_FETCHER_FETCH_BLOCK_STEP=] [default: 5000] + --state-updater.sequencer-registry
+ [env: ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY=] + + --state-updater.check-interval + [env: ETHREX_STATE_UPDATER_CHECK_INTERVAL=] + [default: 1000] + --based [env: ETHREX_BASED=] @@ -617,31 +672,25 @@ Aligned options: --aligned.beacon-url ... List of beacon urls to use. - + [env: ETHREX_ALIGNED_BEACON_URL=] --aligned-network L1 network name for Aligned sdk - + [env: ETHREX_ALIGNED_NETWORK=] [default: devnet] --aligned.from-block Starting L1 block number for proof aggregation search. Helps avoid scanning blocks from before proofs were being sent. - + [env: ETHREX_ALIGNED_FROM_BLOCK=] --aligned.resubmission-timeout Timeout in seconds before resending a proof not yet verified on-chain. Required when --aligned is enabled. Aligned typically aggregates once per day, so this value should be set accordingly (e.g. 86400 for 24h). - + [env: ETHREX_ALIGNED_RESUBMISSION_TIMEOUT_SECS=] - --aligned.fee-estimate - Fee estimate for Aligned sdk - - [env: ETHREX_ALIGNED_FEE_ESTIMATE=] - [default: instant] - Admin server options: --admin-server.addr [env: ETHREX_ADMIN_SERVER_LISTEN_ADDRESS=] @@ -651,46 +700,45 @@ Admin server options: [env: ETHREX_ADMIN_SERVER_LISTEN_PORT=] [default: 5555] + --admin.start-at + Starting L2 block to start producing blocks + + [env: ETHREX_ADMIN_START_AT=] + [default: 0] + + --admin.l2-head-check-rpc-url + L2 JSON-RPC endpoint used only to query the L2 head when `--admin.start-at` is set + + [env: ETHREX_ADMIN_L2_HEAD_CHECK_RPC_URL=] + +Credible Layer options: + --credible-layer-url + gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). Passing this flag enables the integration. + + [env: ETHREX_CREDIBLE_LAYER_URL=] + L2 options: --validium If true, L2 will run on validium mode as opposed to the default rollup mode, meaning it will not publish blobs to the L1. - + [env: ETHREX_L2_VALIDIUM=] --sponsorable-addresses Path to a file containing addresses of contracts to which ethrex_SendTransaction should sponsor txs - + [env: ETHREX_SPONSORABLE_ADDRESSES_PATH=] --sponsor-private-key The private key of ethrex L2 transactions sponsor. - + [env: SPONSOR_PRIVATE_KEY=] [default: 0xffd790338a2798b648806fc8635ac7bf14af15425fed0c8f25bcc5febaa9b192] - --l2.ws-enabled - Enable WebSocket RPC server for L2. Required by the Credible Layer sidecar. - - [env: ETHREX_L2_WS_ENABLED=] - - --l2.ws-addr
- Listening address for the L2 WebSocket RPC server. - - [env: ETHREX_L2_WS_ADDR=] - [default: 0.0.0.0] - - --l2.ws-port - Listening port for the L2 WebSocket RPC server. - - [env: ETHREX_L2_WS_PORT=] - [default: 1729] - -Credible Layer options: - --credible-layer-url - gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). - Passing this flag enables the integration. - - [env: ETHREX_CREDIBLE_LAYER_URL=] + --sponsored-gas-limit + Maximum gas limit for sponsored transactions. Transactions that estimate more gas than this will be rejected. + + [env: ETHREX_SPONSORED_GAS_LIMIT=] + [default: 500000] Monitor options: --no-monitor From f5a59f37c160d42179c084248c02d685bea59c64 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 17:25:00 -0300 Subject: [PATCH 26/33] =?UTF-8?q?Restore=20docs/CLI.md=20to=20main=20?= =?UTF-8?q?=E2=80=94=20CLI=20doc=20updates=20are=20out=20of=20scope=20for?= =?UTF-8?q?=20this=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CLI.md | 220 ++++++++++++++++++---------------------------------- 1 file changed, 74 insertions(+), 146 deletions(-) diff --git a/docs/CLI.md b/docs/CLI.md index 08122900ce2..ecc185979cf 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -247,31 +247,31 @@ Commands: Options: --osaka-activation-time Block timestamp at which the Osaka fork is activated on L1. If not set, it will assume Osaka is already active. - + [env: ETHREX_OSAKA_ACTIVATION_TIME=] -t, --tick-rate time in ms between two ticks - + [default: 1000] --batch-widget-height - + -h, --help Print help (see a summary with '-h') Node options: --network - Alternatively, the name of a known network can be provided instead to use its preset genesis file and include its preset bootnodes. The networks currently supported include sepolia, hoodi and mainnet. If not specified, defaults to mainnet. - + Alternatively, the name of a known network can be provided instead to use its preset genesis file and include its preset bootnodes. The networks currently supported include holesky, sepolia, hoodi and mainnet. If not specified, defaults to mainnet. + [env: ETHREX_NETWORK=] --datadir - Base directory for the database. For public networks a subdirectory named after the network is appended (e.g. ~/.local/share/ethrex/mainnet). If the value is `memory`, the InMemory Engine is used instead. - + If the datadir is the word `memory`, ethrex will use the `InMemory Engine`. + [env: ETHREX_DATADIR=] - [default: ~/.local/share/ethrex] + [default: "/home/runner/.local/share/ethrex"] --force Delete the database without confirmation. @@ -286,184 +286,147 @@ Node options: --metrics Enable metrics collection and exposition - + [env: ETHREX_METRICS=] --dev If set it will be considered as `true`. If `--network` is not specified, it will default to a custom local devnet. The Binary has to be built with the `dev` feature enabled. - + [env: ETHREX_DEV=] --log.level Possible values: info, debug, trace, warn, error - + [env: ETHREX_LOG_LEVEL=] [default: INFO] --log.color Possible values: auto, always, never - + [env: ETHREX_LOG_COLOR=] [default: auto] - --no-migrate - Do not migrate an existing database to the network-specific subdirectory. - - [env: ETHREX_NO_MIGRATE=] - - --log.dir - Directory to store log files. - - [env: ETHREX_LOG_DIR=] - --mempool.maxsize Maximum size of the mempool in number of transactions - + [env: ETHREX_MEMPOOL_MAX_SIZE=] [default: 10000] - --precompute-witnesses - Once synced, computes execution witnesses upon receiving newPayload messages and stores them in local storage - - [env: ETHREX_PRECOMPUTE_WITNESSES=] - P2P options: --bootnodes ... Comma separated enode URLs for P2P discovery bootstrap. - + [env: ETHREX_BOOTNODES=] --syncmode Can be either "full" or "snap" with "snap" as default value. - + [env: ETHREX_SYNCMODE=] [default: snap] --p2p.disabled + [env: ETHREX_P2P_DISABLED=] --p2p.addr
The address to bind P2P sockets to. Defaults to the local IP. Use 0.0.0.0 (IPv4) or :: (IPv6) to listen on all interfaces. See also --nat.extip to announce a different external address. - + [env: ETHREX_P2P_ADDR=] --nat.extip The IP address advertised to other nodes via discovery and ENR. Use this when the node is behind NAT and --p2p.addr is a private/unspecified address. Defaults to the value of --p2p.addr (or the auto-detected local IP if neither is set). - + [env: ETHREX_P2P_NAT_EXTIP=] --p2p.port TCP port for the P2P protocol. - + [env: ETHREX_P2P_PORT=] [default: 30303] --discovery.port UDP port for P2P discovery. - + [env: ETHREX_P2P_DISCOVERY_PORT=] [default: 30303] - --p2p.discv4 - Enable discv4 discovery. - - [default: true] - [possible values: true, false] - - --p2p.discv5 - Enable discv5 discovery. - - [default: true] - [possible values: true, false] - --p2p.tx-broadcasting-interval Transaction Broadcasting Time Interval (ms) for batching transactions before broadcasting them. - + [env: ETHREX_P2P_TX_BROADCASTING_INTERVAL=] [default: 1000] - --p2p.target-peers + --target.peers Max amount of connected peers. - - [env: ETHREX_P2P_TARGET_PEERS=] - [default: 100] - --p2p.lookup-interval - Initial Lookup Time Interval (ms) to trigger each Discovery lookup message and RLPx connection attempt. - - [env: ETHREX_P2P_LOOKUP_INTERVAL=] + [env: ETHREX_P2P_TARGET_PEERS=] [default: 100] RPC options: --http.addr
Listening address for the http rpc server. - + [env: ETHREX_HTTP_ADDR=] [default: 0.0.0.0] --http.port Listening port for the http rpc server. - + [env: ETHREX_HTTP_PORT=] [default: 8545] --ws.enabled Enable websocket rpc server. Disabled by default. - + [env: ETHREX_ENABLE_WS=] --ws.addr
Listening address for the websocket rpc server. - + [env: ETHREX_WS_ADDR=] [default: 0.0.0.0] --ws.port Listening port for the websocket rpc server. - + [env: ETHREX_WS_PORT=] [default: 8546] --authrpc.addr
Listening address for the authenticated rpc server. - + [env: ETHREX_AUTHRPC_ADDR=] [default: 127.0.0.1] --authrpc.port Listening port for the authenticated rpc server. - + [env: ETHREX_AUTHRPC_PORT=] [default: 8551] --authrpc.jwtsecret Receives the jwt secret used for authenticated rpc requests. - + [env: ETHREX_AUTHRPC_JWTSECRET_PATH=] [default: jwt.hex] Block building options: --builder.extra-data Block extra data message. - + [env: ETHREX_BUILDER_EXTRA_DATA=] - [default: ~/.local/share/ethrex] + [default: "ethrex 9.0.0"] --builder.gas-limit Target block gas limit. - + [env: ETHREX_BUILDER_GAS_LIMIT=] [default: 60000000] - --builder.max-blobs - EIP-7872: Maximum blobs per block for local building. Minimum of 1. Defaults to protocol max. - - [env: ETHREX_BUILDER_MAX_BLOBS=] - Eth options: --eth.rpc-url ... List of rpc urls to use. - + [env: ETHREX_ETH_RPC_URL=] --eth.maximum-allowed-max-fee-per-gas @@ -496,7 +459,7 @@ L1 Watcher options: --watcher.watch-interval How often the L1 watcher checks for new blocks in milliseconds. - + [env: ETHREX_WATCHER_WATCH_INTERVAL=] [default: 12000] @@ -506,19 +469,10 @@ L1 Watcher options: --watcher.block-delay Number of blocks the L1 watcher waits before trusting an L1 block. - + [env: ETHREX_WATCHER_BLOCK_DELAY=] [default: 10] - --l1.router-address
- [env: ETHREX_WATCHER_ROUTER_ADDRESS=] - - --watcher.l2-rpcs ... - [env: ETHREX_WATCHER_L2_RPCS=] - - --watcher.l2-chain-ids ... - [env: ETHREX_WATCHER_L2_CHAIN_IDS=] - Block producer options: --watcher.l1-fee-update-interval-ms
[env: ETHREX_WATCHER_L1_FEE_UPDATE_INTERVAL_MS=] @@ -526,7 +480,7 @@ Block producer options: --block-producer.block-time How often does the sequencer produce new blocks to the L1 in milliseconds. - + [env: ETHREX_BLOCK_PRODUCER_BLOCK_TIME=] [default: 5000] @@ -541,7 +495,7 @@ Block producer options: --block-producer.operator-fee-per-gas Fee that the operator will receive for each unit of gas consumed in a block. - + [env: ETHREX_BLOCK_PRODUCER_OPERATOR_FEE_PER_GAS=] --block-producer.l1-fee-vault-address
@@ -555,39 +509,36 @@ Proposer options: L1 Committer options: --committer.l1-private-key Private key of a funded account that the sequencer will use to send commit txs to the L1. - + [env: ETHREX_COMMITTER_L1_PRIVATE_KEY=] --committer.remote-signer-url URL of a Web3Signer-compatible server to remote sign instead of a local private key. - + [env: ETHREX_COMMITTER_REMOTE_SIGNER_URL=] --committer.remote-signer-public-key Public key to request the remote signature from. - + [env: ETHREX_COMMITTER_REMOTE_SIGNER_PUBLIC_KEY=] --l1.on-chain-proposer-address
[env: ETHREX_COMMITTER_ON_CHAIN_PROPOSER_ADDRESS=] - --l1.timelock-address
- [env: ETHREX_TIMELOCK_ADDRESS=] - --committer.commit-time How often does the sequencer commit new blocks to the L1 in milliseconds. - + [env: ETHREX_COMMITTER_COMMIT_TIME=] [default: 60000] --committer.batch-gas-limit Maximum gas limit for the batch - + [env: ETHREX_COMMITTER_BATCH_GAS_LIMIT=] --committer.first-wake-up-time Time to wait before the sequencer seals a batch when started. After committing the first batch, `committer.commit-time` will be used. - + [env: ETHREX_COMMITTER_FIRST_WAKE_UP_TIME=] --committer.arbitrary-base-blob-gas-price @@ -596,34 +547,34 @@ L1 Committer options: Proof coordinator options: --proof-coordinator.l1-private-key - Private key of of a funded account that the sequencer will use to send verify txs to the L1. Has to be a different account than --committer-l1-private-key. - + Private key of a funded account that the sequencer will use to send verify txs to the L1. Has to be a different account than --committer-l1-private-key. + [env: ETHREX_PROOF_COORDINATOR_L1_PRIVATE_KEY=] --proof-coordinator.tdx-private-key - Private key of of a funded account that the TDX tool that will use to send the tdx attestation to L1. - + Private key of a funded account that the TDX tool will use to send the tdx attestation to L1. + [env: ETHREX_PROOF_COORDINATOR_TDX_PRIVATE_KEY=] --proof-coordinator.qpl-tool-path Path to the QPL tool that will be used to generate TDX quotes. - + [env: ETHREX_PROOF_COORDINATOR_QPL_TOOL_PATH=] [default: ./tee/contracts/automata-dcap-qpl/automata-dcap-qpl-tool/target/release/automata-dcap-qpl-tool] --proof-coordinator.remote-signer-url URL of a Web3Signer-compatible server to remote sign instead of a local private key. - + [env: ETHREX_PROOF_COORDINATOR_REMOTE_SIGNER_URL=] --proof-coordinator.remote-signer-public-key Public key to request the remote signature from. - + [env: ETHREX_PROOF_COORDINATOR_REMOTE_SIGNER_PUBLIC_KEY=] --proof-coordinator.addr Set it to 0.0.0.0 to allow connections from other machines. - + [env: ETHREX_PROOF_COORDINATOR_LISTEN_ADDRESS=] [default: 127.0.0.1] @@ -633,17 +584,18 @@ Proof coordinator options: --proof-coordinator.send-interval How often does the proof coordinator send proofs to the L1 in milliseconds. - + [env: ETHREX_PROOF_COORDINATOR_SEND_INTERVAL=] [default: 5000] - --proof-coordinator.prover-timeout - Timeout in milliseconds before a batch assignment to a prover is considered stale. - - [env: ETHREX_PROOF_COORDINATOR_PROVER_TIMEOUT=] - [default: 600000] - Based options: + --state-updater.sequencer-registry
+ [env: ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY=] + + --state-updater.check-interval + [env: ETHREX_STATE_UPDATER_CHECK_INTERVAL=] + [default: 1000] + --block-fetcher.fetch_interval_ms [env: ETHREX_BLOCK_FETCHER_FETCH_INTERVAL_MS=] [default: 5000] @@ -652,13 +604,6 @@ Based options: [env: ETHREX_BLOCK_FETCHER_FETCH_BLOCK_STEP=] [default: 5000] - --state-updater.sequencer-registry
- [env: ETHREX_STATE_UPDATER_SEQUENCER_REGISTRY=] - - --state-updater.check-interval - [env: ETHREX_STATE_UPDATER_CHECK_INTERVAL=] - [default: 1000] - --based [env: ETHREX_BASED=] @@ -672,25 +617,31 @@ Aligned options: --aligned.beacon-url ... List of beacon urls to use. - + [env: ETHREX_ALIGNED_BEACON_URL=] --aligned-network L1 network name for Aligned sdk - + [env: ETHREX_ALIGNED_NETWORK=] [default: devnet] --aligned.from-block Starting L1 block number for proof aggregation search. Helps avoid scanning blocks from before proofs were being sent. - + [env: ETHREX_ALIGNED_FROM_BLOCK=] --aligned.resubmission-timeout Timeout in seconds before resending a proof not yet verified on-chain. Required when --aligned is enabled. Aligned typically aggregates once per day, so this value should be set accordingly (e.g. 86400 for 24h). - + [env: ETHREX_ALIGNED_RESUBMISSION_TIMEOUT_SECS=] + --aligned.fee-estimate + Fee estimate for Aligned sdk + + [env: ETHREX_ALIGNED_FEE_ESTIMATE=] + [default: instant] + Admin server options: --admin-server.addr [env: ETHREX_ADMIN_SERVER_LISTEN_ADDRESS=] @@ -700,46 +651,23 @@ Admin server options: [env: ETHREX_ADMIN_SERVER_LISTEN_PORT=] [default: 5555] - --admin.start-at - Starting L2 block to start producing blocks - - [env: ETHREX_ADMIN_START_AT=] - [default: 0] - - --admin.l2-head-check-rpc-url - L2 JSON-RPC endpoint used only to query the L2 head when `--admin.start-at` is set - - [env: ETHREX_ADMIN_L2_HEAD_CHECK_RPC_URL=] - -Credible Layer options: - --credible-layer-url - gRPC endpoint for the Credible Layer Assertion Enforcer sidecar (e.g. http://localhost:50051). Passing this flag enables the integration. - - [env: ETHREX_CREDIBLE_LAYER_URL=] - L2 options: --validium If true, L2 will run on validium mode as opposed to the default rollup mode, meaning it will not publish blobs to the L1. - + [env: ETHREX_L2_VALIDIUM=] --sponsorable-addresses Path to a file containing addresses of contracts to which ethrex_SendTransaction should sponsor txs - + [env: ETHREX_SPONSORABLE_ADDRESSES_PATH=] --sponsor-private-key The private key of ethrex L2 transactions sponsor. - + [env: SPONSOR_PRIVATE_KEY=] [default: 0xffd790338a2798b648806fc8635ac7bf14af15425fed0c8f25bcc5febaa9b192] - --sponsored-gas-limit - Maximum gas limit for sponsored transactions. Transactions that estimate more gas than this will be rejected. - - [env: ETHREX_SPONSORED_GAS_LIMIT=] - [default: 500000] - Monitor options: --no-monitor [env: ETHREX_NO_MONITOR=] From d9b3e91436073e5a9da265dfddfeb140e98638c9 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 17:28:29 -0300 Subject: [PATCH 27/33] Update architecture diagram to reflect the current implementation: show the 3-step pre/post execution flow (send tx, execute, poll verdict with undo on rejection), remove the non-existent Aeges check, and fix the gRPC label positioning. --- docs/l2/img/credible_layer_architecture.svg | 161 +++++++++----------- 1 file changed, 75 insertions(+), 86 deletions(-) diff --git a/docs/l2/img/credible_layer_architecture.svg b/docs/l2/img/credible_layer_architecture.svg index 403279f2a8c..eb8802c710f 100644 --- a/docs/l2/img/credible_layer_architecture.svg +++ b/docs/l2/img/credible_layer_architecture.svg @@ -1,114 +1,103 @@ - + + + + + + + - - + + - + - - + + ethrex L2 Sequencer - - User - - tx - - - RPC - - - Aeges check - - - Mempool + + User + + tx + + + RPC + + + Mempool - + - - - Block Producer: fill_transactions() + + + Block Producer: fill_transactions() + For each tx: - - For each tx: + + 1. Send Transaction + - - Send Transaction - + + 2. Execute tx in EVM - - Await verdict - + + 3. Poll verdict + - - ASSERTION_FAILED? - Skip tx - - - Otherwise: - include tx + + ASSERTION_FAILED? + undo + drop tx - On block sealed: - Send CommitHead - - - - - WebSocket server (:1730) - eth_subscribe("newHeads") - - - - - debug_traceBlockByNumber - prestateTracer (diff mode) - - - - - Credible Layer - Sidecar - (Assertion Enforcer) - - - - Runs PhEVM to - evaluate assertions - - Tracks chain state - via WS + traces - - - - - - On-chain (L2) - - - + On block sealed: + NewIteration / CommitHead + + + + + eth_subscribe("newHeads") :1730 + + + + debug_traceBlockByNumber :1729 + + + + + gRPC :50051 + Credible Layer Sidecar + (Assertion Enforcer) + + Runs PhEVM to + evaluate assertions + + Tracks chain state + via WS + traces + GetTransaction for verdicts + + + + On-chain (L2) + + - - State Oracle - (registry of assertions) + + State Oracle + (registry of assertions) - - Assertion DA - (bytecode storage) - - - - gRPC + + Assertion DA + (bytecode storage) From ab604e20ba2c154a33560f241e3f709db725cf9a Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 17:30:25 -0300 Subject: [PATCH 28/33] Fix redundant phrasing in credible layer doc overview --- docs/l2/credible_layer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index 9dfe99e068e..fe2a3449df6 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -2,7 +2,7 @@ ## Overview -Credible Layer integrates ethrex L2 with [Phylax Systems' Credible Layer](https://docs.phylax.systems/credible/credible-introduction) — a pre-execution security infrastructure that validates transactions against on-chain assertions before block inclusion. Transactions that violate an assertion are silently dropped before they land on-chain. +ethrex L2 integrates with the [Credible Layer](https://docs.phylax.systems/credible/credible-introduction) — a pre-execution security infrastructure by Phylax Systems that validates transactions against on-chain assertions before block inclusion. Transactions that violate an assertion are silently dropped before they land on-chain. ### Architecture From 808dc8cf6473c3aba2b795a9f773a96625db8c22 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 17:34:32 -0300 Subject: [PATCH 29/33] Fix architecture diagram: move WS/debug endpoints inside the sidecar box as dependencies it reads from ethrex (not separate components), separate Assertion DA and Indexer into an off-chain services section (they are not on-chain), and keep only State Oracle in the on-chain box. --- docs/l2/img/credible_layer_architecture.svg | 80 ++++++++++----------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/docs/l2/img/credible_layer_architecture.svg b/docs/l2/img/credible_layer_architecture.svg index eb8802c710f..740255b4c5e 100644 --- a/docs/l2/img/credible_layer_architecture.svg +++ b/docs/l2/img/credible_layer_architecture.svg @@ -1,4 +1,4 @@ - + @@ -12,16 +12,13 @@ - - - - + - + ethrex L2 Sequencer @@ -39,20 +36,20 @@ - + Block Producer: fill_transactions() For each tx: 1. Send Transaction - + 2. Execute tx in EVM 3. Poll verdict - + ASSERTION_FAILED? @@ -61,43 +58,46 @@ On block sealed: NewIteration / CommitHead - + - - - eth_subscribe("newHeads") :1730 - + + + gRPC :50051 + Credible Layer Sidecar + (Assertion Enforcer) - - debug_traceBlockByNumber :1729 - + + Runs PhEVM to evaluate assertions + GetTransaction for verdicts - - - gRPC :50051 - Credible Layer Sidecar - (Assertion Enforcer) - - Runs PhEVM to - evaluate assertions - - Tracks chain state - via WS + traces - GetTransaction for verdicts + + Reads from ethrex: + eth_subscribe("newHeads") :1730 + debug_traceBlockByNumber :1729 + + + Reads from external: + Assertion DA (bytecode) + State Oracle indexer (GraphQL) - - On-chain (L2) + + On-chain (L2) + + + + State Oracle + (registry of assertions) - + + + Off-chain services - - - State Oracle - (registry of assertions) + + Assertion DA + (bytecode storage) - - - Assertion DA - (bytecode storage) + + Indexer + (State Oracle events) From 7b295aaca5f5c5b59ccf1ba1b906d515d486d9cf Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 18:06:24 -0300 Subject: [PATCH 30/33] =?UTF-8?q?Note=20that=20SubscribeResults=20is=20not?= =?UTF-8?q?=20yet=20consumed=20=E2=80=94=20GetTransaction=20is=20used=20fo?= =?UTF-8?q?r=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/l2/sequencer/credible_layer/client.rs | 6 ++++++ docs/l2/credible_layer.md | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 7b02ce7d7ef..460d62d9bc5 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -330,6 +330,12 @@ impl CredibleLayerClient { .await; } + // TODO: Use the SubscribeResults server stream as the primary mechanism for + // receiving transaction verdicts (like the Besu plugin does). Store pushed + // results in a tx_results map via a result_received handler, and check the + // map here before falling back to GetTransaction polling. The stream delivers + // results faster since they're pushed as soon as the sidecar finishes evaluating, + // while polling adds latency from repeated round-trips. #[request_handler] async fn handle_check_transaction( &mut self, diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index fe2a3449df6..e665b409c10 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -23,7 +23,9 @@ ethrex communicates with the sidecar via **gRPC** using the protocol defined in |-----|------|---------| | `StreamEvents` | Bidirectional stream | Send block lifecycle events (CommitHead, NewIteration, Transaction) | | `SubscribeResults` | Server stream | Receive transaction verdicts as they complete | -| `GetTransaction` | Unary | Fallback polling for a single result | +| `GetTransaction` | Unary | Poll for a single transaction result | + +> **Note:** `SubscribeResults` is not yet consumed by ethrex. Transaction verdicts are currently obtained by polling `GetTransaction`. A future improvement should subscribe to `SubscribeResults` as the primary mechanism and use `GetTransaction` only as a fallback. ### Block Building Flow From c54716cd6a685530f68b24a69fd0a077506fff21 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 18:07:19 -0300 Subject: [PATCH 31/33] =?UTF-8?q?Remove=20TODO=20comment=20from=20client.r?= =?UTF-8?q?s=20=E2=80=94=20the=20note=20lives=20in=20the=20docs=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/l2/sequencer/credible_layer/client.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/l2/sequencer/credible_layer/client.rs b/crates/l2/sequencer/credible_layer/client.rs index 460d62d9bc5..7b02ce7d7ef 100644 --- a/crates/l2/sequencer/credible_layer/client.rs +++ b/crates/l2/sequencer/credible_layer/client.rs @@ -330,12 +330,6 @@ impl CredibleLayerClient { .await; } - // TODO: Use the SubscribeResults server stream as the primary mechanism for - // receiving transaction verdicts (like the Besu plugin does). Store pushed - // results in a tx_results map via a result_received handler, and check the - // map here before falling back to GetTransaction polling. The stream delivers - // results faster since they're pushed as soon as the sidecar finishes evaluating, - // while polling adds latency from repeated round-trips. #[request_handler] async fn handle_check_transaction( &mut self, From fd3d04aed8001965fbdd929f325e9899e78468ce Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 18:17:40 -0300 Subject: [PATCH 32/33] Remove prestateTracer from key files table (generic RPC, not specific to this integration) and update all Cargo.lock files to fix CI check-cargo-lock. --- crates/guest-program/bin/openvm/Cargo.lock | 1 + crates/guest-program/bin/zisk/Cargo.lock | 1 + crates/l2/tee/quote-gen/Cargo.lock | 1 + .../vm/levm/bench/revm_comparison/Cargo.lock | 1 + docs/l2/credible_layer.md | 1 - tooling/Cargo.lock | 70 +++++++++++++++++++ 6 files changed, 74 insertions(+), 1 deletion(-) diff --git a/crates/guest-program/bin/openvm/Cargo.lock b/crates/guest-program/bin/openvm/Cargo.lock index 2550acb10f9..775d336e4bb 100644 --- a/crates/guest-program/bin/openvm/Cargo.lock +++ b/crates/guest-program/bin/openvm/Cargo.lock @@ -863,6 +863,7 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp", + "hex", "rayon", "rustc-hash", "serde", diff --git a/crates/guest-program/bin/zisk/Cargo.lock b/crates/guest-program/bin/zisk/Cargo.lock index 7aa3108b9cb..2855204d802 100644 --- a/crates/guest-program/bin/zisk/Cargo.lock +++ b/crates/guest-program/bin/zisk/Cargo.lock @@ -1203,6 +1203,7 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp", + "hex", "rayon", "rustc-hash 2.1.1", "serde", diff --git a/crates/l2/tee/quote-gen/Cargo.lock b/crates/l2/tee/quote-gen/Cargo.lock index 6fabeeb4efd..798a0bc8b9a 100644 --- a/crates/l2/tee/quote-gen/Cargo.lock +++ b/crates/l2/tee/quote-gen/Cargo.lock @@ -1311,6 +1311,7 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp", + "hex", "rayon", "rustc-hash", "serde 1.0.228", diff --git a/crates/vm/levm/bench/revm_comparison/Cargo.lock b/crates/vm/levm/bench/revm_comparison/Cargo.lock index 2347fa6f1bf..d9ef6447d28 100644 --- a/crates/vm/levm/bench/revm_comparison/Cargo.lock +++ b/crates/vm/levm/bench/revm_comparison/Cargo.lock @@ -1147,6 +1147,7 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp", + "hex", "rayon", "rustc-hash", "serde", diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index e665b409c10..469c50753ca 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -56,7 +56,6 @@ Privileged transactions (L1→L2 deposits) bypass the Credible Layer in this fir | `crates/l2/proto/sidecar.proto` | Sidecar gRPC protocol definition | | `crates/l2/sequencer/block_producer.rs` | Block producer — sends CommitHead + NewIteration | | `crates/l2/sequencer/block_producer/payload_builder.rs` | Transaction selection — sends Transaction events | -| `crates/networking/rpc/tracing.rs` | prestateTracer implementation | ### Configuration diff --git a/tooling/Cargo.lock b/tooling/Cargo.lock index e4b0fa567e7..6bb66d97a9c 100644 --- a/tooling/Cargo.lock +++ b/tooling/Cargo.lock @@ -3430,6 +3430,7 @@ dependencies = [ "hex", "jsonwebtoken", "lazy_static", + "prost", "rand 0.8.5", "ratatui", "reqwest 0.12.28", @@ -3441,7 +3442,10 @@ dependencies = [ "spawned-rt", "thiserror 2.0.18", "tokio", + "tokio-stream", "tokio-util", + "tonic", + "tonic-build", "tracing", "tui-logger", "vergen-git2 1.0.7", @@ -3876,6 +3880,7 @@ dependencies = [ "ethrex-crypto", "ethrex-levm", "ethrex-rlp 9.0.0", + "hex", "rayon", "rustc-hash 2.1.2", "serde", @@ -4023,6 +4028,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "float-cmp" version = "0.10.0" @@ -5771,6 +5782,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "munge" version = "0.4.7" @@ -6835,6 +6852,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.14.0", +] + [[package]] name = "phf" version = "0.11.3" @@ -7167,6 +7194,26 @@ dependencies = [ "prost-derive", ] +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck 0.4.1", + "itertools 0.10.5", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.117", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.13.5" @@ -7180,6 +7227,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + [[package]] name = "protobuf" version = "3.7.2" @@ -10143,6 +10199,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.117", +] + [[package]] name = "tower" version = "0.4.13" From e069306ed7dcc778b3e4dadd53bad24c7fe03390 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 20 Apr 2026 18:36:52 -0300 Subject: [PATCH 33/33] =?UTF-8?q?Move=20Credible=20Layer=20demo=20contract?= =?UTF-8?q?s=20(OwnableTarget.sol,=20TestOwnershipAssertion.sol)=20from=20?= =?UTF-8?q?crates/l2/contracts/src/credible=5Flayer/=20to=20tooling/l2/cre?= =?UTF-8?q?dible=5Flayer/=20since=20the=20sequencer=20never=20compiles=20o?= =?UTF-8?q?r=20references=20them=20=E2=80=94=20they=20exist=20only=20for?= =?UTF-8?q?=20the=20e2e=20setup=20guide.=20Update=20contract=20path=20in?= =?UTF-8?q?=20docs=20and=20relative=20doc=20link=20in=20README.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/l2/credible_layer.md | 2 +- .../src => tooling/l2}/credible_layer/OwnableTarget.sol | 0 .../l2/contracts/src => tooling/l2}/credible_layer/README.md | 2 +- .../l2}/credible_layer/TestOwnershipAssertion.sol | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename {crates/l2/contracts/src => tooling/l2}/credible_layer/OwnableTarget.sol (100%) rename {crates/l2/contracts/src => tooling/l2}/credible_layer/README.md (88%) rename {crates/l2/contracts/src => tooling/l2}/credible_layer/TestOwnershipAssertion.sol (100%) diff --git a/docs/l2/credible_layer.md b/docs/l2/credible_layer.md index 469c50753ca..c399400cb4e 100644 --- a/docs/l2/credible_layer.md +++ b/docs/l2/credible_layer.md @@ -208,7 +208,7 @@ docker run -d --name assertion-da -p 5001:5001 \ PK=0xbcdf20249abf0ed6d944c0288fad489e33f66b3960d9e6229c1cd214ed3bbe31 rex deploy \ - --contract-path /crates/l2/contracts/src/credible_layer/OwnableTarget.sol \ + --contract-path /tooling/l2/credible_layer/OwnableTarget.sol \ --private-key $PK --rpc-url http://localhost:1729 --print-address --remappings "" ``` diff --git a/crates/l2/contracts/src/credible_layer/OwnableTarget.sol b/tooling/l2/credible_layer/OwnableTarget.sol similarity index 100% rename from crates/l2/contracts/src/credible_layer/OwnableTarget.sol rename to tooling/l2/credible_layer/OwnableTarget.sol diff --git a/crates/l2/contracts/src/credible_layer/README.md b/tooling/l2/credible_layer/README.md similarity index 88% rename from crates/l2/contracts/src/credible_layer/README.md rename to tooling/l2/credible_layer/README.md index 51f6726c562..42dd6e0f85f 100644 --- a/crates/l2/contracts/src/credible_layer/README.md +++ b/tooling/l2/credible_layer/README.md @@ -11,7 +11,7 @@ The **State Oracle** is the on-chain registry that maps protected contracts to t assertions. It is maintained by Phylax Systems and must be deployed separately using the Phylax toolchain before starting the Credible Layer sidecar. -See the [Credible Layer docs](../../../../docs/l2/credible_layer.md) for deployment instructions. +See the [Credible Layer docs](../../../docs/l2/credible_layer.md) for deployment instructions. ### References diff --git a/crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol b/tooling/l2/credible_layer/TestOwnershipAssertion.sol similarity index 100% rename from crates/l2/contracts/src/credible_layer/TestOwnershipAssertion.sol rename to tooling/l2/credible_layer/TestOwnershipAssertion.sol