Alternative Arbitrum execution clients still need Nitro for message ingestion. Nitro runs in consensus mode and exposes its execution API over JSON-RPC, and the other client implements the server side.
arbitrum-reth does both halves in one binary. Fine for a standalone node, but an operator can't swap in just one half.
Split it so arbitrum-reth can run three ways:
| Mode |
Role |
| standalone (today) |
consensus + execution |
| execution |
serves the API, driven by Nitro or by arbitrum-reth |
| consensus |
drives the API, against Nitro's gethexec or another client |
Interface
Nitro defines it in execution/interface.go, served under the nitroexecution JSON-RPC namespace (execution/rpcserver, execution/rpcclient).
ExecutionClient: DigestMessage, Reorg, HeadMessageIndex, ResultAtMessageIndex, SetFinalityData, SetConsensusSyncData, MarkFeedStart, maintenance hooks, ArbOSVersionForMessageIndex
ExecutionRecorder: validators and stakers
ExecutionSequencer: sequencing
Only ExecutionClient is in scope. It's enough for a following node.
The seam already exists
- L1 derivation and the feed follower push ordered
BroadcastFeedMessages into a channel, and ArbEngineDriver reads them one at a time. That channel is the boundary.
- arb_revm's
digest_message is the same operation as Nitro's DigestMessage.
- Messages are already numbered by message index rather than block number (
L1SyncConfig::genesis_block), which is what the API uses.
Reorg maps to the existing rewind path, SetFinalityData to forkchoice safe/finalized.
So step 1 below is mostly a refactor.
Why
Adoption: operators can replace one half at a time instead of taking the whole binary.
Differential testing: run arbitrum-reth consensus against Nitro execution, and the reverse. A divergence is then isolated to one side before any block-level debugging. Most parity work here starts with "derivation or execution?", and this answers it directly.
Work
- Extract an
ArbExecutionClient trait from the driver. The current in-process path becomes the first impl. No behaviour change, no new mode, lands on its own.
--mode execution: serve nitroexecution over JSON-RPC. Testable against nitro-testnode with Nitro as consensus.
--mode consensus: an RPC-backed impl of the same trait, so the driver drives a remote client unchanged.
Open questions
- Nitro hashes
MessageWithMetadata (Hash() over its RLP), so the encoding has to match byte for byte, not just structurally. Same for MessageResult.
- Nitro's methods return
PromiseInterface. Over RPC they're already awaited, so this only matters if the in-process path is ever exposed to a Go caller.
SetConsensusSyncData and the maintenance hooks have no equivalent here. Unclear which are needed for a following node and which are Nitro-internal.
- Where the database lives in split mode, and which side owns the resume checkpoint.
- Whether
ExecutionRecorder is needed to be useful to validators, or is a follow-up.
Acceptance
- Standalone unchanged, step 1 with no behavioural diff.
- Nitro consensus plus arbitrum-reth execution gives the same block hashes as standalone over a range on nitro-testnode.
- arbitrum-reth consensus plus Nitro's gethexec gives the same hashes over the same range.
Alternative Arbitrum execution clients still need Nitro for message ingestion. Nitro runs in consensus mode and exposes its execution API over JSON-RPC, and the other client implements the server side.
arbitrum-reth does both halves in one binary. Fine for a standalone node, but an operator can't swap in just one half.
Split it so arbitrum-reth can run three ways:
Interface
Nitro defines it in
execution/interface.go, served under thenitroexecutionJSON-RPC namespace (execution/rpcserver,execution/rpcclient).ExecutionClient:DigestMessage,Reorg,HeadMessageIndex,ResultAtMessageIndex,SetFinalityData,SetConsensusSyncData,MarkFeedStart, maintenance hooks,ArbOSVersionForMessageIndexExecutionRecorder: validators and stakersExecutionSequencer: sequencingOnly
ExecutionClientis in scope. It's enough for a following node.The seam already exists
BroadcastFeedMessages into a channel, andArbEngineDriverreads them one at a time. That channel is the boundary.digest_messageis the same operation as Nitro'sDigestMessage.L1SyncConfig::genesis_block), which is what the API uses.Reorgmaps to the existing rewind path,SetFinalityDatato forkchoice safe/finalized.So step 1 below is mostly a refactor.
Why
Adoption: operators can replace one half at a time instead of taking the whole binary.
Differential testing: run arbitrum-reth consensus against Nitro execution, and the reverse. A divergence is then isolated to one side before any block-level debugging. Most parity work here starts with "derivation or execution?", and this answers it directly.
Work
ArbExecutionClienttrait from the driver. The current in-process path becomes the first impl. No behaviour change, no new mode, lands on its own.--mode execution: servenitroexecutionover JSON-RPC. Testable against nitro-testnode with Nitro as consensus.--mode consensus: an RPC-backed impl of the same trait, so the driver drives a remote client unchanged.Open questions
MessageWithMetadata(Hash()over its RLP), so the encoding has to match byte for byte, not just structurally. Same forMessageResult.PromiseInterface. Over RPC they're already awaited, so this only matters if the in-process path is ever exposed to a Go caller.SetConsensusSyncDataand the maintenance hooks have no equivalent here. Unclear which are needed for a following node and which are Nitro-internal.ExecutionRecorderis needed to be useful to validators, or is a follow-up.Acceptance