Skip to content

execution/state: publish each tx's origins once - #25439

Open
lupin012 wants to merge 3 commits into
mainfrom
lupin012/publish-origins-once
Open

lupin012 wants to merge 3 commits into
mainfrom
lupin012/publish-origins-once

Conversation

@lupin012

Copy link
Copy Markdown
Contributor

Problem

IntraBlockState.PublishOrigins copies the tx's cold-read origin cells into the versionMap, but it keeps them in versionedOrigins. Only Reset/Release clear that set, and ResetVersionedIO does not. So a state that is reused for all txs of a block publishes again the origins of all previous txs at every tx, and the cost grows with the square of the block's tx count. Every StoreOriginStorage call also boxes key and value for sync.Map.LoadOrStore, even when the key is already there.

Affected paths: everything that finalizes several txs on one versioned state through FinalizedWrites. This includes debug_traceBlockByHash/Number, trace_*, eth_callMany, eth_simulateV1, debug_executionWitness, the overlay API, and execution/exec/block_assembler.go when BAL is active. Parallel execution is not affected, because it calls Reset for every tx.

Fix

Release the origin cells at the end of PublishOrigins, once they are in the versionMap. Nothing reads versionedOrigins after the publish.

Test

TestFinalizedWritesHandsOverOrigins (TDD): after FinalizedWrites, the origin cells are no longer kept for the next tx, and the origin can still be read from the versionMap. It fails without the fix.

Measurements

Mainnet node at the tip, --prune.mode=minimal. Load is the mixed profile of rpc-tests (rpc_pattern_gen --profile perf/pattern/mainnet/config/mixed.profile --counts 200000 --verify) replayed with rpc_perf, 60s per step.

Main (dedabcf) CPU profile at 2000 qps: debug_traceBlockByHash is 4.3% of the requests but 83% of the CPU. PublishOrigins alone is 47% of the CPU, and StoreOriginStorage is 62% of the allocated bytes (at 1000 qps).

main this PR
50 debug_traceBlockByHash (callTracer), serial, best of 3 7.00s 2.44s
responses identical on all 50 blocks
mixed 1000 qps, p99 324ms 97ms
mixed 2000 qps, success / p99 78.9% / 22s 100% / 125ms
mixed 3000 qps, success / p99 56.7% / 19.6s 100% / 181ms
CPU at 2000 qps ~27 cores ~11 cores
PublishOrigins share of CPU at 2000 qps 47% 1%

PublishOrigins kept the published origin cells, and only Reset/Release
cleared them. A state reused for all txs of a block (debug_traceBlock*,
trace_*, eth_callMany, eth_simulateV1, the block assembler with BAL)
published again the origins of all previous txs at every tx, so the cost
grew with the square of the block's tx count. Release the cells once they
are in the versionMap.
@lupin012
lupin012 marked this pull request as ready for review October 27, 2026 17:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the origin-cell cleanup intended to prevent repeated publication.

No open issues 🎉

What changed in this PR

This PR prevents repeated publication of transaction origins when Erigon reuses a versioned state across a block, removing the resulting quadratic work.

Changes:

  • Release and reset origin cells after publishing them to the version map.
  • Add a regression test confirming that FinalizedWrites clears pending origins while preserving the published storage value.
File Description
execution/​state/​storage_origin_seed_test.go Tests origin cleanup and preservation in the version map.
execution/​state/​intra_block_state.go Clears origin cells after PublishOrigins completes.

Review effort: Balanced

@yperbasis yperbasis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two non-blocking notes:

  1. PublishOrigins iterates with forEachAddr, which visits an address once per populated path map. An address with both an account origin and storage origins therefore runs the closure twice and re-issues every StoreOriginStorage. Ranging ws.address and then ws.storage directly would halve the LoadOrStore boxing and make the IsEmpty half of the guard redundant. Fine as a follow-up.

  2. TestFinalizedWritesHandsOverOrigins records only a storage origin (a cold GetState never reaches seedOrigin), so nothing in the package asserts that StoreOriginAccount runs before releaseVWAddress nils the cell. A cold GetBalance or Exist plus an assertion that versionMap.LoadOriginAccount(addr) returns the committed account would cover that ordering.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants