Skip to content

refactor engineClient for Unified xlayer binary : implement transport-agnostic engine-client#205

Open
defistar wants to merge 4 commits intodevfrom
feat/xlayer-in-process-engine
Open

refactor engineClient for Unified xlayer binary : implement transport-agnostic engine-client#205
defistar wants to merge 4 commits intodevfrom
feat/xlayer-in-process-engine

Conversation

@defistar
Copy link

@defistar defistar commented Mar 17, 2026

PR Description

Groundwork for running kona and op-reth in the same process. No in-process client yet — just makes the trait and service ready to accept one.

What changed

EngineClient trait — HTTP supertrait removed, engine methods declared explicitly:

// before — Http<HyperAuthClient> baked into the interface, nothing else can implement this
pub trait EngineClient: OpEngineApi<Optimism, Http<HyperAuthClient>> + Send + Sync { ... }

// after — transport-agnostic, any impl works
pub trait EngineClient: Send + Sync {
    async fn new_payload_v2/v3/v4(...);
    async fn fork_choice_updated_v2/v3(...);
    async fn get_payload_v2/v3/v4(...);
}

OpEngineClient (HTTP) still delegates to the same providers — no behaviour change.

RollupNode — injection point added:

flowchart LR
    subgraph before["before"]
        S1["start()"] --> B1["build OpEngineClient\ninternally"] --> W1["wire actors"]
    end

    subgraph after["after"]
        S2["start()"] --> B2["build OpEngineClient"] --> SE["start_with_engine()"] --> W2["wire actors"]
        S3["start_with_client(Arc&lt;E&gt;)"] --> SE
    end
Loading

rollup_boost_server on EngineRpcProcessor: Arc<_>Option<Arc<_>>None on the in-process path.

MockEngineClient consolidated; added test_engine_client_is_transport_agnostic.

Follow-up

  • okx/reth: RethInProcessClient implements EngineClient — routes calls onto BeaconEngineMessage channel into op-reth's engine tree
  • okx/xlayer: binary wires both, calls rollup_node.start_with_client(Arc::new(client))

defistar and others added 2 commits March 17, 2026 17:08
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>
@defistar defistar self-assigned this Mar 17, 2026
@defistar defistar changed the title refactor(kona-engine): make EngineClient transport-agnostic refactor engineClient for Unified xlayer binary : implement transport-agnostic engine-client Mar 17, 2026
@defistar defistar added the enhancement New feature or request label Mar 17, 2026
@defistar defistar requested a review from KyrinCode March 17, 2026 10:38
Comment on lines +372 to +381
async fn new_payload_v2(
&self,
payload: ExecutionPayloadInputV2,
) -> TransportResult<PayloadStatus> {
<L2Provider as OpEngineApi<Optimism, Http<HyperAuthClient>>>::new_payload_v2(
&self.engine,
payload,
)
.await
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

@Vui-Chee Vui-Chee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. New methods added to EngineClient trait
  2. New impls on OpEngineClient
  3. New impls on MockEngineClient
  4. New impls on RethInProcessClient (the upcoming in-process bridge)
  5. 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 V5 to the version enums in versions.rs
  • Add V5 match arm in OpEngineClient::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.

@defistar
Copy link
Author

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:

  1. New methods added to EngineClient trait
  2. New impls on OpEngineClient
  3. New impls on MockEngineClient
  4. New impls on RethInProcessClient (the upcoming in-process bridge)
  5. 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:

// Version enum replaces fork_choice_updated_v2 / _v3
async fn fork_choice_updated(
&self,
version: EngineForkchoiceVersion,
state: ForkchoiceState,
attrs: Option,
) -> TransportResult;

// Version enum replaces get_payload_v2 / _v3 / _v4; returns unified envelope
async fn get_payload(
&self,
version: EngineGetPayloadVersion,
payload_id: PayloadId,
) -> TransportResult;

// 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 `V5` to the version enums in `versions.rs` ✓
* Add `V5` match arm in `OpEngineClient::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.

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:

  1. New methods added to EngineClient trait
  2. New impls on OpEngineClient
  3. New impls on MockEngineClient
  4. New impls on RethInProcessClient (the upcoming in-process bridge)
  5. 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:

// Version enum replaces fork_choice_updated_v2 / _v3
async fn fork_choice_updated(
&self,
version: EngineForkchoiceVersion,
state: ForkchoiceState,
attrs: Option,
) -> TransportResult;

// Version enum replaces get_payload_v2 / _v3 / _v4; returns unified envelope
async fn get_payload(
&self,
version: EngineGetPayloadVersion,
payload_id: PayloadId,
) -> TransportResult;

// 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 `V5` to the version enums in `versions.rs` ✓
* Add `V5` match arm in `OpEngineClient::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
and we can conclude this PR with this pending change. Trying to mantain micro PRs as lean as possible

CC @cliff0412

@defistar
Copy link
Author

defistar commented Mar 19, 2026

@Vui-Chee Planning a follow-up PR to collapse to 3 methods before RethInProcessClient is implemented. Will keep this PR scoped to the trait/injection changes as is.

started working on this equivalent changes in okx/reth , will get back to this change proposed in a new PR instead

defistar and others added 2 commits March 20, 2026 08:21
…num-dispatched

Replace new_payload_v1/v2/v3/v4, fork_choice_updated_v2/v3, and get_payload_v2/v3/v4
on the EngineClient trait with three unified methods:
  - new_payload(OpExecutionPayloadEnvelope)
  - fork_choice_updated(EngineForkchoiceVersion, ForkchoiceState, Option<OpPayloadAttributes>)
  - get_payload(EngineGetPayloadVersion, PayloadId) -> OpExecutionPayloadEnvelope

Version selection logic moves into OpEngineClient's impl; all task files
(insert, build, seal, synchronize) call one method per operation. MockEngineClient
and its builder/setter API simplified to match. Compiles clean with zero warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants