erigontech / erigontech/erigon
BAL commitment fold-ahead never runs at the chain tip
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 455
Description
## Summary
At the chain tip, commitment (the state-root fold) is computed **serially, after a block finishes executing** — about **75 ms per block** in our measurements. The BAL-driven "compute-ahead" optimization, which is meant to fold the root from the block's BAL *while* the block executes, **never runs for tip blocks**. So the parallel-commitment win only helps deep batch sync, not the tip (and not `newPayload`).
The reason is a design rule, not a bug: a block that "owns a reorg changeset" is forced onto the incremental (post-execution) path. Every tip block owns one. This issue asks whether we can capture that reorg changeset *from the BAL fold itself*, so compute-ahead can also run at the tip.
## What we measured (parallel exec, chaintip, `--use-temp-bal`)
Per-block window (~200 ms), warm, split into measured phases:
| part | time | notes |
|---|---|---|
| exec-loop production | ~85 ms | EVM + OCC validation + apply |
| **commitment fold (serial, after exec)** | **~75 ms** | apply loop blocks here waiting for the root |
| commitment that overlapped exec | ~13 ms | small |
We added a per-block counter for *why* compute-ahead did or didn't fire. Result: **every block reported `ownsChangeset`** (the skip reason). Compute-ahead fired **zero** times at the tip.
## Why it happens
- `maybeComputeAhead` (commitment calculator) skips any block where `ownsChangeset(n)` is true, i.e. `n >= perBlockFrom`.
- `perBlockFrom` = the changeset window = `max(startBlockNum, maxBlockNum - maxReorgDepth, frozenBlocks)`.
- At the tip / in `newPayload`, each execution handles **one** block, so `startBlockNum == maxBlockNum == N`, which makes the window start at `N`. So block `N` always owns a changeset.
- Blocks that own a changeset must fold **incrementally at their own boundary** so their per-block branch deltas get recorded (needed to unwind on a reorg). Computing ahead is disallowed because it "would race the exec loop's changeset-accumulator install."
So today it's an either/or: **reorg safety (per-block changeset) OR commitment fold-ahead** — and tip blocks always take the reorg-safety branch.
## The idea / question
The reorg changeset is just the set of branch/state deltas produced by folding the block's changed keys into the trie. **The BAL already contains that full write-set**, and compute-ahead already folds it through the same hash-aware changeset routing the incremental path uses. So the deltas a BAL fold produces should be identical to the incremental ones.
If that's right, we could **capture the reorg changeset from the BAL fold** and let compute-ahead run at the tip too — turning the either/or into "fold ahead from the BAL **and** record the reorg changeset from the same fold." That would remove the ~75 ms serial commitment at the tip (and in `newPayload`).
The real work isn't data availability — it's **ownership/lifecycle**: today the exec loop installs and rotates the per-block changeset accumulator on its own timeline, so a compute-ahead running before/during exec would write into the wrong (or not-yet-installed) accumulator. Making compute-ahead safely own block N's changeset accumulator is the crux.
Open questions:
- Can compute-ahead own/install block N's changeset accumulator without racing the exec loop, using the existing hash-aware routing?
- Does the BAL carry everything the changeset needs (account/storage/code diffs **and** commitment branches), or only part of it?
- Any correctness edges: mid-block step-edge checkpoints, `AlwaysGenerateChangesets`, fork-bounce accumulator disambiguation.
## Setup / caveats
- `integration stage_exec --sync.mode.chaintip --use-temp-bal`, mainnet, parallel executor + BAL-driven commitment, all files on NVMe, warm cache.
- Numbers from lock-free per-block timers we added around the exec loop, apply loop, committer compute, and the block-end root drain; plus a per-block reason counter in `maybeComputeAhead`.
- Instrumentation branch: **`bal_zero`** (`origin/bal_zero` — taratorio's PR #22190 + the temp-BAL feature + these timers).
- This is analysis from instrumentation + code reading, not from a prototype of the change.
Contributor guide
Assessment
This issue has not been assessed yet.