Skip to content

feat(builder): ramp a pre-run's gas limit down; fill: false for a ramp-only pre-run - #349

Draft
qu0b wants to merge 7 commits into
masterfrom
qu0b/prerun-gas-ramp-down
Draft

qu0b wants to merge 7 commits into
masterfrom
qu0b/prerun-gas-ramp-down

Conversation

@qu0b

@qu0b qu0b commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Builder changes (plus review fixes) so a released pre-run can be walked back down to a devnet's gas limit and replayed by every client. The motivation is a mainnet-shadowfork snapshot (glamsterdam msf-2) built from the jochemnet stateful release, whose pre-run ends at a 1T gas limit.

1. Gas bump in both directions

gas_limit below the head's limit used to be a silent no-op (bumpGasLimit skipped when head >= target). It now ramps down. Each block carries payload attribute targetGasLimit, the CL's gas limit target since Amsterdam, which geth's testing_buildBlockV1 honours over its miner ceiling (miner/worker.go, IsAmsterdam && targetGasLimit != nil).

  • Before Amsterdam the attribute doesn't exist and the filler keeps climbing toward its 1T ceiling. The ramp therefore fails on the first block that doesn't fall, with a clear error, instead of after gas_bump_max_blocks.
  • Ramping up is unchanged. No new attribute is sent, so existing pre-runs record identical bundles.

2. fill: false: a pre-run that only ramps the gas limit

A pre-run is gas bump → funding block → fill (EEST setup tests), every block recorded into the bundle. With fill: false (inherited like the other fields) the fill is skipped, along with the EEST checkout. The pre-run is then just the gas ramp (plus funding, if any), recorded as a bundle like any other. tests stays required whenever the fill runs. An empty tests couldn't express this, since it inherits the shared default.

After a ramp down, targetGasLimit stays set for every later block this client builds. Otherwise the funding block (built after the bump on the non-predeploy path) would climb back toward the 1T ceiling.

Slot numbers

An Amsterdam block's slotNumber now continues the parent's. Every existing flow starts from a pre-Amsterdam snapshot, where this equals the old per-client counter, so the bundles are unchanged. A pre-run on top of an Amsterdam chain (e.g. a replayed release) would otherwise restart at slot 1.

Verified

  • gas_ramp_test.go: a fake filler that applies geth's CalcGasLimit.
    • a ramp down lands exactly on target, every block carries targetGasLimit, and slots continue from the head;
    • a pre-Amsterdam ramp down is refused on block 1;
    • a ramp up sends no new attribute;
    • at target, nothing is built;
    • a funding block built after a ramp down keeps the target;
    • fill: false validates without tests, overrides a shared tests, and is inherited from the shared config.
  • Real geth v1.17.7, Amsterdam chain, --miner.gaslimit=1T as the filler runs: from a 2,000,000,000 parent, testing_buildBlockV1 gives 2,001,953,124 without targetGasLimit and 1,998,046,876 with targetGasLimit=1G.
  • go test ./pkg/config ./pkg/builder passes. golangci-lint v2.3.0 (go 1.24) reports nothing in the changed files.

3. base_bundle: continue a recorded chain

An in-place (schelk) target restores the baseline before it boots, so a second target can't continue where the first one stopped on the scratch. base_bundle (an absolute path to a .request file or a pre_run_bundle dir) is replayed onto the restored snapshot first:

  • a fill target bumps or fills from its head; its own bundle holds only the blocks it built, and isn't re-recorded;
  • a replay target then replays replay_from (base, then tail), so the other clients need no concatenated copy of the release.

4. Replays stream

A release's pre-run bundle is 49.7 GB (jochemnet stateful e269bb44: 82,186 lines). readRequestLines did os.ReadFile and then split it, which needs several times that in RAM. Replays now read line by line. Checked against that real bundle with a VALID-everything engine stub: all 82,186 lines replayed with max RSS ~235 MB.

5. Every pre-run's filler gets the flush window; a killed filler fails the target

