Skip to content
This repository was archived by the owner on Jan 16, 2026. It is now read-only.

feat(node): Kona Light CL#3245

Merged
pcw109550 merged 6 commits intomainfrom
pcw109550/kona-light-cl-2
Jan 14, 2026
Merged

feat(node): Kona Light CL#3245
pcw109550 merged 6 commits intomainfrom
pcw109550/kona-light-cl-2

Conversation

@pcw109550
Copy link
Copy Markdown
Contributor

@pcw109550 pcw109550 commented Jan 12, 2026

Base branch: #3242

May conflict with #3229, #3253

Implement the kona light CL(Design doc):

flowchart TB

subgraph A ["Normal Mode (Derivation)"]
  direction TB

  subgraph A0 ["Rollup Node Service"]
    direction TB
    A_Derivation["DerivationActor<br/>(L1->L2 derivation)"]
    A_Engine["EngineActor"]
    A_UnsafeSrc["Unsafe Source<br/>(P2P gossip / Sequencer)"]
  end

  A_L1[(L1 RPC)]
  A_EL[(Execution Layer)]

  A_L1 -->|L1 info| A_Derivation
  A_UnsafeSrc -->|unsafe| A_Engine
  A_Derivation -->|"safe(attr)/finalized"| A_Engine
  A_Engine -->|engine API| A_EL
end

subgraph B ["Light CL Mode"]
  direction TB

  subgraph B0 ["Rollup Node Service"]
    direction TB
    B_DerivationX[["DerivationActor<br/>(NEW: Poll external syncStatus)"]]
    B_Engine["EngineActor"]
    B_UnsafeSrc["Unsafe Source<br/>(P2P gossip / Sequencer)"]
  end

  B_L1[(L1 RPC)]
  B_Ext[(External CL RPC<br/>optimism_syncStatus)]
  B_EL[(Execution Layer)]

  %% Connections
  B_Ext -->|safe/finalized/currentL1| B_DerivationX
  B_L1 -->|canonical L1 check| B_DerivationX
  B_DerivationX -->|"safe(blockInfo)/finalized (validated)"| B_Engine
  B_UnsafeSrc -->|unsafe| B_Engine

  %% Visual indicator for disabled actor
  B_Engine -->|engine API| B_EL
end
Loading

Testing

Acceptance Tests

Running guidelines detailed at #3199:

Injecting CurrentL1 is blocked by kona: Revise SyncStatus CurrentL1 Selection

Local Sync Tests

Validated with syncing op-sepolia between kona-node light CL <> sync tester, successfully finishing the initial EL sync and progress every safety levels reaching each tip.

Devnet Tests

Commit 0b36fdd is baked to us-docker.pkg.dev/oplabs-tools-artifacts/dev-images/kona-node:0b36fdd-light-cl and deployed at changwan-0 devnet:

  • As a verifier: changwan-0-kona-geth-f-rpc-3 [grafana]
  • As a sequencer: changwan-0-kona-geth-f-sequencer-3 [grafana]
    • As a standby | leader

Noticed all {unsafe, safe, finalized} head progression as a kona node light CL.

@pcw109550 pcw109550 force-pushed the pcw109550/kona-light-cl-2 branch 2 times, most recently from 6cb90c6 to c266ef9 Compare January 12, 2026 19:41
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 12, 2026

Codecov Report

❌ Patch coverage is 0% with 244 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.6%. Comparing base (ab0a370) to head (0b36fdd).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...e/service/src/actors/derivation/delegated/actor.rs 0.0% 116 Missing ⚠️
...de/engine/src/task_queue/tasks/consolidate/task.rs 0.0% 76 Missing ⚠️
crates/node/service/src/service/node.rs 0.0% 22 Missing ⚠️
crates/node/service/src/service/builder.rs 0.0% 15 Missing ⚠️
.../service/src/actors/derivation/delegated/client.rs 0.0% 10 Missing ⚠️
...ode/service/src/actors/derivation/engine_client.rs 0.0% 2 Missing ⚠️
...vice/src/actors/engine/engine_request_processor.rs 0.0% 2 Missing ⚠️
crates/node/service/src/actors/derivation/actor.rs 0.0% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (ab0a370) and HEAD (0b36fdd). Click for more details.

