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:

└▸payloads.rlp — every block, for bit-exact replay
feedback — sidecar serves the live composition back to the controller (right → left), closing the loop
1 / 5 · Decide

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 )
  1. The sidecar reports the current composition vector mkm_k (account / storage / code bytes).
  2. The controller computes the residual against the target MM^\star and picks a verb mix.
  3. The facade turns the plan into signed transactions; the block is submitted via Engine API.
  4. 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 FF (verb → axis-byte effect). Given the residual rk=Mmkr_k = M^\star - m_k, the controller solves a projected-gradient step for the verb weights ww that best reduce the residual:

wk=ΠΔ ⁣(Frk),w_k = \Pi_{\Delta}\!\left(F^\top r_k\right),

where ΠΔ\Pi_{\Delta} projects onto the simplex (weights non-negative, sum to one). FF 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:

FileFormatPurpose
payloads.rlp4-byte-LE length-prefixed RLP ExecutionPayloadV3 framesThe committed block stream. Replayable on any EL.
journal.binprotobuf records + BLAKE3 chain hashPer-batch controller state, residuals, plan, observation. Idempotent restart.
run-manifest.jsonJSONgenesis_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.