ethereum-optimism / ethereum-optimism/optimism
op-node: compensate the seal deadline with the measured seal duration instead of a static constant
- 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
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