Skip to content

feat(l2): integrate Phylax Credible Layer into L2 sequencer#6488

Draft
avilagaston9 wants to merge 46 commits intofeat/l1/prestate-tracerfrom
feat/l2/circuit-breaker-integration
Draft

feat(l2): integrate Phylax Credible Layer into L2 sequencer#6488
avilagaston9 wants to merge 46 commits intofeat/l1/prestate-tracerfrom
feat/l2/circuit-breaker-integration

Conversation

@avilagaston9
Copy link
Copy Markdown
Contributor

@avilagaston9 avilagaston9 commented Apr 15, 2026

Depends on: #6496 (eth_subscribe/eth_unsubscribe WS support) — must be merged first.

Motivation

Integrate the Phylax Credible Layer into ethrex's L2 sequencer. This allows protocol teams to write Solidity-based security assertions that are validated against every transaction before block inclusion — violating transactions are silently dropped.

Description

When enabled via --credible-layer, the L2 block producer communicates with the Credible Layer Assertion Enforcer sidecar via gRPC during block building. For each candidate transaction, a Transaction event is sent to the sidecar, which evaluates it against registered assertions. If ASSERTION_FAILED, the transaction is dropped. On any error or timeout, transactions are included (permissive behavior — liveness over safety).

Implementation:

  • gRPC client (tonic/prost) implementing sidecar.proto (StreamEvents bidirectional stream, GetTransaction polling)
  • Block building hooks in fill_transactions: per-tx Credible Layer check for non-privileged txs, NewIteration/CommitHead lifecycle events
  • L2 WebSocket server with eth_subscribe("newHeads") — required by the sidecar for live chain state tracking (implemented in feat(l1): add eth_subscribe/eth_unsubscribe WebSocket subscription support #6496)
  • prestateTracer (diff mode) for debug_traceBlockByNumber — required by the sidecar to build its local state DB
  • eth_getLogs fix: accept requests without topics parameter (Ethereum spec compliance, needed by the sidecar's assertion indexer)
  • Test contracts (OwnableTarget.sol, TestOwnershipAssertion.sol) in tooling/l2/credible_layer/ for e2e validation

How to Test

Full end-to-end guide in docs/l2/credible_layer.md — 10 verified steps from L1 startup to assertion rejection. Summary:

  1. Start L1, deploy L2 contracts, start L2 with --credible-layer
  2. Deploy State Oracle on L2 using Phylax's credible-layer-contracts Forge scripts
  3. Deploy OwnableTarget (from tooling/l2/credible_layer/), compile OwnableAssertion from credible-layer-starter, upload to assertion DA, register on State Oracle
  4. Start sidecar-indexer (Docker) + real Phylax sidecar (Docker)
  5. Send transferOwnershipdropped (assertion violation detected by real sidecar)
  6. Send doSomething()included (no assertion triggered)

Tested end-to-end with the real Phylax sidecar Docker image (ghcr.io/phylaxsystems/credible-sdk/sidecar:main), assertion-da, sidecar-indexer, and credible-layer-contracts deployment scripts.

Checklist

  • Code compiles (cargo check -p ethrex --features l2)
  • Unit tests pass
  • End-to-end tested with real Phylax sidecar
  • Documentation: docs/l2/credible_layer.md with verified step-by-step guide
  • Zero overhead when disabled (no gRPC calls when --credible-layer is not set)
  • CI passing (pending)

@avilagaston9 avilagaston9 self-assigned this Apr 15, 2026
@avilagaston9 avilagaston9 changed the title Integrate Phylax Credible Layer (Circuit Breaker) into L2 sequencer feat(l2): integrate Phylax Credible Layer (Circuit Breaker) into L2 sequencer Apr 15, 2026
@avilagaston9 avilagaston9 changed the title feat(l2): integrate Phylax Credible Layer (Circuit Breaker) into L2 sequencer feat(l2): integrate Phylax Credible Layer into L2 sequencer Apr 15, 2026
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.
…naming

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.
Build with COMPILE_CONTRACTS=true once in prerequisites, then use the
binary directly in Step 2 instead of cargo run which triggers a rebuild.
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).
Docker logs contain ANSI escape sequences that break plain grep matching.
Add sed pipe to strip them in all docker logs grep commands.
… 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).
rex deploy can compile and deploy Solidity directly, no need for
manual solc compilation + cast send --create.
…mentation 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.
…m 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.
@avilagaston9 avilagaston9 force-pushed the feat/l2/circuit-breaker-integration branch from e311925 to 9d8569b Compare April 16, 2026 15:29
@github-actions github-actions Bot added the L2 Rollup client label Apr 16, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 16, 2026

Lines of code report

Total lines added: 545
Total lines removed: 0
Total lines changed: 545

