Skip to content

fix(exec3): execution loops infinitely on zero-gas block regions during initial sync (Polygon bor-mainnet) #150

@martianacademy

Description

@martianacademy

Bug Description

When syncing Polygon bor-mainnet from genesis with --no-downloader, the execution stage (4/6) enters an infinite loop at block ~856,625. The node repeatedly executes the same 5,000 block batch without advancing.

Logs

[4/6 Execution] starting  from=851625 to=878729 fromTxNum=1703301 initialCycle=true
[4/6 Execution] Done      blk=856625 blks=5001 gas/s=0 buf=4B/1.0GB
[4/6 Execution] starting  from=851625 to=878729 fromTxNum=1703301 initialCycle=true
[4/6 Execution] Done      blk=856625 blks=5001 gas/s=0 buf=4B/1.0GB
... (repeats infinitely)

Key indicators:

  • gas/s=0 — all blocks in the batch have zero gas usage
  • buf=4B — no state changes buffered
  • from=851625 never advances
  • blks=5001 = exactly LoopBlockLimit + 1

Root Cause

Early Polygon blocks (~0 to ~1.2M) have zero transactions and zero gas. During initial sync:

  1. ExecV3 processes blocks from the last commitment position
  2. After processing LoopBlockLimit (5000) blocks, the block limit fires at line 815 of exec3.go and breaks the loop
  3. The function returns ErrLoopExhausted to the caller
  4. The caller commits the transaction and re-invokes ExecV3
  5. NewSharedDomains → SeekCommitment reads the same old commitment position because:
    • No state changes occurred (0 gas blocks)
    • The commitment system can only write at step boundaries (DefaultStepSize = 1,562,500)
    • The current txNum (1,703,301) is between step boundaries, so no new commitment was written
  6. Execution starts from the same block → infinite loop

Proposed Fix

Add totalGasUsed > 0 to the block limit condition in execution/stagedsync/exec3.go line 815:

- if blockLimit > 0 && blockNum-startBlockNum+1 >= blockLimit {
+ if blockLimit > 0 && blockNum-startBlockNum+1 >= blockLimit && totalGasUsed > 0 {

This allows execution to continue through empty-block regions until blocks with actual state changes are reached, where commitment CAN be saved. Empty blocks are essentially free to process (~80K blocks/sec).

Why This Is Safe

  • totalGasUsed is already tracked in the execution loop (used for logging at line 727)
  • Empty blocks (0 gas) do not create state changes, so processing more is essentially free
  • Once blocks with actual gas are encountered, the block limit fires normally
  • The commitment system can then save state because real state changes create writable data
  • Tested: node successfully advanced from block 856,625 to 1,100,000+ after applying this fix

Affected Versions

  • v3.5.0 (0xPolygon fork) — loops at block ~856,625
  • v3.1.0 (erigontech upstream) — loops at block ~806,238
  • Any version syncing a chain with extended zero-gas block regions from genesis

Reproduction

erigon --chain=bor-mainnet --no-downloader --prune.mode=minimal
# Wait for execution stage to reach ~856K
# Observe infinite loop in logs

Environment

  • OS: macOS ARM64 (also reproducible on Linux)
  • Chain: Polygon bor-mainnet
  • Sync mode: from genesis, no snapshots (--no-downloader)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions