Skip to content

🐛 fix(state): fall back to scalar RPCs when fork lacks eth_getProof - #2093

Open
ebramanti wants to merge 1 commit into
evmts:mainfrom
herd-labs:fix/eth-getproof-scalar-fallback
Open

🐛 fix(state): fall back to scalar RPCs when fork lacks eth_getProof#2093
ebramanti wants to merge 1 commit into
evmts:mainfrom
herd-labs:fix/eth-getproof-scalar-fallback

Conversation

@ebramanti

@ebramanti ebramanti commented Aug 5, 2026

Copy link
Copy Markdown

Description

Fork account hydration falls back to eth_getBalance + eth_getTransactionCount + eth_getCode on providers that do not serve eth_getProof.

Motivation

Tevm's fork mode hydrates every uncached remote account through a single empty-storageKeys eth_getProof call in getAccountFromProvider — the only RPC path for loading an account's balance/nonce/codeHash/storageRoot. Some EVM chains do not serve eth_getProof at all, so a fork starts fine (startup only needs eth_chainId + eth_getBlockByNumber) and then dies on the first touch of any uncached account — the first tevmCall, tevmSetAccount, tevmDeal, or eth_createAccessList.

Monad mainnet (chainId 143) does not serve eth_getProof on any provider, so tevm cannot fork it today. The gap is not Monad-specific — ZKsync OS chains officially don't support it and Moonbeam lists it as unsupported — while all of them serve the scalar quartet (eth_getBalance/eth_getTransactionCount/eth_getCode/eth_getStorageAt).

Tevm is the outlier here only because it inherited EthereumJS RPCStateManager's proof-coupled loader. Every other major fork simulator already uses the scalar mechanism this PR falls back to:

  • Foundry fetches accounts via block-pinned get_balance+get_transaction_count+get_code_at, with its own availability downgrade (ACCOUNT_FETCH_SEPARATE_REQUESTS) (foundry-fork-db backend.rs)
  • Hardhat/EDR runs the same trio under tokio::try_join! and synthesizes code_hash: Bytecode::hash_slow(code) (edr client.rs)
  • Ganache did the same, with codeHash = keccak(code) computed locally

Change

getAccountFromProvider still prefers the single-round-trip eth_getProof. On a method-unavailable error — -32601, -32004, or -32600 with a "not available/found/supported" message, matched across the error cause chain via viem's BaseError.walk — it permanently downgrades that fork transport to the three concurrent scalar calls, pinned to the same resolved fork block. All other errors rethrow unchanged; uncoded errors (network failures wrapped as UnknownRpcError) never trigger the downgrade. The fetched bytecode primes both contract-code caches, and the public eth_getProof action and light-client verified reads are untouched.

Additional Information

Summary by CodeRabbit

  • New Features

    • Added automatic account data retrieval fallback when proof requests aren’t supported by the connected provider.
    • Fallback requests are pinned to the fork block for consistent balances, transaction counts, and contract code.
    • Retrieved contract bytecode is cached for faster subsequent access.
  • Bug Fixes

    • Improved handling of unsupported proof requests while preserving errors for rate-limit, network, and other provider failures.
    • Added reliable handling for accounts that do not exist.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

@ebramanti is attempting to deploy a commit to the evmts Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 55165830-dc13-4ced-a01d-73101add1fd4

📥 Commits

Reviewing files that changed from the base of the PR and between 6199d04 and 26332f6.

📒 Files selected for processing (3)
  • .changeset/proud-otters-prove.md
  • packages/state/src/actions/getAccountFromProvider.js
  • packages/state/src/actions/getAccountFromProvider.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • .changeset/proud-otters-prove.md
  • packages/state/src/actions/getAccountFromProvider.js
  • packages/state/src/actions/getAccountFromProvider.spec.ts

📝 Walkthrough

Walkthrough

getAccountFromProvider now falls back to block-pinned scalar RPC calls when eth_getProof is unsupported. The fallback persists per transport, primes contract-code caches, and preserves existing account semantics.

Changes

Fork account hydration

Layer / File(s) Summary
Proof detection and routing
packages/state/src/actions/getAccountFromProvider.js
getAccountFromProvider tries eth_getProof, preserves successful proof mapping, detects supported unsupported-method errors, and rethrows unrelated errors.
Scalar fallback and caching
packages/state/src/actions/getAccountFromProvider.js, .changeset/proud-otters-prove.md
Unsupported transports use concurrent balance, nonce, and code calls pinned to the fork block. The fallback updates contract caches, computes codeHash, and uses the canonical empty storage root.
Fallback validation
packages/state/src/actions/getAccountFromProvider.spec.ts
Tests cover proof success, supported and unrelated errors, transport downgrade persistence, block pinning, cache priming, and nonexistent accounts.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant getAccountFromProvider
  participant ForkTransport
  participant ContractCodeCache
  getAccountFromProvider->>ForkTransport: eth_getProof
  ForkTransport-->>getAccountFromProvider: unsupported-method error
  getAccountFromProvider->>ForkTransport: balance, nonce, and code at fork block
  ForkTransport-->>getAccountFromProvider: scalar account data
  getAccountFromProvider->>ContractCodeCache: prime contract-code cache
  getAccountFromProvider-->>getAccountFromProvider: compute codeHash and create account
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: scalar RPC fallback when fork providers lack eth_getProof.
Description check ✅ Passed The description clearly explains the motivation and implementation, but it does not include the template's Testing section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ebramanti ebramanti changed the title 🐛 fix(state): fall back to scalar RPCs when fork lacks eth_getProof 🐛 fix(state): fall back to scalar RPCs when fork lacks eth_getProof Aug 5, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/state/src/actions/getAccountFromProvider.spec.ts (1)

