Engineering · Companion to the bake-off writeup
Published Jul 21, 2026 · Updated Aug 1, 2026
The Bake-off Harness — Function-Level Engineering Reference
This reference covers every test/bakeoff/ script, function, flag, and output file. Read it alongside the scripts: function and flag names form the stable contract, while surrounding code changes. Companion writeups cover measurements, orchestration, and incidents.
Results and methodology are documented in CLIENT_BAKEOFF_RESULTS.md (the source of truth for every number) and CLIENT_BAKEOFF_BLOG.md (the narrative writeup).
In 30 seconds
- 8 scripts, 1 manifest: six executable drivers, a sourced library, and a CI guard; candidates.tsv lists the pairs.
- 3 launch paths share run_candidate.sh: a fixed manifest, a rotating execution-layer (EL) anchor, or an asynchronous rerun queue. Consensus-layer (CL) clients cycle against the synced anchor.
- 3 watchdogs detect trouble: crash-loop (always on), anchor-drift (anchor mode only), stall (opt-in). Only stall restarts services, within a cap; the others only mark state and log.
- env.txt and samples.jsonl feed summarize.sh. Operators tail advisor-alerts.jsonl as a separate structured alert channel.
1. Layout
The harness has about 1,550 lines of shell across eight scripts — run_candidate.sh (~480) and lib.sh (~380) carry most of the logic.
Script layout and shared logging
test/bakeoff/
├── run_bakeoff.sh # sequential multi-candidate orchestrator + resume guard
├── run_candidate.sh # single-candidate driver: reset → install → caps → sample-to-verdict
├── run_anchor_rotation.sh # multi-EL anchor-rotation driver (establish EL once, sweep CLs against it)
├── run_queue.sh # async TSV rerun-queue drainer (waits for preconditions, then calls run_candidate.sh)
├── apply_resource_caps.sh # systemd CPUQuota/MemoryMax/IOWeight apply|clear
├── lib.sh # shared probe/sample/alert/config-gate library (sourced, never executed)
├── summarize.sh # aggregates artifacts/ into summary.csv, process-summary.csv, report.md, a results skeleton
├── candidates.tsv # the manifest: <execution>\t<consensus> pairs to run
├── rerun_queue.tsv.example # queue-file format reference for run_queue.sh
└── test_data_dirs_sync.sh # CI guard: BAKEOFF_DATA_DIRS must match purge_ethereum_data.shThe executable drivers (run_bakeoff.sh, run_candidate.sh, run_anchor_rotation.sh, run_queue.sh, apply_resource_caps.sh, summarize.sh) each set set -Eeuo pipefail and source lib/common_functions.sh for logging (log_info/log_warn/log_error). lib.sh is a sourced library, never executed directly. It inherits strict mode from its caller and doesn't source common_functions.sh itself. Its log_* calls resolve at runtime because its logging callers (run_candidate.sh, run_anchor_rotation.sh, run_queue.sh) source common_functions.sh first. The CI guard test_data_dirs_sync.sh only reads BAKEOFF_DATA_DIRS and never reaches a log_* call.
Data flow at a glance
2. run_bakeoff.sh — the sequential orchestrator
Runs selected client pairs sequentially, continuing after failures. Usage: run_bakeoff.sh --stage=triage|full [--only=el__cl,el__cl,...] [--force]
Run sequence:
- Stage-dependent windows.
--stage=triagesets a 5400s (90 min) sync window with 120s sampling (ETH2QS_BAKEOFF_SYNC_WINDOW_SECONDS,ETH2QS_BAKEOFF_SAMPLE_INTERVAL_SECONDS).--stage=fullsets a 259200s (72h) window with 600s sampling. Environment variables override both defaults, supplied with${VAR:-default}. - Config swap with restore-on-exit. It backs up the current
config/user_config.envtoconfig/user_config.env.bakeoff-backup, installsconfig/bakeoff.envin its place, and registerstrap restore_cfg EXITso the operator's real config is always restored, even after Ctrl-C or a mid-run crash. - Candidate selection from
candidates.tsv. The manifest is<execution>\t<consensus>pairs;run_bakeoff.shturns each row into anel__clpair id withawk -F'\ ' 'NF>=2{print $1"__"$2}'.--only=a__b,c__dexplicitly overrides the manifest.--stage=fullwithout--onlyauto-selects only candidates whose triageenv.txtalready recordedinstall_exit_code=0. This resume guard retries only triage successes. Running--stage=fulltwice is safe: selection comes from disk, without internal orchestrator state.--stage=triagewith no--onlyruns every manifest row.
- Sequential dispatch, tolerant of failure. For each selected pair it calls
run_candidate.sh "$execution" "$consensus"withset +earound the call, so one candidate's non-zero exit does not abort the sweep; the exit code is appended toartifacts/<run-id>/orchestrator-<stage>.log. - Always calls
summarize.shat the end (|| log_warn, non-fatal if it errors).
Artifact root: artifacts/${ETH2QS_BAKEOFF_RUN_ID:-client-bakeoff-2026-06-22}/. All scripts share this environment variable. Start a fresh campaign with a new ETH2QS_BAKEOFF_RUN_ID.
3. run_candidate.sh — the single-candidate state machine
Every results-table candidate row comes from one invocation of this script. Usage: run_candidate.sh <execution> <consensus>. It requires ETH2QS_BAKEOFF_CONFIRMED=yes (log_error … exit 2). This manual safety gate protects against purging data directories and stopping/disabling services.
3.1 Modes — full / establish / anchor, selected by env vars
The script has three modes, selected by env vars:
Trigger: neither var set
stop+disable+purge both EL and CL, install both, sync both
Trigger: ETH2QS_BAKEOFF_KEEP_EL=yes
Same as full. Teardown purges only the CL data directory (datadir), leaving the EL running and synced as the next anchor.
Trigger: ETH2QS_BAKEOFF_ANCHOR_EL=<el>
Stop, disable, purge, install, and cycle only the CL. The EL must already be active and synced.
Anchor mode checks these preconditions before changing anything:
$executionmust equal$ETH2QS_BAKEOFF_ANCHOR_EL; mismatch →exit 3.eth1.servicemust passsystemctl is-active.- Engine API port 8551 must return an HTTP-shaped code (
^[1-5][0-9]{2}$). Authentication-required 401 counts as responding. - A bounded 5-retry/2s-sleep loop must see
eth_syncingcaught up before the CL sweep starts:result == false, or hex-decodedcurrentBlock>=hex-decodedhighestBlock, withhighestBlock != "0x0". This comparison is numeric;bakeoff_is_execution_synced(§5.4) requires exact hex-string equality.
3.2 Resume guard — .done marker + FORCE
If $out/.done exists and ETH2QS_BAKEOFF_FORCE isn't yes, the candidate is skipped entirely (exit 0). Setting ETH2QS_BAKEOFF_FORCE=yes also clears .anchor-poisoned, .crash-looped and .done before rerunning. A stale poison marker would bypass the anchor watchdog and falsely finalize anchor_synced=no for a clean rerun.
3.3 Pre-install sequence — 7 steps, stop to disk-floor guard
- Export
CI_E2E=true(unless the caller set it). The bake-off runsphase2without a priorrun_1, sorun_2.sh'srun_1-dependent post-install security validation — which expects active UFW andsecurity_monitor— would exit 1 and abort every install.CI_E2Eis the codebase's existing switch for arun_1-lessphase2; it skips UFW setup but doesn't change the installed binary, config, datadir, or sync footprint. - Stop+disable the relevant services (
eth1.service cl.service validator.service, or justcl.service validator.servicein anchor mode) —2>/dev/null || true, tolerant of units that don't exist yet. - Purge datadirs via
./scripts/eth2qs.sh clean-data—--dry-runfirst (logged, non-fatal), then--confirm(must succeed). Anchor mode passes--scope=consensusso the EL anchor is never touched.< /dev/nullon everyclean-dataand install call preventsSIGTTINwhen detached (backgrounded/tmux). The kernel stops a background process group that reads its controlling terminal (TTY). The script cannot catch this as an error. bakeoff_snapshot_disk "$out/disk-before.tsv"— baseline footprint.- Disk-floor guard, checked via
df -B1 --output=avail /: anchor mode requiresETH2QS_BAKEOFF_MIN_DISK_BYTES:-429496729600(400 GiB — CL-only, since the EL already occupies disk), full mode requires:-1717986918400(1.6 TiB — full EL+CL). Below the floor, the script aborts withexit 3before starting an install that would fail hours later fromENOSPC. ./scripts/eth2qs.sh plan|doctor|stats --jsonsnapshots, best-effort (|| true).- Opportunistic
GITHUB_TOKENexport fromgh auth tokenif neitherGITHUB_TOKENnorGH_TOKENis already set — avoids the 60/hr unauthenticated GitHub API rate limit during client-version lookups inside the install scripts.
3.4 Install — eth2qs.sh phase2 + post-install snapshots
timeout "$install_timeout" /usr/bin/time -v -o "$out/install-time.txt" \
./scripts/eth2qs.sh phase2 --execution="$execution" --consensus="$consensus" --mev=none \
</dev/null > "$out/install.log" 2>&1(Anchor mode omits --execution= since only the CL installs.) install_timeout defaults to 90m (ETH2QS_BAKEOFF_INSTALL_TIMEOUT). /usr/bin/time -v is what summarize.sh later parses for install wall-clock (see §8). The exit code is captured with set +e/set -e bracketing and written to env.txt as install_exit_code=.
After install: apply_resource_caps.sh apply (§4), then snapshots (doctor, stats, monitor export, debug --service eth1, debug --service cl, systemctl status eth1 cl validator), then the config-optimality gate, bakeoff_check_config_optimal (§5.6). This non-blocking gate stamps config_optimal=yes|no into env.txt and never aborts the run.
3.5 Observation window — sampling loop + 3 watchdogs
Skipped entirely if install_rc != 0. Otherwise a while loop runs until $(date +%s) >= end_at (end_at = now + window), sleeping interval seconds between iterations. Before the loop starts, the crash-loop watchdog (below) records a baseline systemctl show <unit> -p NRestarts --value for both eth1.service and cl.service, then checks only the units under test — both in full/establish mode, just cl.service in anchor mode (the pre-existing anchor eth1.service is out of scope for that row). Each iteration:
Runs only if the client pair installed — otherwise the watch is skipped.
watchdogs on this run
The run ends exactly one of four ways ↓
bakeoff_write_sample "$out" "$REPO_ROOT"— the sampling primitive (§5.5).bakeoff_services_alivecheck — setscrashed=yesif either service unit is no longer active (this becomesservice_crash_observedinenv.txt, a hard fail signal downstream).- Crash-loop watchdog (always-on, not opt-in). It detects flapping: repeated exits and restarts under systemd's
Restart=. A config error can cause immediate exits and respawns every few seconds. The stall watchdog detects no progress. Each iteration re-readsNRestartsfor every unit under test and computes the delta from the pre-loop baseline. If any unit's delta exceedsETH2QS_BAKEOFF_MAX_RESTARTS(default 20: well above healthy behavior, well below a real crash loop), the row is invalidated immediately: ittouches$out/.crash-looped; writescrash_loop_detected=yes,crash_loop_unit=, andcrash_loop_restarts=(the delta) toenv.txt; logs an error; fires abakeoff_advisor_alert(error, kindcrash_loop, §5.2); and breaks the observation loop rather than waiting out the rest of the window. It never restarts services. Systemd already does that; the watchdog detects the loop and exits. Motivating incident: lodestar'scl.servicecrash-looped onUnknown argument: chain(exit 1, ~5s per cycle) and was respawned 20,892 times over ~2 days. Before this watchdog existed, the harness kept sampling to the full 72h window on generic “service is no longer active” warnings alone and needed human intervention to produce anything. - Anchor watchdog (anchor mode only, and only while
.anchor-poisoneddoesn't already exist): detects if the anchor EL silently dropped out of sync (service down, oreth_syncingno longer reporting caught-up). “Caught-up” allows the anchor to trail the network head by up toETH2QS_BAKEOFF_ANCHOR_LAG_BLOCKS(default 128). Some ELs return aneth_syncingobject at tip withhighestBlockset to the network head. While a CL warms up, nothing drives forkchoice, so the anchor can legitimately trail a few blocks. Demanding an exact catch-up there poisoned healthy rows. A real re-snap dropscurrentBlockto ~0, so it still trips far inside the tolerance. Two consecutive misses (anchor_miss_streak -ge 2) touches.anchor-poisonedand logs an error. It never restarts or kills anything: killing the shared EL would invalidate every remaining CL candidate. - Stall watchdog (opt-in via
ETH2QS_BAKEOFF_STALL_RESTART=yes, inert otherwise): tracksno_progress_streak— the CL'shead_slot(or, if absent, async_distance-derived proxy) in anchor mode, or the EL'scurrentBlock(hex-decoded) otherwise.stall_samples(default 10) consecutive non-advancing samples triggerssudo systemctl restarton the stalled unit, up tostall_max_restarts(default 3) times; exceeding that touches.stalled, writesstall_restarts=/stall_failed=yestoenv.txt, and breaks out of the sampling window early. The row is invalid past that point, but teardown still runs cleanly rather than hanging forever on a genuinely dead node. - Synced-streak check:
bakeoff_is_synced(§5.4) must return true on two consecutive samples (synced_streak -ge 2) before the row is accepted as synced. One true reading can reflect a race between EL and CL heads. On that streak, it snapshotsdisk-synced.tsv, writessynced_at_utc=/fully_synced=yes, and breaks out of the sampling window early, before the 72h cap.
After any loop exit (crash-loop, exhausted stall watchdog, synced streak, or timeout), a final bakeoff_write_sample call captures the end state, then env.txt gets fully_synced=no if nothing set it, service_crash_observed=, and (anchor mode only) anchor_synced=yes|no depending on whether .anchor-poisoned exists. A window_capped_unsynced warn-level advisor alert (§5.2) fires only when install succeeded, the window expired without fully_synced=yes, and neither the crash-loop nor the stall watchdog already alerted this same row. This is the third and last of the three give-up paths, after crash-loop and stall, ensuring at most one give-up alert per dead candidate.
3.6 Teardown — journalctl tails, findings.md, exit code
Teardown captures evidence and cleans up regardless of outcome:
- Capture
journalctltails (-u eth1 -n 700,-u cl -n 700,-u validator -n 300) and./scripts/eth2qs.sh repairpreview. Seedfindings.mdwith the install/crash verdict (Result: TBD-by-reviewer) for human completion. bakeoff_snapshot_disk "$out/disk-final.tsv"records the pre-cleanup footprint, synced or capped.summarize.shuses it whendisk-synced.tsvis absent.- Run
apply_resource_caps.sh clear, thenclean-datawith the pre-install scope. Recorddisk-after-cleanup.tsvandended_at_utc; runtouch .done.
Normally, teardown ends with exit "$install_rc". A crash-loop instead forces exit 4, preserving all cleanup and artifacts. Even install_exit_code=0 can precede later flapping; exit 0 would make run_bakeoff.sh/run_queue.sh record an invalid measurement as clean. Exit 4 differs from 2 (operator-confirm gate) and 3 (anchor precondition/disk-floor failure).
4. apply_resource_caps.sh — systemd runtime caps
Runtime caps limit the node stack to roughly 8 cores / 36G total, protecting other work on the shared host. Usage: apply_resource_caps.sh <apply|clear>.
cap_unit <unit> <prop...>guards onsystemctl cat "$unit"existing (skips with a warning if the unit isn't defined yet) and applies each property withsudo systemctl set-property --runtime <unit> <prop>—--runtimemeans these are transient, gone on reboot, never written into the unit file.apply:eth1.servicegetsCPUQuota=600% MemoryMax=24G MemoryHigh=22G IOWeight=50;cl.servicegetsCPUQuota=200% MemoryMax=12G MemoryHigh=11G IOWeight=50.clear: resets both units'CPUQuota/MemoryMax/MemoryHigh/IOWeightback to infinity/infinity/infinity/100. It avoidssystemctl revert, which can strip custom unit overrides/definitions, beyond the runtime properties set here. In anchor mode,clearexplicitly skips revertingeth1.service(ETH2QS_BAKEOFF_ANCHOR_ELset) so the anchor's caps stay consistent for the rest of the CL sweep; onlycl.servicereverts.
5. lib.sh — the shared probe/sample/gate library
This library shares probes, sampling, and configuration checks. Sourced by run_candidate.sh, run_anchor_rotation.sh, run_queue.sh (§7), and the CI guard test_data_dirs_sync.sh. Never executed directly.
5.1 BAKEOFF_DATA_DIRS — the canonical datadir list
BAKEOFF_DATA_DIRS — the canonical datadir listThe canonical list of every client datadir the harness knows about ($HOME/.ethereum, $HOME/.local/share/nethermind, … through $HOME/ethgas). This array must stay in sync with the DATA_DIRS arrays in install/utils/purge_ethereum_data.sh — test_data_dirs_sync.sh is a CI guard that fails the build if they drift (it diffs the two lists rather than sourcing purge_ethereum_data.sh, since sourcing it would invoke main and run the actual purge).
5.2 bakeoff_advisor_alert() — the structured alert channel
bakeoff_advisor_alert() — the structured alert channelOperators and supervising AI can tail -f one alert channel throughout a multi-hour run. It collects crash-loop, anchor-poison, stall, install-failure, window-capped-without-sync, and malformed/timed-out queue alerts.
- Args:
severity(warn|error),candidate(theel__clpair, or-when not candidate-scoped),kind(a short machine token —crash_loop,anchor_poisoned,stall,install_failed,window_capped_unsynced,queue_malformed_row,queue_precondition_timeout,queue_candidate_failed),detail(free text — may contain quotes, backslashes, or newlines). - Side effects, in order: first, a single-line
ADVISOR_ALERT severity=... kind=... candidate=... detail=...marker to stdout — printed before the JSONL write, so it survives even if the write below fails. Then, one JSON object (ts_utc,severity,run_id,candidate,kind,detail) appended to${ETH2QS_BAKEOFF_ALERT_LOG:-./advisor-alerts.jsonl}. Bothrun_candidate.shandrun_queue.shexportETH2QS_BAKEOFF_ALERT_LOGto the same$artifact_root/advisor-alerts.jsonl(keyed byETH2QS_BAKEOFF_RUN_ID), so one file aggregates alerts for the whole run — including rows driven asynchronously through the queue. - Never aborts the caller: always returns 0, even under
set -Eeuo pipefail, even when the target directory is unwritable, even whenjqis missing (falls back to manual shell string-escaping — backslashes escaped before quotes, since escaping quotes first would re-escape the backslash the quote-escape step just inserted), even whendetailitself contains characters that would otherwise corrupt a hand-rolled JSON string.
5.3 Snapshot/probe primitives — 5 functions
bakeoff_snapshot_disk <outfile>— for every path inBAKEOFF_DATA_DIRSthat exists, writespath\tbytes\thumanusingdu -sb/du -sh, then a finalfilesystem:/row fromdf -B1 /. Theducalls are2>/dev/nullwith|| trueafter theawk—ducan exit non-zero on a file that vanishes mid-walk (a live datadir churning during snapshot), and|| truekeeps the number that was printed instead of lettingpipefailpropagate a fatal exit into an unguarded caller.bakeoff_probe_execution_sync—curl -sS --max-time 5tohttp://127.0.0.1:8545with aneth_syncingJSON-RPC payload; on a curl connection failure, prints{"error":"execution_rpc_unavailable"}instead of failing. There's no--failflag, so an HTTP-error response with a non-JSON body (e.g. an HTML error page) is not caught by this fallback — the actual guard against malformed output downstream isbakeoff_write_sample'sfromjson? // {raw: ...}fallback (§5.5).bakeoff_probe_beacon_sync— tries four ports in order (3500,5051,5052,9596— the different consensus clients' REST API defaults) against/eth/v1/node/syncing, returns the first non-empty body; falls back to{"error":"beacon_rest_unavailable"}.bakeoff_snapshot_processes—ps -eo pid=,comm=,%cpu=,%mem=,rss=,vsz=,etime=,args=piped to anawkregex over known client names; the pattern matches against the whole formattedpsrow (commandargscolumns together), not just thecommfield. Processes with matching arguments are included, even if their binary names differ. Matching lines are converted to a JSON array viajq -R -s 'split("\n")[:-1]'.bakeoff_services_alive—systemctl is-active --quiet eth1.service && systemctl is-active --quiet cl.service.
5.4 bakeoff_is_execution_synced / bakeoff_is_synced
bakeoff_is_execution_synced / bakeoff_is_syncedbakeoff_is_execution_synced checks the execution client alone. eth_syncing must return boolean false, or a progress object with currentBlock == highestBlock and highestBlock != "0x0". The != "0x0" guard rejects pre-sync zero state: an EL that hasn't started downloading would otherwise look trivially caught up.
Between anchor-mode candidates, the CL is stopped and its datadir purged. A beacon-inclusive check cannot pass. This separate function lets callers check the preserved EL alone; run_queue.sh's anchor precondition (§7) uses it directly.
bakeoff_is_synced returns 0 only when both layers report caught-up:
- Beacon:
.datapresent,sync_distance <= 4,is_optimistic == false,el_offline == false. - Execution: delegates to
bakeoff_is_execution_syncedabove.
5.5 bakeoff_write_sample() — the per-tick sampling routine
bakeoff_write_sample() — the per-tick sampling routineEach tick appends one JSON object to samples.jsonl:
- Write disk, execution-sync, beacon-sync, and process snapshots into
tmp/. Add./scripts/eth2qs.sh doctor --jsonandstats --json, each best-effort withtimeout 30. - Determine
alive="up"/"down"throughbakeoff_services_alive. - Assemble with
jq -cn --rawfile. Malformed sub-outputs fall back to raw-string fields throughfromjson? // {raw: ...}. Exception:processesexpects an array, so(($processes | fromjson?) // [])produces[], rather than a{raw: ...}object.
An assembly-wide jq failure logs log_warn without propagating. One bad sample cannot kill the observation loop.
5.6 bakeoff_check_config_optimal() — the config-optimality gate
bakeoff_check_config_optimal() — the config-optimality gateThis gate checks disk-efficient configuration before a footprint enters the ranked results. It stamps each row with a verdict, always returns 0, and never blocks a run.
Mechanism:
- Pull the configured
ExecStartline from each systemd unit:systemctl cat eth1.service | grep -i ExecStart(same forcl.service).systemctl catreads the unit file text (what the service is configured to run, including drop-ins) — not the live process's actual command line — so this reflects the configuration, not a runtime introspection of the running process. - Extract only the first referenced config path using
grep -oP | head -1, even with multiple flags. Onegrep -oPalternation matches three styles:--config-file=/--config-file,--rcConfig=/--rcConfig(lodestar), and--config=/--config(erigon). As the source comment explains, PCRE lookbehind checks preceding text at a specific position.(?<=--config[= ])requires=or a space immediately after--config. In--config-file=x, that character is-, so this alternative cannot falsely match. - If the extracted path exists as a file, its contents are appended to the “combined” haystack alongside the
ExecStartline — so tokens can live in either the command line or the config file. _has_token <label> <pattern> <haystack>runsecho "$haystack" | grep -qP -- "$pattern"— the haystack is theExecStartline plus any referenced config file's contents, piped in as text, not a file path. The--before the pattern is required because several patterns start with a dash (e.g.--prune-storage) and without--those would be parsed as agrepoption and silently swallowed by the2>/dev/nullas a false “miss.”- Per-client token table (kept current with what the install scripts actually emit):
Client Token checked geth history.chainpresentnethermind AncientBodiesBarrierpresent andPivotNumberextracted and> 0(a zero pivot means snap sync is inert) — the pivot regex handles Nethermind's quoted-JSON form ("PivotNumber": 15900000,) with an optional closing quote between the key and the separatorerigon prune.modeset tofull. Matches both YAML-colon (prune.mode: "full") and=separator styles, quotes optionalreth both --prune.bodies.pre-mergeand--prune.receipts.pre-mergepresentbesu history-expiry-prune=truenimbus_eth1 prune = trueethrex no history-prune lever exists, so “optimal” is redefined as --syncmode snapbeing presentprysm beacon-db-pruning(flag or config-key form)lighthouse checkpoint-sync-urlpresent (default pruning behavior is already fine)teku data-storage-modeset tominimalorprune(leading letter either case)nimbus (CL) history = prunein persistent config.trustedNodeSync/--trusted-node-urlonly appear in the one-shot bootstrap subcommand, before service startup. Neither appears in persistentExecStartornimbus.toml, so this running-config gate cannot observe them and excludes them.lodestar pruneHistory=true(orchain.pruneHistory)grandine --prune-storagepresent - Any client not in the table falls through to
unchecked(counted as found, not a miss). Adding clients without token entries cannot silently fail existing runs. - Writes
config_optimal=yes|noandconfig_optimal_detail=found=...;missed=...into$out_dir/env.txt. On a miss, alsotouch $out_dir/.config-not-optimalandlog_error.
This is the mechanism behind the “Superseded — non-optimal config” and “optimal config only” table split in summarize.sh's generated results skeleton (§8) — a row only counts toward the ranked results if every applicable token for its EL and CL was found.
6. run_anchor_rotation.sh — multi-EL anchor rotation
Reuse each synced EL anchor across CLs, avoiding a fresh EL sync for every pair. Usage: run_anchor_rotation.sh <anchors_csv> [<cls_csv>]. CLs default to lighthouse,teku,nimbus,lodestar,grandine.
Per anchor in <anchors_csv>:
- Establish: unset
ETH2QS_BAKEOFF_ANCHOR_EL, setETH2QS_BAKEOFF_KEEP_EL=yes, and callrun_candidate.sh <anchor> prysm. This wipes the prior EL, installs fresh, and syncs to tip, preserving the EL datadir at teardown. The rotation driver defaultsETH2QS_BAKEOFF_SYNC_WINDOW_SECONDSto259200(72h), even for what looks like a “just establish it” step. As the source comment explains,run_candidate.sh's 5400s (90-minute smoke-test) default would mark multi-hour EL syncsfully_synced=noand silently skip that anchor's entire CL sweep. - Tip check: reads
artifacts/<run-id>/<anchor>__prysm/env.txtand requiresfully_synced=yes. If missing or notyes, logs a warning and skips the CL sweep for this anchor entirely (continue) rather than running a CL sweep against an EL that never reached head. - CL sweep: exports
ETH2QS_BAKEOFF_ANCHOR_EL=<anchor>, then callsrun_candidate.sh <anchor> <cl>for every CL in<cls_csv>— each invocation runs in anchor mode (§3.1), purging only the CL datadir between candidates and reusing the live, already-synced EL. - Unsets
ETH2QS_BAKEOFF_ANCHOR_ELand moves to the next anchor in the list — establishing the next anchor'srun_candidate.shcall will purge the previous anchor's EL datadir as part of its own normal pre-install cleanup, which is the “rotation.”
Same ETH2QS_BAKEOFF_CONFIRMED=yes gate as run_candidate.sh. All results land in the same artifacts/<run-id>/<el>__<cl>/ layout as a direct run_candidate.sh call, so summarize.sh needs no special-casing for anchor-mode rows. Ends by calling summarize.sh itself.
7. run_queue.sh — async rerun-queue drain
The queue waits until each candidate can run safely, then calls run_candidate.sh. It complements run_bakeoff.sh and run_anchor_rotation.sh with queued measurements instead of a fixed manifest. Leave it running in tmux/systemd while another candidate finishes or an anchor EL catches up. Usage: ETH2QS_BAKEOFF_CONFIRMED=yes test/bakeoff/run_queue.sh [--dry-run].
7.1 Queue file format
${ETH2QS_BAKEOFF_QUEUE_FILE:-test/bakeoff/rerun_queue.tsv}, TAB-separated, # comments and blank lines ignored:
# execution\tconsensus\treason (TAB-separated; # comments and blank lines ignored)
geth\tprysm\trerun after extract_archive -o fix
nethermind\tlodestar\trerun: lodestar pruneHistory CLI-flag fix
erigon\tteku\trerun: previous row crash-looped before the watchdog existedA row missing the consensus column is malformed: counted, logged, fires a queue_malformed_row warn alert (§5.2), and the drain moves on — one bad line never stops the queue. A trailing \r is stripped from the execution field (tolerates a queue file edited on/copied from a CRLF host).
7.2 Preconditions — the wait-until-safe gate
Same operator gate as run_candidate.sh — ETH2QS_BAKEOFF_CONFIRMED=yes is required even for --dry-run: previewing the plan for a destructive harness still implies the operator has looked at this host. Before every row, polled every ETH2QS_BAKEOFF_QUEUE_POLL_SECONDS (default 60) up to a bounded ETH2QS_BAKEOFF_QUEUE_WAIT_SECONDS (default 7200) timeout:
- No other candidate running:
pgrep -f 'run_candidate\.sh', excluding this process's PID. This safety net is never load-bearing in practice: synchronous children already exit before the next row's check. - Anchor readiness: when
ETH2QS_BAKEOFF_ANCHOR_ELis set, require activeeth1.serviceandbakeoff_is_execution_synced. Like the candidate preflight (§3.1), this checks only the EL. The beacon-inclusivebakeoff_is_syncedcannot pass between shared-anchor candidates (§5.4); using it would time out and skip every queued row.
A timeout skips the row (queue_precondition_timeout warn alert, logged to run_queue.log) and the drain continues to the next row.
7.3 Force and the FORCE default
Reruns default to FORCE=yes so existing .done markers cannot turn measurements into no-op successes. After preconditions pass, the invocation is ETH2QS_BAKEOFF_FORCE="${ETH2QS_BAKEOFF_QUEUE_FORCE:-yes}" run_candidate.sh <execution> <consensus>. ETH2QS_BAKEOFF_QUEUE_FORCE defaults to “yes”. Otherwise, run_candidate.sh exits 0 without measuring when .done exists and FORCE is unset (§3.2).
Forcing overwrites that pair's previous artifacts. Set ETH2QS_BAKEOFF_QUEUE_FORCE=no to skip completed pairs and preserve their measurements.
7.4 Drain behavior
- Shares the same
ETH2QS_BAKEOFF_ALERT_LOGchannel asrun_candidate.sh(§5.2), defaulting under the sameartifacts/${ETH2QS_BAKEOFF_RUN_ID:-...}/root, so one file captures alerts for the whole run whether a row came from the manifest or the queue. - Every drained row appends one line to
$artifact_root/run_queue.log. A row that actually ran logs timestamp, row number, pair,rc, and the queue row's free-text reason; a malformed row logspair=-andresult=malformed, and a timed-out row logs the pair andresult=skipped_precondition_timeout— neither of those two carries the reason column. - A failing row (
run_candidate.shexits non-zero) is recorded and fires aqueue_candidate_failederror alert, but never aborts the drain — the loop always continues to the next row, mirroringrun_bakeoff.sh's own tolerance of a single candidate's failure. --dry-runprints the plan — queue file path, which preconditions would be checked, and each row's execution/consensus/reason plus the exactrun_candidate.shinvocation it would make — with no side effects.
8. summarize.sh — aggregation
Four generated outputs aggregate every artifacts/<run-id>/<el>__<cl>/ directory. The script never touches the hand-curated docs/CLIENT_BAKEOFF_RESULTS.md, the blog's source of truth.
Candidate artifacts → summarize.sh
config_optimal=yes; anchor_synced != no
Results (optimal config only)
config_optimal=no
Superseded — non-optimal config
No config verdict
Pre-gate — unknown config
Four output files, columns, and derivation rules
summary.csv— one row per candidate:pair,execution,consensus,install_exit_code,crash,sample_count,last_doctor_status, last_disk_bytes,residual_bytes,config_optimal,config_optimal_detail,fully_synced, sync_duration,sync_only,last_el_block,el_bytes,cl_bytes,anchor_synced,crash_loop_detected- Sync-time derivation:
_epochconvertsstarted_at_utc/synced_at_utcISO-8601 stamps to epoch seconds (date -u -d ... +%s); their difference istotal_s._install_wall_sparses theElapsed (wall clock)line out of/usr/bin/time -v'sinstall-time.txt(handles bothh:mm:ssandm:ss.ddforms).sync_only_s = total_s - install_sisolates sync time from install time; negative results are discarded (clamped to empty) rather than shown as a nonsense negative duration._fmt_hmrenders seconds asXhYYm. - Last EL block: hex-decodes
execution_sync.result.currentBlockfrom the lastsamples.jsonlline — a progress proxy even for candidates that hit the 72h cap without finishing. - Per-layer disk split:
el_bytes/cl_bytespartition the chosen disk TSV (disk-synced.tsvif synced, elsedisk-final.tsv) by basename match against EL/CL basenames extracted frominstall/utils/purge_ethereum_data.sh'sEL_DATA_DIRS/CL_DATA_DIRSarrays viaawk+gensub— exact basename matching so e.g.nimbus-eth1andnimbusare never confused.
- Sync-time derivation:
process-summary.csv— per-pair count of process-telemetry rows captured across the run (jq -r '.processes[]?'count fromsamples.jsonl).report.md— a human-readable dump: thesummary.csvtable (viacolumn -s, -t) plus every candidate'sfindings.md, concatenated.- A generated results skeleton at
$artifact_root/CLIENT_BAKEOFF_RESULTS.generated.md(gitignored — never the committeddocs/CLIENT_BAKEOFF_RESULTS.md), row-split into three sections by theconfig_optimalcolumn:- “Results (optimal config only)” —
config_optimal=yesandanchor_synced != no. - “Superseded — non-optimal config” —
config_optimal=no, explicitly excluded from ranking with a note that footprints aren't comparable. - “Pre-gate — unknown config” — no verdict recorded at all (pre-gate or triage-only rows).
Plus placeholder
## Recommendation/## Final synced disk footprint/## Changes driven by this bake-offsections with HTML comments telling the human reviewer what to fill in. - “Results (optimal config only)” —
9. Data model reference
Every file the harness writes, what writes it, and what's in it — 10 entries.
Show all 10 files (env.txt, samples.jsonl, marker files, ...)
| File | Written by | Contents |
|---|---|---|
env.txt | run_candidate.sh (append-only through the run) | Key-value run metadata: execution=, consensus=, mev=, started_at_utc=, sync_window_seconds=, sample_interval_seconds=, install_timeout=, harness_mode=full|establish|anchor, anchor_el= (if set), install_exit_code=, config_optimal=yes|no, config_optimal_detail=, fully_synced=yes|no, synced_at_utc= (if synced), service_crash_observed=, stall_restarts=/stall_failed=yes (if the stall watchdog fired), crash_loop_detected=yes/crash_loop_unit=/crash_loop_restarts= (if the crash-loop watchdog fired — see §3.5), anchor_synced=yes|no (anchor mode only), ended_at_utc= |
samples.jsonl | bakeoff_write_sample (one JSON line per tick) | timestamp_utc, services_alive, disk_tsv (raw TSV text), execution_sync, beacon_sync, processes, doctor, stats |
disk-before.tsv / disk-synced.tsv / disk-final.tsv / disk-after-cleanup.tsv | bakeoff_snapshot_disk at four checkpoints | path\tbytes\thuman rows per BAKEOFF_DATA_DIRS entry that exists, plus a filesystem:/ row |
candidates.tsv | manifest (hand-edited) | <execution>\t<consensus> rows — the source list run_bakeoff.sh expands into el__cl pair ids |
install.log / install-time.txt | run_candidate.sh install step | stdout+stderr of phase2, and /usr/bin/time -v resource/wall-clock accounting |
findings.md | run_candidate.sh teardown (seeded, human-completed) | Install/crash verdict skeleton for reviewer sign-off |
.done / .anchor-poisoned / .config-not-optimal / .stalled / .crash-looped | run_candidate.sh (touch), except .config-not-optimal — touched by bakeoff_check_config_optimal in lib.sh (§5.6) | Resume guard, anchor-invalidation flag, config-gate miss flag, stall-watchdog exhaustion flag, crash-loop-watchdog trip flag — all checked by filename existence, never by content |
advisor-alerts.jsonl | bakeoff_advisor_alert (§5.2), shared across every candidate in a run | One JSON object per line: ts_utc, severity (warn|error), run_id, candidate, kind (crash_loop, anchor_poisoned, stall, install_failed, window_capped_unsynced, queue_malformed_row, queue_precondition_timeout, queue_candidate_failed), detail. Every entry is also echoed as a greppable ADVISOR_ALERT ... line to the driver's stdout. |
rerun_queue.tsv / run_queue.log | queue file: operator (hand-edited); log: run_queue.sh (§7) | Queue: TAB-separated <execution>\t<consensus>[\t<reason>] rows (# comments/blank lines ignored). Log: one line per drained row — a row that actually ran logs timestamp, row number, pair, rc, and the reason; a malformed row logs pair=- and result=malformed; a timed-out row logs the pair and result=skipped_precondition_timeout — neither carries the reason column |
summary.csv / process-summary.csv / report.md / CLIENT_BAKEOFF_RESULTS.generated.md | summarize.sh | Aggregated, gitignored — inputs for a human to curate into the committed CLIENT_BAKEOFF_RESULTS.md |
10. Hardening fixes
Real campaign failures drove these defenses. See CLIENT_BAKEOFF_ISSUES_LOG.md for the incidents:
Always on
Crash-loop
Restart delta > 20 by default
Mark and log; exit the loop
Anchor mode
Anchor-drift
2 consecutive misses
Mark and log; preserve the shared EL
Opt-in
Stall
10 non-advancing samples by default
Restart the stalled unit, up to 3 times
Failure patterns and the fixes they motivated
du/pipefail resilience (bakeoff_snapshot_disk):du -sb ... || truesurvives a datadir file vanishing mid-walk during a live sync.- Capped-path capture (
_has_tokeninbakeoff_check_config_optimal): the--before dash-prefixed patterns preventsgrepfrom misparsing a pattern like--prune-storageas an option flag, which would otherwise exit 2 and get silently absorbed by2>/dev/nullinto a false “miss.” </dev/nullon every non-interactive command (clean-data,phase2install): preventsSIGTTINwhen the harness runs detached from a controlling terminal (background process group).- Stall watchdog (
ETH2QS_BAKEOFF_STALL_RESTART): entirely opt-in and inert by default; bounded restart count so a genuinely dead client still terminates the observation loop with a.stalledmarker rather than looping until the window cap. - Crash-loop watchdog (§3.5): always on. A flapping unit looks active again seconds after systemd's
Restart=, sobakeoff_services_alivecan miss a crash if no sample lands during the brief down window. Cumulative, monotonicNRestartscatches them regardless of sample timing by comparison with the pre-loop baseline. ExceedingETH2QS_BAKEOFF_MAX_RESTARTS(default 20) touches.crash-loopedand forces exit4, overriding a possibly-zeroinstall_exit_code. - Structured advisor-alert channel (
bakeoff_advisor_alert, §5.2): every failure mode listed in §9'sadvisor-alerts.jsonlrow funnels into one JSONL file plus a greppable stdout marker. - Anchor-poison detection is read-only: the watchdog that guards a shared anchor EL during a CL sweep only ever marks state (
.anchor-poisoned) and logs — it never intervenes, because restarting or killing a shared anchor would invalidate every remaining candidate in the rotation, not just the current one. - FORCE-rerun poison-marker clear:
ETH2QS_BAKEOFF_FORCE=yesclears.anchor-poisoned,.crash-loopedand.donetogether — without this, a stale poison marker from a previous attempt would silently survive a forced rerun and falsely finalizeanchor_synced=nofor an otherwise clean run. - Two-consecutive-sample sync confirmation (
synced_streak -ge 2inrun_candidate.sh,synced_streaklocal to the observation loop): guards against a single racy sample where the EL and CL heads transiently appear caught up. - Execution-only anchor precondition in the queue (
run_queue.sh, §7.2):bakeoff_is_execution_synced, not the beacon-inclusivebakeoff_is_synced— see §5.4 for why a beacon-inclusive check on a shared anchor can never pass.
Bottom line
run_bakeoff.sh,run_anchor_rotation.sh, andrun_queue.shsharerun_candidate.sh: every results row follows the same state machine and artifact format.- Crash-loop and anchor-drift watchdogs are detection-only by design: they only mark state and log. They watch a flapping client or a shared EL, outside their repair scope. Only the opt-in stall watchdog restarts services, within a cap.
- The config-optimality gate (§5.6) files synced clients with history-pruning silently off under “Superseded — non-optimal config,” excluding their footprints from rankings.
- Every detected failure, including crash-loop, anchor-poisoned, stall, and malformed queue rows, reaches
advisor-alerts.jsonlfrom any driver.
See also
- HOW_WE_TESTED_WITH_CLAUDE.md — the agent-orchestration model, the six-week timeline (23-day initial campaign, then steady-state and restart-resume follow-ups), and the war stories behind the hardening fixes above.
- CLIENT_BAKEOFF_RESULTS.md — the numbers, source-of-truth.
- CLIENT_BAKEOFF_BLOG.md — the narrative writeup of the findings.
- CLIENT_BAKEOFF_ISSUES_LOG.md — the incident log that motivated several of the hardening fixes above.
Read next
Sources: this page renders docs/CLIENT_BAKEOFF_HARNESS.md on GitHub. See also the bake-off results writeup.