The 15-minute stop window was granted only to schelk_options.promote targets, and a SIGKILLed filler still left the target OK. A pre-run's datadir is always its product — promoted, exported, or read as output_dir — so both now apply to every target. Found on the release-snapshot e2e: geth v1.17.7 wrote its ramp journal in 32 ms, then sat in "Waiting background history index initer to exit" past 30 s and was killed with pebble unclosed, and the build reported OK geth-ramp. The bundle is written before the stop and is unaffected.

"Stopped cleanly" is exit 0, 130 or 143: nethermind (dotnet) and besu (java) return 128 + the signal our stop sends after a complete shutdown (All DBs closed … Nethermind is shut down, then 130, measured). 137 (SIGKILL after the grace period), an OOM kill or any other code fails the target.

Example: walk a release down to 200M, then replay it on another client

builder:
  pre_runs:
    config: { fork: amsterdam, datadir_method: schelk }
    targets:
      - name: geth-ramp
        filler_client: geth
        base_bundle: /work/release/pre_run_bundle     # the release's pre-run
        fill: false
        gas_limit: 200000000
        gas_bump_max_blocks: 10000
        bundle_dir: /work/tail                        # off the schelk scratch
        filler_extra_args: [--cache.gc=0]             # flush geth's write buffer every block
        ...
      - name: reth-replay
        filler_client: reth
        base_bundle: /work/release/pre_run_bundle
        replay_from: /work/tail/pre_run_bundle
        ...

Used by ethpandaops/benchmarkoor-tests#44 (configs/snapshots/jochemnet/v1/builder.yaml).

…ut tests

A pre-run's gas_limit could only be reached from below: bumpGasLimit skipped
whenever the head was already at or above it. Below the head it now ramps down,
sending payload attribute targetGasLimit (the CL's gas limit target since
Amsterdam) on each block. Before Amsterdam there is no such attribute and the
filler keeps rising toward its miner ceiling, so a ramp down fails on the first
block that does not fall rather than after gas_bump_max_blocks.

A pre-run with no tests is now valid and skips the EEST checkout and the fill: a
pure gas ramp (plus funding, if any), recorded as a bundle like any other
pre-run. Together these let a released pre-run's 1T head be walked down to a
devnet's limit and replayed by every client.

Amsterdam slot numbers continue the parent's when it has one. Every existing
flow starts from a pre-Amsterdam snapshot, where that equals the old per-client
counter, so their bundles are unchanged; a pre-run on top of an Amsterdam chain
no longer restarts at slot 1.
@qu0b
qu0b requested a review from skylenet October 8, 2026 13:49
redpandabot[bot]

This comment was marked as outdated.

…fill: false (review)

- The non-predeploy pre-run builds its funding block after the bump. Clearing
  targetGasLimit when the ramp returned sent that block back toward the
  filler's 1T miner ceiling, ~1/1024 above the target. A ramp down now leaves
  the target set for every later block this client builds.
- An empty tests list cannot opt out of the fill: ResolveTarget re-inherits
  config.tests. A pre-run that only ramps the gas limit is now explicit,
  fill: false, hoisted like the other fields; tests stays required otherwise.
@qu0b qu0b changed the title feat(builder): ramp a pre-run's gas limit down; allow a pre-run without tests feat(builder): ramp a pre-run's gas limit down; fill: false for a ramp-only pre-run Oct 8, 2026
redpandabot[bot]

This comment was marked as outdated.

qu0b added 2 commits October 8, 2026 16:14
…hain

