fix(simulate): return 0 for unresolvable BLOCKHASH in eth_simulateV1 instead of throwing#12037
fix(simulate): return 0 for unresolvable BLOCKHASH in eth_simulateV1 instead of throwing#12037kamilchodola wants to merge 3 commits into
Conversation
When a tx's BLOCKHASH targets an ancestor whose hash cannot be resolved in the simulate context, the shared BlockhashProvider throws InvalidDataException, failing the entire eth_simulateV1 request (-32603). eth_simulateV1 is best-effort, so the simulate-only decorator now returns null (the EVM out-of-window BLOCKHASH result) instead. Canonical block processing is unaffected — it always has the hash and never hits this.
|
Claude finished @kamilchodola's task in 2m 4s —— View job Review
The fix is correct and minimal. The root cause is clear: Medium — Missing regression testAGENTS.md mandates: "When fixing a bug, always add a regression test." No test was added. A unit test for
This is easy to write with a mock — the Low —
|
…ASH null-return Per AGENTS.md 'always add a regression test' (review on #12037): inner-throws->null, normal pass-through, and the best-known clamp path.
|
@claude re-review |
| ? blockhashProvider.GetBlockhash(blockTree.BestSuggestedHeader!, bestKnown, spec) | ||
| : blockhashProvider.GetBlockhash(currentBlock, number, spec); | ||
| } | ||
| catch (InvalidDataException) |
There was a problem hiding this comment.
We should inject blockhashProvider that does this internally instead of catching exceptions
There was a problem hiding this comment.
could handle this by adding a TryGetBlockHash method that returns nullable result
|
Claude finished @kamilchodola's task in 2m 54s —— View job Re-review
Progress since first review:
Remaining issues: Medium — Unresolved CHANGES_REQUESTED (architectural coupling)@LukaszRozmej's change request is still unaddressed. The The reviewer's preferred direction: the null-safe behaviour should live in an injected
Summary: The behavior fix is correct and the tests now exist. The only blocking item is @LukaszRozmej's architectural CHANGES_REQUESTED: the exception-swallowing should live in a purpose-built injectable provider, not as a catch clause in |
| ? blockhashProvider.GetBlockhash(blockTree.BestSuggestedHeader!, bestKnown, spec) | ||
| : blockhashProvider.GetBlockhash(currentBlock, number, spec); | ||
| } | ||
| catch (InvalidDataException) |
There was a problem hiding this comment.
Medium — Unresolved CHANGES_REQUESTED: exception catch couples to BlockhashProvider's internals
@LukaszRozmej's concern is valid and unaddressed. SimulateBlockhashProvider is consuming an exception that is thrown by a specific implementation detail of BlockhashProvider (line 52 of BlockhashProvider.cs):
: blockhashCache.GetHash(currentBlock, (int)depth)
?? throw new InvalidDataException("Hash cannot be found when executing BLOCKHASH operation")That throw is not part of IBlockhashProvider's contract — it's an internal invariant of BlockhashProvider. If the implementation changes (returns null directly, throws a different type, or the condition changes), this catch silently becomes a no-op.
The fix suggested by LukaszRozmej: inject a null-safe IBlockhashProvider into the simulate scope that returns null for cache misses rather than throwing. One clean path:
- In
SimulateReadOnlyBlocksProcessingEnvFactory, add a decorator beforeSimulateBlockhashProviderin the DI chain that catchesInvalidDataExceptionand returnsnull— keeping the exception-swallowing logic in a single dedicated type rather than spread across the decorator hierarchy. - Remove the
try/catchfromSimulateBlockhashProviderentirely.
OR: modify BlockhashProvider itself to return null for cache misses (treating them as "hash not available", semantically equivalent to out-of-window). This is the simpler option but changes canonical-processing behaviour that the current throw was presumably guarding.
| ? blockhashProvider.GetBlockhash(blockTree.BestSuggestedHeader!, bestKnown, spec) | ||
| : blockhashProvider.GetBlockhash(currentBlock, number, spec); | ||
| } | ||
| catch (InvalidDataException) |
There was a problem hiding this comment.
could handle this by adding a TryGetBlockHash method that returns nullable result
| } | ||
| catch (InvalidDataException) | ||
| { | ||
| return null; |
There was a problem hiding this comment.
would it be worth a if (_logger.IsTrace) _logger.Trace(...) here, like the existing out-of-window path does? Just so a real corruption case doesn't go completely silent
Summary
eth_simulateV1can run transactions whoseBLOCKHASHopcode targets an ancestor block whose hash can't be resolved in the simulate context. Today the sharedBlockhashProviderthrowsInvalidDataException("Hash cannot be found when executing BLOCKHASH operation"), which fails the entireeth_simulateV1request with-32603.eth_simulateV1is best-effort. Per EVM semantics,BLOCKHASHreturns 0 for out-of-window/unavailable blocks, so the simulate-only decorator (SimulateBlockhashProvider) now catches that case and returns null (→ the EVM pushes 0) instead of failing the whole request.Scope / safety
SimulateBlockhashProvider(the eth_simulateV1-only decorator). Canonical block processing is unaffected — it always has the hash available and never hits this path.Found while using
eth_simulateV1for benchmark cache-warming; it's an independent robustness improvement.