79-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add explicit return types to the new functions.

Add an explicit object return type to createRecordingTransport. Add number to methodCount. Add Promise<void> to each new async it callback.

As per coding guidelines, **/*.{ts,tsx}: “We always explicitly type return types.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/state/src/actions/getAccountFromProvider.spec.ts` around lines 79 -
296, The new helper and test callbacks lack explicit return types. Update
createRecordingTransport with its concrete object return type, annotate
methodCount as returning number, and annotate every async it callback in the
scalar fallback describe block with Promise<void>.

Source: Coding guidelines

packages/state/src/actions/getAccountFromProvider.js (1)

14-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Complete the JSDoc for both functions.

Add working @example blocks with imports. Document propagated errors with @throws for isMethodUnavailableError and getAccountFromProvider.

As per coding guidelines, **/*.js: “We always include complete jsdoc information including @throws @example etc.”

Also applies to: 37-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/state/src/actions/getAccountFromProvider.js` around lines 14 - 23,
Complete the JSDoc for both isMethodUnavailableError and getAccountFromProvider
by adding import-based, working `@example` blocks and documenting propagated
errors with `@throws`. Keep the existing descriptions and behavior unchanged, and
ensure each example reflects the actual function API.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/state/src/actions/getAccountFromProvider.js`:
- Around line 29-30: Update the error-message predicate in
getAccountFromProvider to accept “unavailable” method wording, including “Method
unavailable” and “method is unavailable,” alongside the existing not
available/found/supported patterns while retaining the -32600 requirement. Add a
matching test next to the Monad case verifying these messages use the scalar
fallback instead of being rethrown.

---

Nitpick comments:
In `@packages/state/src/actions/getAccountFromProvider.js`:
- Around line 14-23: Complete the JSDoc for both isMethodUnavailableError and
getAccountFromProvider by adding import-based, working `@example` blocks and
documenting propagated errors with `@throws`. Keep the existing descriptions and
behavior unchanged, and ensure each example reflects the actual function API.

In `@packages/state/src/actions/getAccountFromProvider.spec.ts`:
- Around line 79-296: The new helper and test callbacks lack explicit return
types. Update createRecordingTransport with its concrete object return type,
annotate methodCount as returning number, and annotate every async it callback
in the scalar fallback describe block with Promise<void>.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dfc2b737-5948-420a-b7e0-a2b0c0bb0fa4

📥 Commits

Reviewing files that changed from the base of the PR and between 6199d04 and cba1061.

📒 Files selected for processing (3)
  • .changeset/proud-otters-prove.md
  • packages/state/src/actions/getAccountFromProvider.js
  • packages/state/src/actions/getAccountFromProvider.spec.ts

Comment thread packages/state/src/actions/getAccountFromProvider.js Outdated
Fork account hydration previously required an empty-storageKeys
eth_getProof from the fork provider — the only RPC path for loading an
account's balance/nonce/codeHash/storageRoot. Chains that do not serve
eth_getProof (Monad mainnet, ZKsync OS, Moonbeam) therefore failed on
the first touch of any uncached account.

getAccountFromProvider now probes eth_getProof once per fork transport
and, on a method-unavailable error (-32601, -32004, or -32600 with a
'not available/found/supported' message, matched across the viem cause
chain), permanently downgrades that transport to three concurrent
scalar calls — eth_getBalance + eth_getTransactionCount + eth_getCode —
pinned to the same fork block. codeHash is computed locally via
keccak256(code); storageRoot defaults to the canonical empty trie root,
which EVM execution never reads (storage is fetched per-slot via
eth_getStorageAt) and which exactly satisfies getAccount's
nonexistent-account predicate. This is the same fork mechanism used by
Foundry, Hardhat/EDR, and Ganache. The fetched bytecode primes both
contract-code caches, so the fallback costs no extra round trips versus
the proof path once code is needed.

The capability flag is a module-level WeakMap keyed by the fork
transport object (reference-stable across state-manager deep/shallow
copies), so the downgrade survives the per-call VM clone in tevmCall.
All other errors rethrow unchanged; uncoded errors never trigger the
downgrade. The public eth_getProof action and light-client reads are
unaffected and continue to fail honestly on such chains.

Verified: full @tevm/state suite (157 passed) with coverage gates;
end-to-end smoke via createTevmNode against a deterministic
Monad-shaped mock (probe → fallback → sticky, one eth_getProof total)
and against live Monad mainnet (chainId 143): WMON codeHash ===
keccak256(eth_getCode), block-pinned balance parity with direct RPC,
nonexistent account → AccountNotFound.
@ebramanti
ebramanti force-pushed the fix/eth-getproof-scalar-fallback branch from cba1061 to 26332f6 Compare August 5, 2026 04:58
@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 26332f6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tevm/state Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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.

1 participant