An in-place (schelk) target restores the baseline before it boots, so a second
target cannot continue the first one's scratch. base_bundle replays a recorded
bundle (e.g. a release's pre-run) before the gas bump; the target's own bundle
holds only the blocks it built, replayed after the base.
@redpandabot

This comment has been minimized.

A release's pre-run bundle is 49.7 GB, which readRequestLines held in memory
several times over. Replays now read line by line. A replay target can take
base_bundle as well (base, then replay_from), so the other clients continue a
release + ramp without a concatenated copy of the release.

@redpandabot redpandabot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

This PR makes the builder's gas bump bidirectional (via the Amsterdam targetGasLimit payload attribute), adds fill: false for a ramp-only pre-run, streams replay bundles instead of loading them whole, and lets replay targets take a base_bundle. The streaming and fill: false work look sound and the prior two findings (tests required, fill undocumented) are fixed; I found one functional gap in the ramp-down check for predeploy targets and one documentation omission.

Issues

  • 🟡 pkg/builder/engine.go:673 — ramp-down aborts on a predeploy target's first pre-fork bump block — see the thread on that line
  • 🟢 docs/configuration.md — new base_bundle option is undocumented in the configuration reference — base_bundle is a new per-target option (pkg/config/pre_runs.go:188) used by both the fill and replay paths, but the builder.pre_runs.targets[] table in docs/configuration.md still lists only replay_from; only config.example.yaml mentions it. A row describing the pre-run it continues and the replay order (base, then the target's own bundle / replay_from) belongs next to replay_from.

Reviewed @ 2a66455c
"The most dangerous phrase in the language is 'we've always done it this way.'" — Grace Hopper

Comment thread pkg/builder/engine.go
return built, fmt.Errorf("building gas-bump block %d: %w", built+1, buildErr)
}

if down && built == 0 && gl >= gasLimit {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 ramp-down aborts on a predeploy target's first pre-fork bump block

With predeploy, the funding/deploy blocks are built pre-fork and the gas bump is what crosses into Amsterdam (forkAt returns preFork until activationTS). If the head is above gas_limit and the first bump block is still pre-fork, buildBlock sends no targetGasLimit, the limit rises, and this check returns an error on block 1 — even though the later Amsterdam blocks would ramp it down. The pre-fork bump is the expected case when activation is more than one block after the deploy block, so this combination cannot use ramp-down.

…filler fails the target

The 15-minute stop window was granted only to schelk_options.promote targets,
and the target still passed when the filler was SIGKILLed. A pre-run's datadir
is always its product (promoted, exported or read as output_dir): on the
release-snapshot e2e, geth v1.17.7 wrote its ramp journal in 32 ms and was then
killed at 30 s while waiting on its history indexer, with pebble unclosed, and
the target was reported OK.
@redpandabot

This comment has been minimized.

@redpandabot

This comment has been minimized.

…ed cleanly

nethermind (dotnet) returns 130 and besu (java) 143 after the signal a
container stop sends, having logged a complete shutdown ("All DBs closed").
Reading graceful as exit 0 logged every nethermind pre-run as not stopped
cleanly, and with the previous commit would have failed it.
@redpandabot

redpandabot Bot commented Oct 8, 2026

Copy link
Copy Markdown

Summary

This PR adds a gas-limit ramp-down (via the Amsterdam targetGasLimit payload attribute), a fill: false mode for ramp-only pre-runs, streaming bundle replay, and base_bundle continuation, plus a stricter clean-stop requirement for pre-run fillers. The earlier findings about the hoisted tests default and missing fill docs are fixed, and the ramp logic is otherwise sound (targetGasLimit/slotNumber are correctly sent and geth v1.17.7 honours them). Two issues remain: the predeploy ramp-down abort and the undocumented base_bundle option.

Issues

  • 🔴 pkg/builder/engine.go:673 — ramp-down aborts on a predeploy target's first pre-fork bump block — see the thread on that line
  • 🟢 docs/configuration.md — new base_bundle option missing from the pre_runs options table — base_bundle is a new per-target field (pkg/config/pre_runs.go:188) with non-trivial semantics (the produced bundle holds only the new blocks and must be replayed after the base), but it appears only in the config.example.yaml comment — the builder.pre_runs options table documents every other field (replay_from, fill, bundle_dir, …), so add a row there.

Reviewed @ fb3f99db
"The most dangerous phrase in the language is 'we've always done it this way.'" — Grace Hopper

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant