refactor engineClient for Unified xlayer binary : implement transport-agnostic engine-client#205
refactor engineClient for Unified xlayer binary : implement transport-agnostic engine-client#205
Conversation
Remove OpEngineApi<Optimism, Http<HyperAuthClient>> as a supertrait of EngineClient. The eight Engine API methods used by the task queue (new_payload_v2/v3/v4, fork_choice_updated_v2/v3, get_payload_v2/v3/v4) are now declared explicitly on EngineClient itself. OpEngineClient's EngineClient impl is updated to delegate to its existing HTTP providers and rollup-boost server — behaviour is unchanged. MockEngineClient in test_utils is consolidated: the redundant OpEngineApi impl block is removed and the same methods are served from the single EngineClient impl. Unused storage fields and builder helpers for OpEngineApi-only methods (get_payload_bodies, exchange_capabilities, etc.) are removed. A new test `test_engine_client_is_transport_agnostic` asserts at compile time that a struct with no HTTP types can implement EngineClient and be used as `dyn EngineClient`, which is the prerequisite for the xlayer-node in-process engine bridge (RethInProcessClient in op-reth). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add RollupNode::start_with_client<E: EngineClient> so callers can supply a pre-built engine client instead of having the node build an HTTP client from EngineConfig. The existing start() method is unchanged in behaviour: it builds OpEngineClient as before and delegates to the new shared body start_with_engine(). To support clients that have no RollupBoost server (e.g. in-process bridges), EngineRpcProcessor.rollup_boost_server is changed from Arc<RollupBoostServer> to Option<Arc<RollupBoostServer>>. When None: - Admin queries are logged and ignored. - Health queries respond with ServiceUnavailable. create_engine_actor is made generic over E: EngineClient and now receives a pre-built Arc<E> + Option<Arc<RollupBoostServer>> instead of building the client itself. This is the injection point required for RethInProcessClient (op-reth), which will implement EngineClient and route Engine API calls over Rust channels instead of HTTP. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| async fn new_payload_v2( | ||
| &self, | ||
| payload: ExecutionPayloadInputV2, | ||
| ) -> TransportResult<PayloadStatus> { | ||
| <L2Provider as OpEngineApi<Optimism, Http<HyperAuthClient>>>::new_payload_v2( | ||
| &self.engine, | ||
| payload, | ||
| ) | ||
| .await | ||
| } |
There was a problem hiding this comment.
delegates the function call to OpEngineAPI which will make http based
The delegation always existed — before it was implicit (compiler wired it via supertrait), now it's explicit (we wrote it by hand). Same machine code, different source structure.
Vui-Chee
left a comment
There was a problem hiding this comment.
Extensibility concern: trait will need modification for every new fork
The transport-agnostic refactor is the right direction. One follow-up concern worth addressing before RethInProcessClient is written.
The problem
versions.rs already has the right version-selection enums:
```rust
EngineNewPayloadVersion { V2, V3, V4 }
EngineForkchoiceVersion { V2, V3 }
EngineGetPayloadVersion { V2, V3, V4 }
```
BuildTask and SealTask already consult these enums via from_cfg(). But then the tasks switch on them to call individual versioned methods on the trait (fork_choice_updated_v2/v3, get_payload_v2/v3/v4, new_payload_v2/v3/v4).
This means every new OP Stack fork (V5, V6, ...) requires:
- New methods added to
EngineClienttrait - New impls on
OpEngineClient - New impls on
MockEngineClient - New impls on
RethInProcessClient(the upcoming in-process bridge) - New match arms in
InsertTask,SealTask,BuildTask
Steps 1–4 are pure boilerplate from a leaky abstraction. Step 5 is unavoidable and desirable (compiler-enforced exhaustiveness check is a safety net).
Suggested fix: enum-dispatched methods
Replace the 8 versioned methods with 3 stable methods. The version enum (or the payload variant itself) becomes the discriminant:
```rust
pub trait EngineClient: Send + Sync {
// OpExecutionPayload variant IS the version — no vN method needed
async fn new_payload(
&self,
envelope: OpExecutionPayloadEnvelope,
) -> TransportResult;
// Version enum replaces fork_choice_updated_v2 / _v3
async fn fork_choice_updated(
&self,
version: EngineForkchoiceVersion,
state: ForkchoiceState,
attrs: Option<OpPayloadAttributes>,
) -> TransportResult<ForkchoiceUpdated>;
// Version enum replaces get_payload_v2 / _v3 / _v4; returns unified envelope
async fn get_payload(
&self,
version: EngineGetPayloadVersion,
payload_id: PayloadId,
) -> TransportResult<OpExecutionPayloadEnvelope>;
// all non-versioned methods unchanged
}
```
Impact on call sites
InsertTask::execute() — the 30-line match that calls new_payload_vN collapses to:
```rust
let response = self.client.new_payload(self.envelope.clone()).await;
```
The OpExecutionPayload variant match (for constructing the OpBlock) stays — that's the compiler safety net we want.
SealTask::seal_payload() — the 30-line match that calls get_payload_vN and normalises three different return types into OpExecutionPayloadEnvelope collapses to:
```rust
let version = EngineGetPayloadVersion::from_cfg(cfg, payload_timestamp);
let payload_envelope = self.engine.get_payload(version, payload_id).await?;
```
The normalisation moves into OpEngineClient::get_payload() where it belongs.
BuildTask::start_build() — the match calling fork_choice_updated_v2/v3 collapses to:
```rust
let update = engine_client.fork_choice_updated(forkchoice_version, new_forkchoice, Some(...)).await?;
```
When V5 arrives
- Add
V5to the version enums inversions.rs✓ - Add
V5match arm inOpEngineClient::new_payload()/get_payload()— one place each ✓ - Compiler forces new match arms in the tasks — the safety check we want ✓
- No trait changes. No new impl blocks on Mock or RethInProcessClient. ✓
Recommendation
This is worth doing before RethInProcessClient is written. Once that impl exists, this refactor touches one more file. The trait surface is the contract between kona-node and the in-process bridge — better to get it right before it has two implementors.
@Vui-Chee this sounds nice. will add a new task to enhance this to support versioning. probably in to same PR CC @cliff0412 |
Context
Part of the xlayer-node initiative: running kona-node (CL/derivation) and op-reth (EL) in the same OS process, replacing the HTTP Engine API hop with direct Rust channel communication.
Current architecture:
Target architecture:
This PR lays the groundwork — it does not yet implement the in-process client. It makes the existing
EngineClienttrait andRollupNodeservice ready to accept one.Problem
EngineClientwas defined as:The
OpEngineApi<Optimism, Http<HyperAuthClient>>supertrait hardcodes the HTTP transport into the interface. Any struct withoutHyperAuthClient— including an in-process channel bridge — cannot implementEngineClient. There is also no way to inject a pre-built client intoRollupNode::start(); the node always constructs its ownOpEngineClientinternally.Changes
Commit 1 —
refactor(kona-engine): make EngineClient transport-agnosticFiles:
client.rs,test_utils/engine_client.rsOpEngineApi<Optimism, Http<HyperAuthClient>>supertrait fromEngineClient.new_payload_v2/v3/v4,fork_choice_updated_v2/v3,get_payload_v2/v3/v4) are now declared explicitly onEngineClientitself.impl EngineClient for OpEngineClientdelegates each method to the existing HTTP providers — zero behaviour change on the HTTP path.impl OpEngineApi for OpEngineClientis kept intact; other callers using it directly are unaffected.MockEngineClientconsolidated from two impl blocks into one; dead fields removed (get_payload_bodies,exchange_capabilities, etc. — unused byEngineClient).test_engine_client_is_transport_agnostic: compile-time proof that a struct with no HTTP types satisfiesEngineClientand can be used asdyn EngineClient.Commit 2 —
feat(kona-node): expose injection point for pluggable engine clientFiles:
rpc_request_processor.rs,service/node.rsEngineRpcProcessor.rollup_boost_server:Arc<RollupBoostServer>→Option<Arc<RollupBoostServer>>.Nonewhen using an in-process client (no RollupBoost sidecar in that topology).Noneserver: logged + ignored.Noneserver: respondServiceUnavailable(honest — the service is not present).create_engine_actormade generic overE: EngineClient. Accepts a pre-builtArc<E>andOption<Arc<RollupBoostServer>>. No longer fallible (client building moved out).RollupNode::start()unchanged in behaviour: buildsOpEngineClient, extractsrollup_boost, delegates to shared body.RollupNode::start_with_client<E: EngineClient + 'static>(engine_client: Arc<E>): public entry point for in-process integration. PassesNonefor RollupBoost.start_with_engine<E>: shared actor-wiring body. Bothstart()andstart_with_client()converge here — no duplication.What does NOT change
start()) — identical behaviour, sameOpEngineClient, same RollupBoost wiring.OpEngineApitrait and its impl onOpEngineClient— untouched.RollupNodeexcept the addition ofstart_with_client.EngineConfig— still buildsOpEngineClientfor the HTTP path.Testing
kona-engineunit tests pass including the newtest_engine_client_is_transport_agnostic.kona-node-servicecompiles clean (10 pre-existing warnings, zero new).Follow-up (not in this PR)
RethInProcessClient(inokx/reth) — implementsEngineClient, routes engine calls ontoBeaconEngineMessagechannel directly into op-reth's engine tree.xlayer-nodebinary — starts op-reth, createsRethInProcessClient, callsrollup_node.start_with_client(Arc::new(client)).--authrpcconfig from unified binary (no HTTP auth needed).