ethereum-optimism / ethereum-optimism/optimism

op-node: compensate the seal deadline with the measured seal duration instead of a static constant

Open
#22,320 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
6.5k
Forks
4k
Avg merge
2d 38m
Merged PRs (30d)
164

Description

The sequencer starts sealing a fixed interval before the block's timestamp, using a **static** [`sealingDuration`](https://github.com/ethereum-optimism/optimism/blob/80d82a24ea78ef9eb3bbdb1ba4ad698ab3811593/op-node/rollup/sequencing/sequencer.go#L24-L25) (default 50 ms, operator-tunable via [`--sequencer.sealing-duration`](https://github.com/ethereum-optimism/optimism/blob/80d82a24ea78ef9eb3bbdb1ba4ad698ab3811593/op-node/flags/flags.go#L292-L300)):

```go
// startBuildingBlock, after a successful StartBuild
payloadTime := time.Unix(int64(result.Parent.Time+d.rollupCfg.BlockTime), 0)
remainingTime := payloadTime.Sub(now)
if remainingTime < d.sealingDuration {
d.nextAction = now // not enough time to seal, don't wait
} else {
d.nextAction = payloadTime.Add(-d.sealingDuration)
}
```

That constant is a guess about the execution engine, and it is wrong in both directions:

- **Too low** → the seal finishes after the block timestamp. The block goes out late and squeezes the next slot, which is the failure class #19263 was about. Most likely with large blocks, a slow EL, or an external builder in the path.
- **Too high** → we stop building early every single block and give away transaction-inclusion time for nothing. A 50 ms default against a seal that actually takes 5 ms wastes 45 ms of every block.

Every operator is expected to tune a flag for something the node can simply observe.

### What kona already does

`kona-node`'s sequencer actor tracks the measured duration of the last seal and compensates the next deadline with it ([`actor.rs#L493-L504`](https://github.com/ethereum-optimism/optimism/blob/80d82a24ea78ef9eb3bbdb1ba4ad698ab3811593/rust/kona/crates/node/service/src/actors/sequencer/actor.rs#L493-L504), field at [`#L93`](https://github.com/ethereum-optimism/optimism/blob/80d82a24ea78ef9eb3bbdb1ba4ad698ab3811593/rust/kona/crates/node/service/src/actors/sequencer/actor.rs#L93), updated at [`#L479`](https://github.com/ethereum-optimism/optimism/blob/80d82a24ea78ef9eb3bbdb1ba4ad698ab3811593/rust/kona/crates/node/service/src/actors/sequencer/actor.rs#L479)):

```rust
// next block time is last + block_time - time it takes to seal.
let next_block_time = UNIX_EPOCH + Duration::from_secs(next_block_seconds) - self.last_seal_duration;
```

Same design intent as `sealingDuration`, but self-calibrating.

### Why this is cheap here now

After #22241 the sequencer runs `SealBuild` as a direct, synchronous call on its own goroutine, so timing it is a `time.Since` around one call — no event round-trip to correlate. The scheduling site above is the only consumer.

### Design questions to settle before writing it

Not a straight port; kona uses the raw last value, which is the simplest choice and not obviously the right one:

1. **Smoothing.** Raw last-value tracking lets a single outlier (GC pause, engine hiccup) set the budget for the next block, and oscillate. An EWMA, or a decaying max, is steadier. A max-with-decay is the conservative pick: react fast to slowdowns, release slowly.
2. **Bounds.** Clamp it. Never below some floor, never above a fraction of block time — otherwise a pathological seal eats the entire building window and the sequencer stops including transactions.
3. **Relationship to the existing flag.** Keep `--sequencer.sealing-duration` as the initial value and floor, as a hard cap, or deprecate it? It is a documented operator knob, so this needs an explicit answer rather than silently ignoring it.
4. **Cold start.** First block after startup or a reset has no measurement; fall back to the configured value.
5. **Metrics.** Worth emitting the measured seal duration — it is useful on its own for diagnosing late blocks, independent of whether it feeds back into scheduling.

### Validating it

The A/B harness from #19263 applies directly: builder-playground `cook opstack --block-time 1 --batcher-max-channel-duration 50`, host op-node overridden with each binary, identical windows from genesis, and an external ~20 ms RPC poller recording block arrival so the measurement does not depend on the node's own logs. The metric to move here is not max interval but **how close block arrival sits to its timestamp**, plus inclusion time per block. Flashblocks/external-builder configs are the interesting case, since that is where the seal cost is least predictable.

Deferred from the #22241 design as a follow-up. Not urgent: nothing is broken, the static default works, and this is an optimisation with real tuning questions attached.

🤖 *Co-created with Claude (Opus 5)*

Contributor guide

Open the contributing guide

Research direction

Start in op-node/rollup/sequencing/sequencer.go at startBuildingBlock and the synchronous SealBuild call, then review the sealing-duration flag in op-node/flags/flags.go. Resolve the smoothing, bounds, flag, cold-start, and metrics questions before implementation. Done means measured seal duration safely informs deadlines without losing the existing fallback or transaction-inclusion time.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, distributed-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.