HEAD has 19 uploads less than BASE
Flag BASE (ab0a370) HEAD (0b36fdd)
proof 7 0
e2e 11 0
unit 2 1

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pcw109550 pcw109550 force-pushed the pcw109550/kona-light-cl-2 branch from c266ef9 to 968fb96 Compare January 12, 2026 20:24
Base automatically changed from pcw109550/move-l2-finalization-derivationActor to main January 12, 2026 20:56
@pcw109550 pcw109550 force-pushed the pcw109550/kona-light-cl-2 branch from ab6a7fe to e670cb3 Compare January 12, 2026 21:00
@pcw109550 pcw109550 marked this pull request as ready for review January 12, 2026 21:28
@pcw109550 pcw109550 marked this pull request as draft January 13, 2026 07:26
@pcw109550 pcw109550 force-pushed the pcw109550/kona-light-cl-2 branch 8 times, most recently from b6ed730 to 58f2418 Compare January 13, 2026 16:01
@pcw109550 pcw109550 marked this pull request as ready for review January 13, 2026 16:54
Copy link
Copy Markdown
Collaborator

@op-will op-will left a comment

Choose a reason for hiding this comment

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

Looks good!

I left a number of nit comments, but there are a few different things that we can do to add clarity, enable unit testing, and/or future-proof logic that might be useful.

Comment on lines +139 to +141
/// The source must be an OP Stack L2 CL RPC exposing optimism_syncStatus.
#[arg(long, visible_alias = "l2.follow.source", env = "KONA_NODE_L2_FOLLOW_SOURCE")]
pub l2_follow_source: Option<Url>,
Copy link
Copy Markdown
Collaborator

@op-will op-will Jan 13, 2026

Choose a reason for hiding this comment

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

Can we make this any clearer to prevent misconfiguration?

Maybe something like this?

Suggested change
/// The source must be an OP Stack L2 CL RPC exposing optimism_syncStatus.
#[arg(long, visible_alias = "l2.follow.source", env = "KONA_NODE_L2_FOLLOW_SOURCE")]
pub l2_follow_source: Option<Url>,
/// If configured, we will not run derivation, instead trusting the provided delegate to do so. The provided url must be an OP Stack L2 CL RPC exposing optimism_syncStatus.
#[arg(long, visible_alias = "l2.derivation.delegate.rpc.url", env = "KONA_NODE_L2_DERIVATION_DELEGATE_RPC_URL")]
pub l2_derivation_delegate_rpc_url: Option<Url>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agreed, would even love to add the URL to the design doc if possible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason I chose the flag name l2.follow.source is to leave room for a small namespace around how the CL follows an external L2 source, e.g.:

  • l2.follow.source
  • l2.follow.verify
  • l2.follow.poll-interval

This mirrors the existing naming in op-node, which already uses the l2.follow.* prefix, so I was trying to stay consistent with established conventions.

Conceptually, the CL is "following" an external L2 derivation source, which is why follow felt like the right abstraction to me. That said, I understand the concern about misconfiguration and ambiguity, especially since this is an RPC URL and not just an abstract source.

Open to tightening the name if we think the explicitness is worth the verbosity, and I agree that adding the concrete URL to the design doc would help reduce confusion regardless of the final flag name.

Comment thread crates/node/engine/src/task_queue/tasks/consolidate/task.rs Outdated
Comment thread crates/node/engine/src/task_queue/tasks/consolidate/task.rs Outdated
Comment thread crates/node/engine/src/task_queue/tasks/consolidate/task.rs
Comment thread crates/node/engine/src/task_queue/tasks/consolidate/task.rs Outdated
Comment thread crates/node/service/src/actors/derivation/actor.rs Outdated
Comment thread crates/node/service/src/actors/derivation/engine_client.rs
Comment thread crates/node/service/src/actors/engine/engine_request_processor.rs Outdated
Comment thread crates/node/service/src/service/builder.rs Outdated
Comment thread crates/node/service/src/service/builder.rs Outdated

/// Optional derivation delegation client.
#[clap(flatten)]
pub derivation_delegate_args: DerivationDelegateArgs,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: we could remove the option inside DerivationDelegateArgs and instead just wrap the whole thing inside an Option so that it is really optional to use the derivation delegation client:

Suggested change
pub derivation_delegate_args: DerivationDelegateArgs,
pub derivation_delegate_args: Option<DerivationDelegateArgs>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can we rely on the optionality of DerivationDelegateConfig instead of DerivationDelegateArgs?

/// L2 derivation delegate connection arguments.
#[derive(Clone, Debug, Default, clap::Args)]
pub struct DerivationDelegateArgs {
    /// The source must be an OP Stack L2 CL RPC exposing optimism_syncStatus.
    #[arg(long, visible_alias = "l2.follow.source", env = "KONA_NODE_L2_FOLLOW_SOURCE")]
    pub l2_follow_source: Option<Url>,
}

impl DerivationDelegateArgs {
    /// Builds the derivation delegate configuration if an L2 CL URL was provided.
    pub fn config(self) -> Option<DerivationDelegateConfig> {
        self.l2_follow_source.map(|url| DerivationDelegateConfig { l2_cl_url: url })
    }
}

I followed the existing pattern for the L2ClientArgs.

Comment thread crates/node/service/src/actors/derivation/actor.rs Outdated
Comment thread crates/node/service/src/actors/derivation/actor.rs Outdated
Copy link
Copy Markdown
Member

@theochap theochap left a comment

Choose a reason for hiding this comment

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

General question: how hard/cumbersome would it be to make the delegate derivation actor be a separate implementation of some derivation actor trait? Depending on the CLI inputs we could decide to either spawn a normal or a "delegate derivation actor". There's probably some plumbing to do at the RollupService level, but it would be really helpful down the line. The normal and the light derivation actors actually share very little logic so this would make sense to have them implement the same trait.

If this is too long/hard to do in this PR, I am happy to track this as a follow-up work. It would be really helpful though to get it done sooner than later (and also a great testimony to how flexible kona's architecture can be!)

Comment thread crates/node/service/src/actors/derivation/actor.rs Outdated
{
/// Hardcoded poll interval for Derivation Delegation
const DERIVATION_DELEGATE_POLL_INTERVAL: std::time::Duration =
std::time::Duration::from_secs(4);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We probably said it somewhere, but would it make sense to use websockets instead? Is that on the roadmap as a future improvement? Hardcoded polls are quite brittle in my experience, specially if we want to guarantee a similar level of responsiveness we had before the light CL introduction

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question. Today, the light CL design consumes delegate derivation info via the HTTP RPC optimism_syncStatus. As far as I know, op-node does not currently expose this over WebSockets, so polling is effectively the only option right now.

That said, I agree that hardcoded polling can be brittle if we want to preserve the same level of responsiveness. There is definitely room to generalize both the connection mechanism and the data source.

I would consider WS / push based updates a reasonable future improvement rather than something we can rely on today.

That said, the external source itself could potentially be generalized to an EL source ( via eth_getBlockByNumber(safe|finalized)).

Comment thread crates/node/service/src/actors/derivation/actor.rs Outdated
Comment thread crates/node/service/src/actors/derivation/actor.rs Outdated
Comment thread crates/node/service/src/actors/derivation/actor.rs Outdated
Comment thread crates/node/service/src/actors/derivation/actor.rs Outdated
Comment thread crates/node/service/src/actors/derivation/delegate_client.rs Outdated
Comment thread crates/node/service/src/actors/derivation/delegate_client.rs Outdated

/// Sends a safe L2 block for consolidation (Derivation Delegate mode).
/// Note: This does not wait for the engine to process it.
async fn send_safe_l2_block(&self, safe_l2: L2BlockInfo) -> EngineClientResult<()>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we had a separate DelegateDerivationActor struct we could:

  • Define an associated type
  • Add this associated type as an argument of send_derived_attributes
  • For the normal derivation client implementation the associated type can be OpAttributesWithParent, for the delegate client this can be L2BlockInfo.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My understanding is that we want to use a unified EngineActorRequest::ProcessSafeL2SignalRequest. This request must handle two different types, l2blockinfo and attributes. So we need a enum or a trait(suggested at #3245 (comment)).

if we use the associated type, we still need to convert or wrap with the trait / enum. afaik we cannot use the traits because of #3245 (comment).

I think it is better to use the union (trait / enum) and combine two safe related method into one like:

 async fn send_safe_l2_signal(&self, signal: ConsolidateInput) -> EngineClientResult<()>;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Attempted to unify by using enums at 442d9f2

},
/// Derivation Delegation: consolidate based on safe L2 block info.
BlockInfo(L2BlockInfo),
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of making it an enum, I'd make two structures and have them implement the same trait. The parent struct (that holds the ConsolidateInput) can be generic over the ConsolidateInput and when we do the actor wiring (after CLI parsing), we can decide which type to plug in there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I attempted this approach. So making the ConsolidateInput as a trait. Currently ConsolidateInput is a field of ConsolidateTask:

pub struct ConsolidateTask<EngineClient_: EngineClient> {
    /// The engine client.
    pub client: Arc<EngineClient_>,
    /// The [`RollupConfig`].
    pub cfg: Arc<RollupConfig>,
    /// The input for consolidation (either attributes or block info).
    pub input: ConsolidateInput,
}

So this struct will receive another trait like

pub struct ConsolidateTask<EngineClient_: EngineClient, ConsolidateInput_: ConsolidateInput> {
   /// The engine client.
   pub client: Arc<EngineClient_>,
   /// The [`RollupConfig`].
   pub cfg: Arc<RollupConfig>,
   /// The input for consolidation (either attributes or block info).
   pub input: ConsolidateInput_,
}

The tasks are consumed at

pub fn enqueue(&mut self, task: EngineTask<EngineClient_>) {
,

    pub fn enqueue(&mut self, task: EngineTask<EngineClient_>) {
        self.tasks.push(task);
        self.task_queue_length.send_replace(self.tasks.len());
    }

so if we add an additional trait, my understanding is that all other tasks must be aware of this type. Are there any clear methods to use the additional trait types in this case?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah I suppose that this would entail that EngineTask become generic, ie instead of being an enum, they implement a trait.

Let's leave it that way for now. I will revisit in the future and try to simplify this structure

@pcw109550 pcw109550 force-pushed the pcw109550/kona-light-cl-2 branch 3 times, most recently from 936a5f5 to 102877e Compare January 14, 2026 15:41
- Comments and naming
- Drop `is_derived` from ConsolidateTask
- Combine SafeL2 related EngineProcessingRequests
- Clean up derivation delegate sync status validation
- Comments and method renaming for reconcile_to_safe_head case
- Clean up derivation delegate sync status validation
- Explicitly ignoring DerivationActorRequest for the remainig case
- Merge derivationActor state machine
- break out derivation delegation logic
- DelegationDerivationActor with some hacks
- Wire in ConfiguredDerivationActor
@pcw109550 pcw109550 force-pushed the pcw109550/kona-light-cl-2 branch from 102877e to 205c5d0 Compare January 14, 2026 16:14
/// Derivation delegate provider.
derivation_delegate_provider: DerivationDelegateClient,
/// L1 provider for validating L1 info for derivation delegation.
l1_provider: AlloyChainProvider,
Copy link
Copy Markdown
Contributor Author

@pcw109550 pcw109550 Jan 14, 2026

Choose a reason for hiding this comment

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

Acknowledging that @op-will suggested that DerivationDelegateClient and AlloyChainProvider may be swapped to a trait for unit testing. The previous comment is gone because i attempted to create a separate delegation actor.

@pcw109550 pcw109550 requested a review from theochap January 14, 2026 17:59
Copy link
Copy Markdown
Member

@theochap theochap left a comment

Choose a reason for hiding this comment

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

Small comment: instead of having delegate_actor.rs and delegate_client.rs inside the actors/derivation folder, I'd create a subfolder delegated and then add mod.rs/actor.rs/client.rs there. This makes the structure easier to follow

derivation_actor_request_rx,
self.create_pipeline().await,
)))
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think in the future we should try to make this enum go away. This is certainly out of the scope of this PR but this showcases that our current implementation of the RollupNode is not flexible enough. Ideally swapping out one actor for another one should be easier at the rollup node level.

Good to merge the PR as is. Dealing with the rollup node interface is a workstream in itself

Copy link
Copy Markdown
Member

@theochap theochap left a comment

Choose a reason for hiding this comment

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

Looks good, a few small comments but I think this look much better like that. Thanks for taking our feedback into consideration! Good for you to merge once the comments are addressed

@pcw109550 pcw109550 added this pull request to the merge queue Jan 14, 2026
Merged via the queue into main with commit 6a3e71c Jan 14, 2026
27 of 32 checks passed
@pcw109550 pcw109550 deleted the pcw109550/kona-light-cl-2 branch January 14, 2026 21:45
@pcw109550 pcw109550 linked an issue Jan 15, 2026 that may be closed by this pull request
theochap pushed a commit to ethereum-optimism/optimism that referenced this pull request Jan 15, 2026
~Base branch: op-rs/kona#3242

~May conflict with op-rs/kona#3229,
op-rs/kona#3253

Implement the kona light CL([Design
doc](https://github.com/ethereum-optimism/design-docs/blob/main/protocol/kona-node-light-cl.md)):
- [DerivationActor - Target
Determination](https://github.com/ethereum-optimism/design-docs/blob/main/protocol/kona-node-light-cl.md#derivationactor---target-determination)
- [EngineActor - Fork Choice
Update](https://github.com/ethereum-optimism/design-docs/blob/main/protocol/kona-node-light-cl.md#engineactor---fork-choice-update)

```mermaid
flowchart TB

subgraph A ["Normal Mode (Derivation)"]
  direction TB

  subgraph A0 ["Rollup Node Service"]
    direction TB
    A_Derivation["DerivationActor<br/>(L1->L2 derivation)"]
    A_Engine["EngineActor"]
    A_UnsafeSrc["Unsafe Source<br/>(P2P gossip / Sequencer)"]
  end

  A_L1[(L1 RPC)]
  A_EL[(Execution Layer)]

  A_L1 -->|L1 info| A_Derivation
  A_UnsafeSrc -->|unsafe| A_Engine
  A_Derivation -->|"safe(attr)/finalized"| A_Engine
  A_Engine -->|engine API| A_EL
end

subgraph B ["Light CL Mode"]
  direction TB

  subgraph B0 ["Rollup Node Service"]
    direction TB
    B_DerivationX[["DerivationActor<br/>(NEW: Poll external syncStatus)"]]
    B_Engine["EngineActor"]
    B_UnsafeSrc["Unsafe Source<br/>(P2P gossip / Sequencer)"]
  end

  B_L1[(L1 RPC)]
  B_Ext[(External CL RPC<br/>optimism_syncStatus)]
  B_EL[(Execution Layer)]

  %% Connections
  B_Ext -->|safe/finalized/currentL1| B_DerivationX
  B_L1 -->|canonical L1 check| B_DerivationX
  B_DerivationX -->|"safe(blockInfo)/finalized (validated)"| B_Engine
  B_UnsafeSrc -->|unsafe| B_Engine

  %% Visual indicator for disabled actor
  B_Engine -->|engine API| B_EL
end
```

### Testing

#### Acceptance Tests

Running guidelines detailed at op-rs/kona#3199:

- [x] `TestFollowL2_Safe_Finalized_CurrentL1`
- [x] `TestFollowL2_WithoutCLP2P`
- [ ] `TestFollowL2_ReorgRecovery` (blocked by [kona: Check L2 reorg due
to L1
reorg](#18676))

Injecting CurrentL1 is blocked by [kona: Revise SyncStatus CurrentL1
Selection](#18673)

#### Local Sync Tests

Validated with syncing op-sepolia between kona-node light CL <> sync
tester, successfully finishing the initial EL sync and progress every
safety levels reaching each tip.

#### Devnet Tests

Commit
op-rs/kona@0b36fdd
is baked to
`us-docker.pkg.dev/oplabs-tools-artifacts/dev-images/kona-node:0b36fdd-light-cl`
and deployed at `changwan-0` devnet:
- As a verifier: `changwan-0-kona-geth-f-rpc-3`
[[grafana]](https://optimistic.grafana.net/d/nUSlc3d4k/bedrock-networks?orgId=1&refresh=30s&from=now-1h&to=now&timezone=browser&var-network=changwan-0&var-node=$__all&var-layer=$__all&var-safety=l2_finalized&var-cluster=$__all&var-konaNodes=changwan-0-kona-geth-f-rpc-3)
- As a sequencer: `changwan-0-kona-geth-f-sequencer-3`
[[grafana]](https://optimistic.grafana.net/d/nUSlc3d4k/bedrock-networks?orgId=1&refresh=30s&from=now-1h&to=now&timezone=browser&var-network=changwan-0&var-node=$__all&var-layer=$__all&var-safety=l2_finalized&var-cluster=$__all&var-konaNodes=changwan-0-kona-geth-f-sequencer-3)
    - As a standby | leader

Noticed all {unsafe, safe, finalized} head progression as a kona node
light CL.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[M3] kona-node: Light CL

3 participants