Feedback loop
Bloating naïvely — spam one transaction type until the state is big enough — produces a state with the wrong shape. The whole point of the project is comparing apples to apples across multipliers, so the synthetic state has to keep mainnet’s composition (≈27 % account / 67 % storage / 6 % code) as it grows. That’s a control problem.
The loop
The Go orchestrator runs a strictly sequential, sensor-paced loop — step through one tick below:
The controller compares the live composition to the mainnet-shaped target and scores the bloating “verbs”, picking the transaction mix that closes the gap fastest.
verb mix ← argmax( score | target − current )- The sidecar reports the current composition vector (account / storage / code bytes).
- The controller computes the residual against the target and picks a verb mix.
- The facade turns the plan into signed transactions; the block is submitted via Engine API.
- After commit, the loop repeats — paced by the sensor, never running ahead of measured state.
How the controller picks verbs
Each verb has an effect on the composition axes. Stack those into a matrix (verb → axis-byte effect). Given the residual , the controller solves a projected-gradient step for the verb weights that best reduce the residual:
where projects onto the simplex (weights non-negative, sum to one). is seeded
from a REFERENCE_F calibration and learned online as observed deltas come back from the
sidecar.
Two stabilizers (learned the hard way)
Replay artifacts
Every run is reproducible by construction — three files capture it:
| File | Format | Purpose |
|---|---|---|
payloads.rlp | 4-byte-LE length-prefixed RLP ExecutionPayloadV3 frames | The committed block stream. Replayable on any EL. |
journal.bin | protobuf records + BLAKE3 chain hash | Per-batch controller state, residuals, plan, observation. Idempotent restart. |
run-manifest.json | JSON | genesis_sha256, chain_identity_hash, target_history, final_state_root. |
A pending-batch.bin.<id> gob captures the in-flight batch for crash recovery, so a killed run
resumes without re-sending or skipping a block.
Cross-EL replay
Anyone with payloads.rlp + run-manifest.json + genesis.json can reproduce the state on a
different client:
# 1. heal block-number gaps left by partial-acceptance skips
heal-payloads heal --in payloads.rlp --out payloads.healed.rlp \
--nm-rpc <source-NM> --jwt-path jwt.hex --include-tail
# 2. replay against the target EL and verify the final state root
orchestrator replay --payloads payloads.healed.rlp \
--manifest run-manifest.json --rpc-url http://<target-el>:8551 --jwt-path jwt.hex
Outcomes are explicit: ErrReplayInvalid (a payload was rejected), ErrReplayMismatch (all
blocks accepted but final stateRoot ≠ manifest), or success (bit-exact replay verified).
Sensor lag
The sidecar tracks chain head with ~0 blocks behind under load, but on an idle chain the composition read can trail head by ~30 blocks (a known RocksDB-secondary visibility artifact, not a throughput bug). Headline measurements are therefore taken after the bloater stops and the sidecar has caught up, the same hygiene rule snap-sync benchmarking uses.