Skip to content

fix(simulator): remove double-application of scaleFactor in targetCalls#183

Closed
meyer9 wants to merge 4 commits into
mainfrom
fix/simulator-scale-factor-double-apply
Closed

fix(simulator): remove double-application of scaleFactor in targetCalls#183
meyer9 wants to merge 4 commits into
mainfrom
fix/simulator-scale-factor-double-apply

Conversation

@meyer9
Copy link
Copy Markdown
Collaborator

@meyer9 meyer9 commented May 15, 2026

Root cause

scaleFactor is applied per-tx inside sendTxs to scale up the operations each transaction performs:

// line 608 — scaleFactor applied inside each tx's operation count
expected := t.payloadParams.Mul(float64(t.numCalls+1) * t.scaleFactor)
blockCounts := expected.Sub(actual).Round()

So each tx already does scaleFactor × payloadParams operations (e.g. with avg_gas_used=35M and GasLimit=200M, scaleFactor≈5.71, each tx does ~280 storage reads instead of 49).

targetCalls was then computed as:

targetCalls := uint64(math.Ceil(float64(t.numCallsPerBlock) * t.scaleFactor))
// = ceil(100 * 5.71) = 572

This multiplies scaleFactor in a second time at the tx-count level. The result: the simulator tries to keep 572 txs in flight per block, but the builder only absorbs ~100 (the actual numCallsPerBlock worth of gas). After 2 blocks, pendingTxs grows past 572 and callsToSend=0 forever — producing empty (deposit-only) blocks for the rest of the run.

Observed: 7,000+ consecutive empty blocks in a 900-block base-mainnet-simulation run (GasLimit=200M, calls_per_block=100, avg_gas_used=35M).

Fix

targetCalls should be numCallsPerBlock — the number of txs to send per block. scaleFactor belongs only inside the per-tx operation calculation.

// Before
targetCalls := uint64(math.Ceil(float64(t.numCallsPerBlock) * t.scaleFactor))

// After
targetCalls := t.numCallsPerBlock

The configForAllBlocks storage pre-allocation at Setup (line 392) correctly uses numCallsPerBlock × numBlocks × scaleFactor × 1.05 — scaleFactor appears once there too, so no change needed.

meyer9 added 4 commits May 15, 2026 09:26
…empty blocks

The sequencer was dispatching engine_forkchoiceUpdatedWithAttributes immediately
after sendTxs, whose duration is proportional to tx workload. For heavy payloads
(ecrecover at 150M gas), this left the FCU arriving with <75ms until block_timestamp,
below flashblocks' leeway window, causing 'FCU arrived too late' and deposit-only
empty blocks.

Fix: sleep until the next whole-second Unix boundary before dispatching the FCU.
block_timestamp is set to boundary+1s, so the builder always sees exactly
~1000ms of building time minus FCU RPC latency, regardless of sendTxs duration.
The getBuiltPayload sleep is changed to sleep until boundary+1s rather than a
fixed 1s, so total block period is unchanged.

Tested before/after with ecrecover simulator payload at 150M gas, 20 blocks:
  Before: 1 empty block (block #11, 1 tx, deposit only)
  After:  0 empty blocks (all 20 blocks full at 352 txs)
…argetCalls

When scaleFactor > 1 (e.g. base-mainnet-simulation with avg_gas_used=35M and
GasLimit=200M gives scaleFactor≈5.71, targetCalls=572), the previous guard
  if pendingTxs >= targetCalls { callsToSend = 0 }
allowed pendingTxs to accumulate past targetCalls after just 2 blocks (572 sent,
only ~50 included per block → pendingTxs grows to 1044 ≥ 572). From that point
callsToSend=0 forever and every subsequent block is empty (deposit-tx only).

The gate should use numCallsPerBlock (unscaled) since scaleFactor scales the
ops-per-tx, not the tx count threshold for backpressure. With numCallsPerBlock=100
as the gate, pending tx buildup is bounded to one block's worth and the simulator
continues generating txs regardless of scaleFactor.
scaleFactor is applied per-tx inside sendTxs at line 608:
  expected = payloadParams.Mul(numCalls+1 * scaleFactor)
meaning each tx already does scaleFactor times the base operations.

targetCalls was then computed as:
  numCallsPerBlock * scaleFactor
which multiplied scaleFactor in again — resulting in 5.71x more txs being
targeted than actually needed (e.g. 572 instead of 100 for base-mainnet-simulation
with GasLimit=200M, avg_gas_used=35M).

Since the builder only absorbs ~100 txs/block worth of those scaled ops,
pendingTxs accumulated beyond targetCalls after 2 blocks, causing callsToSend=0
forever and 7,000+ consecutive empty (deposit-only) blocks in a 900-block run.

Fix: targetCalls = numCallsPerBlock (the tx count, not the ops count).
scaleFactor belongs only in the per-tx operation calculation.
@cb-heimdall
Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@meyer9
Copy link
Copy Markdown
Collaborator Author

meyer9 commented May 15, 2026

Replaced by cleaner branch — one commit, one file.

@meyer9 meyer9 closed this May 15, 2026
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.

2 participants