Detailed view
+--------------------------------------------------------------+-------+------+
| File                                                         | Lines | Diff |
+--------------------------------------------------------------+-------+------+
| ethrex/cmd/ethrex/l2/options.rs                              | 1120  | +16  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/build.rs                                    | 15    | +4   |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/block_producer.rs                 | 347   | +24  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/block_producer/payload_builder.rs | 283   | +49  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/configs.rs                        | 113   | +5   |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/credible_layer/client.rs          | 338   | +338 |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/credible_layer/errors.rs          | 14    | +14  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/credible_layer/mod.rs             | 74    | +74  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/l2/sequencer/mod.rs                            | 295   | +18  |
+--------------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/hooks/l2_hook.rs                   | 675   | +3   |
+--------------------------------------------------------------+-------+------+

@avilagaston9 avilagaston9 changed the base branch from main to feat/l1/ws-subscriptions April 16, 2026 15:46
Integrates the SubscriptionManager actor refactoring (replacing broadcast channels)
with the Credible Layer integration. Updates WS CLI flags in the credible layer guide
from the removed --l2.ws-enabled/--l2.ws-port to --ws.enabled/--ws.port, fixes
rex address and rex deploy command syntax, and removes unused TxExecutionId import.
…_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.
…ectly

preserved during the merge and is unrelated to the Credible Layer integration.
@avilagaston9 avilagaston9 changed the base branch from feat/l1/ws-subscriptions to feat/l1/prestate-tracer April 17, 2026 18:06
…tor,

split transaction evaluation into pre/post execution phases, and simplify
the block producer interface.

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.
…ssing 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.
…oncurrency

primitives: connection is established in #[started], the ack stream is bridged
into actor messages via spawn_listener, and reconnection on failure is scheduled
with send_after. This ties the background stream lifecycle to the actor — when
the actor stops, the stream listener is automatically cancelled. The Arc<AtomicBool>
for tracking connection state is replaced by a plain bool in the actor's owned state.
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.
…ient

already wraps the channel internally and tonic clients are Clone, so
try_connect just clones grpc_client for new stream connections.
… handle

to sidecar), grpc_client -> sidecar_client, connected -> stream_connected,
try_connect -> open_event_stream. Remove unnecessary clone of sidecar_client
when opening the stream — use self.sidecar_client directly since the stream
lives independently once established.
…n mod.rs,

