Skip to content

Fix eth_getTransactionReceipt race condition#1802

Merged
librelois merged 20 commits intopolkadot-evm:masterfrom
moonbeam-foundation:manuel/fix-receipt-race-condition
Feb 3, 2026
Merged

Fix eth_getTransactionReceipt race condition#1802
librelois merged 20 commits intopolkadot-evm:masterfrom
moonbeam-foundation:manuel/fix-receipt-race-condition

Conversation

@manuelmauro
Copy link
Copy Markdown
Contributor

@manuelmauro manuelmauro commented Jan 15, 2026

Problem

Users report intermittent issues where eth_getTransactionReceipt returns null immediately after getting a transaction hash from eth_getBlockByNumber. This happens because:

  • eth_getBlockByNumber reads from runtime storage (available immediately after block import)
  • eth_getTransactionReceipt reads from the mapping-sync database (delayed until worker processes the block)

Solution

Rely on mapping-sync storage for all impacted RPCs.

Changes

  • ts-tests/tests/test-receipt-consistency.ts: Integration tests for receipt consistency

Testing

The TDD commit includes an artificial 2-second delay in mapping-sync to reliably reproduce the race condition. Remove before merging.

@manuelmauro manuelmauro force-pushed the manuel/fix-receipt-race-condition branch from ba8c8fd to 5d7e6f0 Compare January 15, 2026 11:46
@manuelmauro manuelmauro force-pushed the manuel/fix-receipt-race-condition branch from 5d7e6f0 to 105efa2 Compare January 15, 2026 16:16
ethers.js v6 caches eth_getTransactionCount responses at the provider
level. When multiple tests run sequentially using the same signer,
subsequent tests may receive stale cached nonces instead of making
fresh RPC calls.

This fixes the "nonce has already been used" test failures by:
- Using direct RPC calls (context.ethersjs.send) to bypass caching
- Explicitly setting nonces in transactions to ensure correctness

The backend changes to latest_block_hash() use mapping-sync as the
source of truth for consistency with other RPCs. When the best block
isn't indexed yet, it falls back to the latest indexed block.
@manuelmauro manuelmauro marked this pull request as ready for review January 26, 2026 15:18
@manuelmauro manuelmauro requested a review from sorpaas as a code owner January 26, 2026 15:18
@librelois
Copy link
Copy Markdown
Member

@sorpaas we would appreciate to have your feedback on this PR, can you take a look? Thanks

Copy link
Copy Markdown
Member

@librelois librelois left a comment

Choose a reason for hiding this comment

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

Missing Database Migration

The diff adds a new column BLOCK_NUMBER_MAPPING = 4 and changes NUM_COLUMNS from 4 to 5, but upgrade.rs still shows:

CURRENT_VERSION: u32 = 2
V2_NUM_COLUMNS: u32 = 4

Existing databases won't have column 4 populated for already-synced blocks. After an upgrade, historical blocks won't be queryable via eth_getBlockByNumber until re-synced from scratch. A migration from version 2 to version 3 should be added to backfill the block_number -> ethereum_block_hash mapping.

@manuelmauro
Copy link
Copy Markdown
Contributor Author

Missing Database Migration

The diff adds a new column BLOCK_NUMBER_MAPPING = 4 and changes NUM_COLUMNS from 4 to 5, but upgrade.rs still shows:

CURRENT_VERSION: u32 = 2
V2_NUM_COLUMNS: u32 = 4

Existing databases won't have column 4 populated for already-synced blocks. After an upgrade, historical blocks won't be queryable via eth_getBlockByNumber until re-synced from scratch. A migration from version 2 to version 3 should be added to backfill the block_number -> ethereum_block_hash mapping.

Added KV store migration in 091f2ae

@librelois librelois added this pull request to the merge queue Feb 3, 2026
Merged via the queue into polkadot-evm:master with commit f90139d Feb 3, 2026
4 checks passed
@librelois librelois deleted the manuel/fix-receipt-race-condition branch February 3, 2026 16:35
manuelmauro added a commit to moonbeam-foundation/frontier that referenced this pull request Feb 3, 2026
* test: 🧪 add TDD failing test

* refactor: ♻️ use mapping-sync as single source of truth for eth_ RPCs

* fix: 🐛 fix missing args

* test: ✅ redefine waitForBlock as a polling function

* style: 🎨 fmt

* fix: 🐛 wait for the right block number

* revert: 🔥 remove temporary delay

* chore: 📦 update package lock

* test: ✅ update test timeouts

* test: ✅ remove explicit nonce to fix EIP-7702 test race condition

* fix: bypass ethers.js nonce caching in EIP-7702 tests

ethers.js v6 caches eth_getTransactionCount responses at the provider
level. When multiple tests run sequentially using the same signer,
subsequent tests may receive stale cached nonces instead of making
fresh RPC calls.

This fixes the "nonce has already been used" test failures by:
- Using direct RPC calls (context.ethersjs.send) to bypass caching
- Explicitly setting nonces in transactions to ensure correctness

The backend changes to latest_block_hash() use mapping-sync as the
source of truth for consistency with other RPCs. When the best block
isn't indexed yet, it falls back to the latest indexed block.

* fix: 🐛 verify mapped blocks against canonical chain during reorgs

* revert: 🔥 remove LATEST_INDEXED_BLOCK

* fix: 🐛 correctly error instead of defaulting to block number 0

* fix: 🐛 verify all non-genesis blocks

* test: ✅ add error handling to waitForBlock

* fix: 🐛 use consistent behavior for eth_getBlockByNumber("latest") on SQL backend

* feat: ✨ add KV store migration

* fix: 🐛 do not default to 0

* feat: 🔊 log processed entries in parity db migration
manuelmauro added a commit to moonbeam-foundation/frontier that referenced this pull request Feb 3, 2026
* test: 🧪 add TDD failing test

* refactor: ♻️ use mapping-sync as single source of truth for eth_ RPCs

* fix: 🐛 fix missing args

* test: ✅ redefine waitForBlock as a polling function

* style: 🎨 fmt

* fix: 🐛 wait for the right block number

* revert: 🔥 remove temporary delay

* chore: 📦 update package lock

* test: ✅ update test timeouts

* test: ✅ remove explicit nonce to fix EIP-7702 test race condition

* fix: bypass ethers.js nonce caching in EIP-7702 tests

ethers.js v6 caches eth_getTransactionCount responses at the provider
level. When multiple tests run sequentially using the same signer,
subsequent tests may receive stale cached nonces instead of making
fresh RPC calls.

This fixes the "nonce has already been used" test failures by:
- Using direct RPC calls (context.ethersjs.send) to bypass caching
- Explicitly setting nonces in transactions to ensure correctness

The backend changes to latest_block_hash() use mapping-sync as the
source of truth for consistency with other RPCs. When the best block
isn't indexed yet, it falls back to the latest indexed block.

* fix: 🐛 verify mapped blocks against canonical chain during reorgs

* revert: 🔥 remove LATEST_INDEXED_BLOCK

* fix: 🐛 correctly error instead of defaulting to block number 0

* fix: 🐛 verify all non-genesis blocks

* test: ✅ add error handling to waitForBlock

* fix: 🐛 use consistent behavior for eth_getBlockByNumber("latest") on SQL backend

* feat: ✨ add KV store migration

* fix: 🐛 do not default to 0

* feat: 🔊 log processed entries in parity db migration
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