fix(simulator): OpcodeStats Add/Sub must use union of keys#193
Closed
meyer9 wants to merge 1 commit into
Closed
Conversation
Before this change, both OpcodeStats.Add and OpcodeStats.Sub iterated only
the 'other' argument:
for opcode, count := range other {
result[opcode] = o[opcode] + count
}
This silently dropped any key present in the receiver 'o' but absent from
'other'. For Add(empty) the result was an empty map, losing 'o' entirely.
Concrete consequence in sendTxs: per-tx blockCounts is computed as
blockCounts = expected.Sub(actual).Round()
where 'expected' is base x Mul(numCalls+1 x scaleFactor) (carrying all base
opcodes + precompiles) and 'actual' starts empty for the first tx. With the
buggy Sub iterating actual=empty, blockCounts.Opcodes and
blockCounts.Precompiles were ALWAYS empty for every tx in every block.
The downstream effect: workloads with precompile counts in their config
(e.g. base-mainnet-simulation: bls12381MapG2, ecrecover, bls12381G1Add,
bls12381G1MultiExp) had per-tx execution that skipped all precompile work.
Each tx therefore consumed far less gas than the gas estimate predicted,
and blocks under-filled the gas budget. Example: base-mainnet-simulation @
GasLimit=250M (block_time=1s) reported gas/per_block ~51M (20% of limit)
in production runs because per-tx skipped the heaviest workload component.
Fix: both Add and Sub now iterate the union of keys. For Sub, a key present
only in 'other' produces a negative result entry (matches arithmetic
semantics).
Includes unit tests for the union semantics and a regression test for the
first-tx scenario in sendTxs.
Collaborator
🟡 Heimdall Review Status
|
Collaborator
Author
|
Superseded by #194 which combines this OpcodeStats fix with the pre-init buffer bump and observed-gas recalibration into one coordinated change. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
OpcodeStats.AddandOpcodeStats.Sub(inrunner/payload/simulator/simulatorstats/types.go) only iterate the other argument, silently dropping keys present in the receiverobut absent fromother:Calling
OpcodeStats{"A": 1, "B": 2}.Add(OpcodeStats{})returns{}— the receiver is gone.Downstream impact in
sendTxsThe simulator worker accumulates per-tx config like:
For the first tx,
actualis empty. The buggySubiteratesactual(empty) → producesblockCountswith emptyOpcodes/Precompilesmaps. Thenactual.Add(blockCounts)likewise iteratesblockCounts(now also empty for these fields) → still empty.Result: every tx in every block has empty
Precompilesin itsSimulatorConfigcall, so the on-chainRun()skips all precompile work that the YAML config specified.Observable consequence
Workloads with precompiles in their config —
base-mainnet-simulationis the only one inmainnet-config.yml— under-fill the gas budget because per-tx gas is far below whatcalcNumCallspredicted from the gas-estimation call (which DID include precompiles). Concrete data from production runtest-1779411190779930:Note the flat ~49–51 M ceiling regardless of GasLimit — that's the per-tx cost of the non-precompile portion (storage + accounts + opcodes) × 100 txs, which is what the worker actually executes. Other workloads (
storage-create-full-block, etc.) don't declare precompiles in their config, so they're unaffected and fill ~98%.Fix
Both
AddandSubnow iterate the union of keys:func (o OpcodeStats) Add(other OpcodeStats) OpcodeStats { - result := make(OpcodeStats) + result := make(OpcodeStats, len(o)+len(other)) + for opcode, count := range o { + result[opcode] = count + } for opcode, count := range other { - result[opcode] = o[opcode] + count + result[opcode] += count } return result }Submatches: keys only inotherproduce negative entries (correct arithmetic semantics).Tests added
runner/payload/simulator/simulatorstats/types_test.go:TestOpcodeStatsAdd_UnionOfKeys/TestOpcodeStatsSub_UnionOfKeys— direct union-of-keys checkTestOpcodeStatsAdd_EmptyOther/TestOpcodeStatsAdd_EmptyReceiver/TestOpcodeStatsSub_EmptyOther— empty-side edge casesTestStatsSubAdd_FirstTxBlockCountsIncludePrecompiles— regression test for the production usage pattern insendTxsAll pass.
go test ./...clean across the repo.Expected outcome after this PR + #192 land together
base-mainnet-simulationat 150M/200M/250M should fill to ~98% (gas budget becomes the cap, not the missing precompiles). Other workloads unchanged. The 1.05x→2.0x pre-init buffer in #192 covers the slightly higher consumption that comes from actually executing precompile work per tx.Draft because end-to-end validation needs one CI run.