next to the proto type definitions. Removes build_transaction_env from client.rs
— the handler now just calls (tx, sender).into().
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.
and update sp1/risc0 guest program Cargo.lock files to fix CI check-cargo-lock.
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.
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.
this integration) and update all Cargo.lock files to fix CI check-cargo-lock.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 2.999 ± 0.028 2.977 3.052 1.12 ± 0.02
main_levm_BubbleSort 2.681 ± 0.034 2.643 2.739 1.00
pr_revm_BubbleSort 3.013 ± 0.026 2.978 3.064 1.12 ± 0.02
pr_levm_BubbleSort 2.682 ± 0.022 2.653 2.724 1.00 ± 0.02

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 981.7 ± 10.5 969.0 1005.1 1.00
main_levm_ERC20Approval 1021.1 ± 11.6 1001.7 1038.3 1.04 ± 0.02
pr_revm_ERC20Approval 987.0 ± 6.6 979.8 1001.3 1.01 ± 0.01
pr_levm_ERC20Approval 1021.4 ± 14.2 1010.0 1054.6 1.04 ± 0.02

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 133.6 ± 0.9 132.5 135.5 1.00 ± 0.02
main_levm_ERC20Mint 146.5 ± 1.7 145.4 151.3 1.10 ± 0.02
pr_revm_ERC20Mint 133.1 ± 2.0 131.1 136.1 1.00
pr_levm_ERC20Mint 148.3 ± 1.6 146.8 151.1 1.11 ± 0.02

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 231.4 ± 1.1 230.4 234.0 1.01 ± 0.01
main_levm_ERC20Transfer 248.8 ± 2.6 246.3 254.6 1.08 ± 0.01
pr_revm_ERC20Transfer 230.0 ± 1.0 229.3 232.7 1.00
pr_levm_ERC20Transfer 253.7 ± 9.9 248.7 281.3 1.10 ± 0.04

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 227.3 ± 12.6 220.7 262.2 1.02 ± 0.06
main_levm_Factorial 250.8 ± 5.9 247.0 266.9 1.12 ± 0.03
pr_revm_Factorial 223.8 ± 0.8 222.9 225.8 1.00
pr_levm_Factorial 250.4 ± 1.9 247.6 253.9 1.12 ± 0.01

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.633 ± 0.025 1.592 1.671 1.04 ± 0.02
main_levm_FactorialRecursive 1.594 ± 0.016 1.570 1.612 1.01 ± 0.02
pr_revm_FactorialRecursive 1.639 ± 0.020 1.611 1.678 1.04 ± 0.02
pr_levm_FactorialRecursive 1.576 ± 0.018 1.556 1.599 1.00

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 202.7 ± 1.1 201.3 205.2 1.00
main_levm_Fibonacci 224.6 ± 1.9 222.8 228.3 1.11 ± 0.01
pr_revm_Fibonacci 203.0 ± 2.4 199.3 209.0 1.00 ± 0.01
pr_levm_Fibonacci 226.2 ± 6.6 220.0 243.2 1.12 ± 0.03

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 848.5 ± 11.0 824.8 863.4 1.24 ± 0.02
main_levm_FibonacciRecursive 688.5 ± 6.0 683.4 702.2 1.00 ± 0.01
pr_revm_FibonacciRecursive 849.0 ± 8.1 833.5 862.6 1.24 ± 0.02
pr_levm_FibonacciRecursive 685.8 ± 5.7 677.6 695.7 1.00

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.3 ± 0.0 8.2 8.4 1.00
main_levm_ManyHashes 9.6 ± 0.1 9.5 9.7 1.16 ± 0.01
pr_revm_ManyHashes 8.4 ± 0.0 8.3 8.4 1.00 ± 0.01
pr_levm_ManyHashes 9.7 ± 0.1 9.6 9.8 1.16 ± 0.01

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 260.1 ± 4.7 256.8 271.2 1.12 ± 0.02
main_levm_MstoreBench 231.8 ± 1.0 230.6 233.6 1.00 ± 0.01
pr_revm_MstoreBench 261.5 ± 6.7 256.8 275.3 1.13 ± 0.03
pr_levm_MstoreBench 231.4 ± 1.2 229.8 233.9 1.00

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 286.4 ± 1.7 284.3 290.7 1.03 ± 0.03
main_levm_Push 287.4 ± 2.0 283.0 290.5 1.03 ± 0.03
pr_revm_Push 290.9 ± 3.3 284.9 295.4 1.04 ± 0.03
pr_levm_Push 278.8 ± 7.8 274.1 300.6 1.00

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 170.8 ± 2.1 167.4 175.2 1.71 ± 0.02
main_levm_SstoreBench_no_opt 99.8 ± 0.2 99.3 100.0 1.00
pr_revm_SstoreBench_no_opt 170.3 ± 1.4 167.3 171.3 1.71 ± 0.02
pr_levm_SstoreBench_no_opt 101.0 ± 1.4 99.5 102.5 1.01 ± 0.01

…ssertion.sol)

from crates/l2/contracts/src/credible_layer/ to tooling/l2/credible_layer/ since
the sequencer never compiles or references them — they exist only for the e2e
setup guide. Update contract path in docs and relative doc link in README.
…explicit by adding example output for the owner() call before the violation test and clarifying that the value after must match exactly.
…_transaction restores SSTORE changes. Without this fix, the Credible Layer demo corrupts state when a transaction is dropped because the L2Hook cleared the call_frame_backup before BackupHook could capture storage slot rollback data.
…_build step in crates/l2/build.rs which compiles sidecar.proto for the Credible Layer gRPC client.
…e_err on actor constructor, replace disallowed as_u32 with u32::try_from
…uilds

are not burdened with an L2-only dependency. Protoc is now installed only in
L2 CI jobs (via arduino/setup-protoc) and conditionally in the Docker builder
stage when BUILD_FLAGS contains l2. Also revert unnecessary diff in rpc.rs
(derive order, expect→allow), and fix result_large_err properly by boxing
CredibleLayerError instead of suppressing the lint with #[allow].
…protoc

action following the same pattern as install-solc (curl download from official
protobuf releases). In the Dockerfile, install protoc alongside solc in the
same RUN block instead of the hacky BUILD_FLAGS conditional. Revert the
unnecessary setup-rust addition to the L1 check-cargo-locks job — cargo
metadata doesn't need a specific toolchain.
…protoc.

The build.rs now calls protox::compile() to produce a FileDescriptorSet and
feeds it into tonic-build via compile_fds(), so no external protoc binary is
needed at build time. This removes the need to install protoc in CI workflows
and the Dockerfile entirely — both are reverted to main.
toolchain from rust-toolchain.toml (1.91.0). Without it, the job uses
whatever Rust version is pre-installed on the runner and cargo metadata
--locked can fail with 'lock file needs to be updated' because a different
cargo version resolves some transitive dependencies differently (e.g.
ruint 1.17.2 -> 1.18.0). Also remove a now-redundant comment in build.rs.
@avilagaston9 avilagaston9 force-pushed the feat/l2/circuit-breaker-integration branch from c66cc58 to ef9a11a Compare April 22, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L2 Rollup client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant