ethereum-optimism / ethereum-optimism/optimism
kona: Revise SyncStatus CurrentL1 Selection
- Dominant language
- Go
- Stars
- 6.5k
- Forks
- 4k
- Avg merge
- 2d 38m
- Merged PRs (30d)
- 164
Description
`current_l1` Not Set to Derivation Cursor
## Summary
The `optimism_syncStatus` RPC endpoint returns `current_l1` that is incorrectly set to the latest L1 head instead of the actual derivation cursor (where the derivation pipeline last idled at).
Impact: Affects observability and external services that rely on knowing where the node is in the derivation process.
## Discovery
While implementing follow mode (Step 1-A), we needed to inject external `current_l1` values when derivation is disabled. During investigation of how `current_l1` is populated, we discovered the current implementation doesn't match the specification.
## The Specification
From `crates/protocol/protocol/src/sync.rs:15-22`:
```rust
pub struct SyncStatus {
/// The current L1 block.
///
/// This is the L1 block that the derivation process is last idled at.
/// This may not be fully derived into L2 data yet.
/// The safe L2 blocks were produced/included fully from the L1 chain up to _but excluding_
/// this L1 block. If the node is synced, this matches the `head_l1`, minus the verifier
/// confirmation distance.
pub current_l1: BlockInfo,
/// The L1 head block ref.
///
/// The head is not guaranteed to build on the other L1 sync status fields,
/// as the node may be in progress of resetting to adapt to a L1 reorg.
pub head_l1: BlockInfo,
// ...
}
```
**Key point**: `current_l1` should be "the L1 block that the derivation process is last idled at" (the derivation cursor), NOT the latest L1 head.
## Current (Incorrect) Implementation
### How It Currently Works
**L1WatcherActor** (`crates/node/service/src/actors/l1_watcher/actor.rs:158-191`):
```rust
L1WatcherQueries::L1State(sender) => {
let current_l1 = *latest_head.borrow(); // ← BUG: Uses L1 head, not derivation cursor
// ...
sender.send(L1State {
current_l1,
head_l1,
// ...
})
}
```
**What `latest_head` is** (`crates/node/service/src/service/node.rs:231-251`):
```rust
let head_stream = BlockStream::new_as_stream(
self.l1_config.engine_provider.clone(),
BlockNumberOrTag::Latest, // ← Polls actual L1 latest head
Duration::from_secs(HEAD_STREAM_POLL_INTERVAL),
);
let l1_watcher = L1WatcherActor::new(
// ...
l1_head_updates_tx.clone(), // ← This becomes latest_head
// ...
head_stream, // ← Fed by Latest tag
finalized_stream,
);
```
**L1WatcherActor updates** (`l1_watcher/actor.rs:110-116`):
```rust
new_head = self.head_stream.next() => {
Some(head_block_info) => {
// Send the head update event to all consumers.
self.latest_head.send_replace(Some(head_block_info)); // ← Latest L1 head
}
}
```
### Where the Real Derivation Cursor Lives
**DerivationPipeline** (`crates/protocol/derive/src/pipeline/core.rs:53-55`):
```rust
impl OriginProvider for DerivationPipeline {
fn origin(&self) -> Option {
self.attributes.origin() // ← This is the actual derivation cursor
}
}
```
**DerivationActor uses it** (`crates/node/service/src/actors/derivation.rs:234, 265`):
```rust
self.pipeline.origin() // ← Returns current L1 origin being processed
```
**But**: This value is **never communicated back** to L1WatcherActor for RPC queries.
### Who This Affects
- **External monitoring services**: Get wrong information about derivation progress
- **Dependent microservices**: May make incorrect decisions based on `current_l1`
- **Operators**: Can't accurately monitor actual derivation lag
- **Metrics/dashboards**: Show incorrect sync status
## Data Flow Diagram
### Current (Incorrect) Flow
```
BlockStream(Latest) ─┐
polls every 4s │
▼
L1WatcherActor.latest_head
│
▼
Used as current_l1 in RPC
│
▼
External Services
(wrong info!)
DerivationPipeline.origin() ─► Used internally only
(actual cursor) ❌ Never exposed!
```
### Correct Flow (Proposed)
```
BlockStream(Latest) ─┐
polls every 4s │
▼
L1WatcherActor.latest_head
│
▼
Used as head_l1 in RPC ✓
DerivationPipeline.origin() ─► DerivationActor
(actual cursor) │
▼
derivation_origin_tx
│
▼
L1WatcherActor.current_l1
│
▼
Used in RPC ✓
│
▼
External Services
(correct info!)
```
## Proposed Fix (General Case)
### Changes Needed
1. **Add new channel for derivation origin**:
```rust
// In RollupNode::start()
let (derivation_origin_tx, derivation_origin_rx) = watch::channel(None);
```
2. **DerivationActor sends origin updates**:
```rust
// In DerivationActor
pub struct DerivationActor {
// ...
derivation_origin_tx: watch::Sender>,
}
// After processing/advancing origin:
if let Some(origin) = self.pipeline.origin() {
self.derivation_origin_tx.send_replace(Some(origin));
}
```
3. **L1WatcherActor uses derivation origin**:
```rust
pub struct L1WatcherActor {
latest_head: watch::Sender>, // For head_l1
derivation_origin: watch::Receiver>, // For current_l1 ✓
// ...
}
L1WatcherQueries::L1State(sender) => {
let current_l1 = *self.derivation_origin.borrow(); // ✓ Use derivation cursor
let head_l1 = /* query L1 provider */;
// ...
}
```
Contributor guide
Assessment
This issue has not been assessed yet.