eth_call - perf improvements - #11965
Conversation
|
Claude finished @svlachakis's task in 8m 33s —— View job PR Review: EVM StreamTasks
SummaryThis PR introduces a preprocessed instruction stream interpreter for the EVM: a first pass analyzes bytecode into basic blocks with summed static gas and pre-decoded PUSH immediates; a second pass executes the stream with block-level gas charging, gas-free in-block cores, fused PUSH+op pairs, and a metered fallback for low-gas or mid-block landings. It also adds a compile-time spec fingerprinting system ( The architecture is sound and the safety invariants (JUMPDEST as solo block, pcToEntry invalidation inside fused pairs, fingerprint-guarded engagement) are well-reasoned. The differential tests and FindingsHigh1. Diagnostic code must be removed before merge —
2. The addition of Medium3. Magic number
4. A test thread flipping Low5.
6. Prague has the same 7. PR description is empty No changes listed, no type-of-change checkboxes ticked, no testing notes. This is required by the PR template (it drives automatic labeling and release notes) and should be filled in before merge. 8. No EF consensus test run with stream enabled The differential unit tests are synthetic. Running the Ethereum Foundation state test suite with Correctness AssessmentThe core gas accounting is correct: block gas is the exact sum of individual in-block instruction costs, so whether charged once at |
|
Claude finished @svlachakis's task in 2m 31s —— View job PR Review: EVM Stream (Tenth Pass — focused on benaadams' findings)Re-reviewed against benaadams' comment. All of his findings have been addressed in the current branch HEAD ( benaadams findings — status
High — out-of-gas detection (verified resolved)All five sites benaadams listed now correctly use
This matches the canonical pattern:
Regression test added (
Both assert the stream engaged ( Medium — fork-gating comment
One last stale sentence remains — the Low nits — all resolved
Verdict: Mergeable ✓benaadams' High consensus-divergence finding is fixed at all five sites and covered by a new regression test; his Medium and Low items are resolved. The only outstanding item is a one-line stale doc-comment (Low) which does not block merge. |
Findings
Optimization Notes
@claude verify these points |
|
Claude finished @benaadams's task in 2m 15s —— View job Verification of @benaadams' findingsI read the current branch HEAD and checked each point against the code.
1. Critical — StaticJump/StaticJumpI skip the PUSH2 overflow check ✅ ConfirmedThe unfused/dispatch path deliberately preserves the overflow a real // Head < MaxStackSize - 1 preserves the StackOverflow a non-fused PUSH2 would raise
// at head == 1024 (even though the following JUMP/JUMPI would immediately pop it).
stack.Head < EvmStack.MaxStackSize - 1 && ...When The stream's fused handlers ( case (Instruction)FusedOpcode.StaticJump:
TGasPolicy.Consume(ref gas, GasCostOf.VeryLow + GasCostOf.Jump);
if (TGasPolicy.IsOutOfGas(in gas)) { ... }
opCodeCount++;
entryIndex = (int)entry.Operand - 1; // jumps — never checks Head == 1024
break;
case (Instruction)FusedOpcode.StaticJumpI:
TGasPolicy.Consume(ref gas, GasCostOf.VeryLow + GasCostOf.JumpI);
...So bytecode that fills the stack to 1024 items and then runs Fix — before consuming jump gas, mirror the case (Instruction)FusedOpcode.StaticJump:
if (stack.Head >= EvmStack.MaxStackSize - 1) { exceptionType = EvmExceptionType.StackOverflow; break; }
TGasPolicy.Consume(ref gas, GasCostOf.VeryLow + GasCostOf.Jump);
...The order matters: the real 2. Medium — public mutable stream API ✅ Confirmed
The cleaner posture is 3. Low — benchmark leaks the world-state scope ✅ Confirmed
Optimization notes
Summary: All three findings and both optimization notes are valid. Finding #1 is a genuine consensus-style divergence on the cancelable RPC path (success where the spec requires |
|
@claude review again all @benaadams 's comments |
|
Claude finished @svlachakis's task in 1m 37s —— View job Re-review of all @benaadams' findings — current HEAD
|
| # | Finding | Severity | Status |
|---|---|---|---|
| 1 | StaticJump/StaticJumpI skip the PUSH2 stack-overflow check | Critical | ✅ Fixed |
| 2 | Stream impl exposed as public mutable API | Medium | ✅ Fixed |
| 3 | Benchmark leaks the world-state scope | Low | ✅ Fixed |
| A | FramesExecuted Interlocked.Increment per frame |
opt | ✅ Addressed |
| B | ConstantBytes always allocated |
opt | ✅ Fixed |
#1 — Critical (fixed + tested). Both fused cases now mirror InstructionPush2 and fail with StackOverflow at a full stack before consuming jump gas:
VirtualMachine.Stream.cs:256—StaticJump:if (stack.Head >= EvmStack.MaxStackSize - 1) { exceptionType = StackOverflow; break; }VirtualMachine.Stream.cs:276—StaticJumpI: same guard.
The exact differential tests benaadams asked for are present: FullStackStaticJumpOverflows and FullStackStaticJumpIOverflows (InstructionStreamTests.cs:338-339) build 1024×PUSH0; PUSH2 dest; JUMP/JUMPI (BuildFullStackJump, :282-287) and assert the stream result matches the bytecode loop. Confirmed real consensus-path divergence is closed.
#2 — Medium (fixed). The stream types are no longer public surface:
StreamInterpreteris nowinternal static class(:13) — theEnabled/ForceAllContexts/BuildThresholdprocess-wide switches are no longer externally reachable.InstructionStreamisinternal sealed(InstructionStream.cs:111);StreamOp/StreamOpKind/FusedOpcodeare allinternal.CodeInfo.GetOrBuildStream()is nowinternal(CodeInfo.cs:73).
In-process plugins can no longer mutate cached stream arrays or flip the global switches.
#3 — Low (fixed). MultipleUnsignedOperations now stores the scope in _stateScope (:34, assigned :74) and disposes it in GlobalCleanup (:102), matching the new benchmarks.
Opt A (addressed). FramesExecuted is now [ThreadStatic] public static long with a plain ++ (:32, :52) — no Interlocked on the hot path, and thread-local so there is no race. Cheaper than the per-frame atomic and resolves the original non-atomicity caveat too.
Opt B (fixed). ConstantBytes is now built only when bitwise/EQ fusion was emitted — the constructor takes buildConstantBytes and assigns ConstantBytes = [] otherwise (InstructionStream.cs:134-145). Arithmetic/shift-only streams no longer carry the big-endian copy.
Batch 1 (2026-06-26 03:14) — re-confirmed still fixed
| Finding | Severity | Status |
|---|---|---|
Out-of-gas never detected (ulong < 0 always false) |
High | ✅ All five sites use TGasPolicy.IsOutOfGas (Stream.cs:262, 282, 333, 347, 485) |
| Fork-gating comment contradicts gate | Medium | ✅ InstructionStream.cs:102-109 remarks corrected |
PUSH9+ excluded comment inaccurate |
Low | ✅ Fixed |
InstructionStreamCache memory note |
Low | ✅ Added |
StaticJump transiently-stale programCounter note |
Low | ✅ Added (Stream.cs:270) |
OOG regression tests OutOfGasOnBoundarySLoad and OutOfGasInMeteredFallback are present (InstructionStreamTests.cs:416, 423).
One remaining nit (Low, doc-only — does not block)
The FusedOpcode doc-comment still references the removed system: "The fingerprint gate keeps new forks ... off the stream until reviewed" (InstructionStream.cs:28-29). There is no fingerprint gate — the real gate is spec.IncludePush0Instruction (any fork ≥ Shanghai), already correctly documented on the InstructionStream remarks just below. This is the last stale reference; I flagged it inline previously. Documentation-only.
Verdict
All of @benaadams' Critical / High / Medium findings (and both optimization notes) are resolved on 913eb6b, the Critical #1 backed by the exact differential tests requested. The only outstanding item is a one-line stale doc-comment (Low). Mergeable from the standpoint of his review.
• branch evm-stream
negative-overflow trick for OOG detection — so every Consume now guards the subtraction with a bounds branch. The stream block precharge already proves remaining >= cost one line above, making that branch redundant on the hottest per-block charge. ConsumeUnchecked does the branchless `Value -= cost` only where the caller has verified sufficiency; checked Consume stays everywhere else. ulong API and OOG semantics unchanged (differential suite 24/24, incl. out-of-gas cases).
|
@claude review again |
|
Claude finished @svlachakis's task in 2m 28s —— View job PR Review: EVM Stream (Eleventh Pass)Focused on the commits added since @benaadams' approval (
New commits since approval —
|
| Site | VirtualMachine.Stream.cs |
Guard |
|---|---|---|
| Per-block precharge | :85 |
if (GetRemainingGas >= cost) one line above (:81) |
Fused StaticJump |
:270 |
if (GetRemainingGas < jumpCost) → SetOutOfGas; goto OutOfGas (:264) |
Fused StaticJumpI |
:292 |
if (GetRemainingGas < jumpiCost) → SetOutOfGas; goto OutOfGas (:286) |
EthereumGasPolicy.ConsumeUnchecked (:124) is the raw gas.Value -= cost with no clamp and no OutOfGas flag. In all three cases sufficiency (Value >= cost) is proven immediately before, so the unsigned subtraction cannot wrap. The block-precharge set is arithmetic/stack/push/comparison/bitwise/shift only — none carry EIP-8037 state gas — so charging Value alone is complete and correct. The interface default (IGasPolicy.cs:49) routes ConsumeUnchecked back to the safe Consume for any non-EthereumGasPolicy implementation, so the optimization is opt-in per policy. ✅
The < cost → OutOfGas branches at :264/:286 correctly preserve benaadams' batch-1 finding (no ulong < 0 dead checks remain): the metered fallback (:94) and the boundary/MSTORE paths all gate on IsOutOfGas, not a signed-negative trick.
Prior High/Medium findings — all still resolved
- StaticJump/StaticJumpI PUSH2 stack-overflow (benaadams Critical) — both fused cases guard
stack.Head >= MaxStackSize - 1 → StackOverflowbefore charging jump gas (:258,:280). ✅ - OOG never detected (
ulong < 0) — all sites useIsOutOfGas. ✅ - Public mutable stream API —
StreamInterpreter,InstructionStream,StreamOp/FusedOpkind/FusedOpcode, andCodeInfo.GetOrBuildStream()areinternal. ✅ - State.Flat bundling (asdacap) — split to Flat - Persistence Cache #12035/SLOAD Improvements #12043; no State.Flat files in this PR. ✅
- PUSH-immediate
0x5Bjump target —TryBuildreturnsnull; regression test present. ✅ - Benchmark scope leak,
FramesExecuted([ThreadStatic]),ConstantByteslazy alloc, stale fork-gate comments — all addressed.
One informational note (non-blocking)
Gas-vs-overflow ordering in fused static jumps — VirtualMachine.Stream.cs:258 / :280
The fused path checks stack overflow then gas; the real InstructionPush2 (EvmInstructions.Stack.cs:126) charges VeryLow first, then overflows on the fall-through push. So in the corner case of a full stack (1024) AND insufficient gas for the push, the unfused path terminates as OutOfGas (the loop's IsOutOfGas check wins over the returned StackOverflow) while the fused path returns StackOverflow.
This is not an observable divergence: the stream runs only on non-tracing cancelable RPC frames, both outcomes are exceptional halts that consume all remaining gas, and both yield StatusCode = failure with identical GasSpent. eth_call/estimateGas cannot distinguish them. Noting only for completeness — does not block, and the differential tests (which compare status + gas) correctly pass.
Verdict: Mergeable ✓
The new ConsumeUnchecked commits are correct and safely guarded. All Critical/High/Medium findings from all prior passes (including both of benaadams' batches) remain resolved, and benaadams has approved. The single new observation is informational and not observable on the gated RPC path.
• branch evm-stream
|
I still don't like the added maintenance cost |
|
Running era mainnet replay, just in case... |
|
Looks like it has regressed in terms of |
|
Nevermind, not apples to apples because machine is different. Let's get it merged. @asdacap did it passed your test? |
|
@claude review one last time the changes by this PR deeply and confirm there are no regressions in block processing or RPC path. |
|
Claude finished @svlachakis's task in 3m 8s —— View job PR Review: EVM Stream — final regression-focused passConfirming the two questions you asked: block-processing regressions and RPC-path regressions. I re-read the shared dispatch core, the gas policy, the stream interpreter, the fused cores, and the
Block processing — no regression ✅Block processing is structurally excluded from every new code path, enforced by a compile-time flag, not a runtime check:
The only block-processing-visible change is the inline- RPC / cancelable path — no regression ✅All consensus-divergence risks raised across the 11 prior passes + @benaadams' two batches are fixed in the current code:
Gas correctness spot-checks hold: block gas ( The shared One residual (already acknowledged, not a found defect)Plugin RPC paths (OP/Taiko) are unverified, per the author. Block processing on these chains is unaffected (same cancelable gate). On their RPC path, if the tracer is cancelable and the fork is Shanghai+, the stream will engage with the plugin's VerdictNo regressions found in block processing or the RPC path. Block processing is provably excluded at compile time; the RPC path is differential-tested and every prior Critical/High/Medium finding is resolved in the current code. Both @benaadams and @LukaszRozmej have approved. @asdacap's era-mainnet replay (in progress) is the right final gate before merge — that directly exercises the block-processing path on real history. |
Name / Identifier Stavros Vlachakis Team / Project Nethermind Start date of relevant projects July 2025 (part-time) February 2026 (full-time) Proposed weight Full (1.0) Summary of work / eligibility Stavros joined Nethermind in March 2025. Since July 2025, he has contributed part-time to the Nethermind Ethereum Execution Client (Core team) alongside other responsibilities. Since February 2026, he has worked full-time on Nethermind Client, with 100+ merged PRs in total. He owns the JSON-RPC for Nethermind Client. He is also expected to contribute extensively to Frame Transactions on Hegota. Representative work: - EIP-4444 history expiry (EraE): implemented the EraE archive format end-to-end — era export/import and remote download with SHA-256 verification. (#10812 (NethermindEth/nethermind#10812)) - JSON-RPC (owner): broad spec-compliance and Geth-parity work plus new endpoints — e.g. Geth-compatible error codes (#11335 (NethermindEth/nethermind#11335)) and eth_signTransaction / raw-transaction methods (#11517 (NethermindEth/nethermind#11517), #11521 (NethermindEth/nethermind#11521)). - Streaming for large RPC responses: streaming approach for trace_* and debug_* results, avoiding buffering huge responses in memory. (#11755 (NethermindEth/nethermind#11755), #11693 (NethermindEth/nethermind#11693)). - EVM & execution performance: eth_call interpreter/dispatch optimizations (#11965 (NethermindEth/nethermind#11965)), EVM memory pooling and SLOAD / flat-state read caching (#11991 (NethermindEth/nethermind#11991), #12043 (NethermindEth/nethermind#12043)). - State & consensus reliability: correct persisted-code tracking in the code DB (#11714 (NethermindEth/nethermind#11714)), forkchoice canonical-chain corruption healing after beacon sync (#10876 (NethermindEth/nethermind#10876)). All merged PRs: https://github.com/NethermindEth/nethermind/pulls?q=is%3Apr+author%3Asvlachakis+is%3Aclosed (edited)
Summary
A set of EVM optimizations targeting
eth_calland the rest of the cancelable RPC call/simulation surface (estimateGas,createAccessList,simulate).Behaviour-preserving — differential-tested to produce identical gas and output — and scoped to cancelable frames, so block processing is unaffected. No consensus change.
1. Instruction-stream interpreter — on by default, scoped to cancelable RPC frames
Runs hot, repeatedly-executed bytecode over a preprocessed stream instead of the raw loop:
PUSHimmediates.PUSH+oppairs.PUSH2+JUMP/JUMPIresolved to a target entry.Engages only on cancelable (
eth_call/estimateGas/createAccessList/simulate), non-tracing, tip-fork frames.Block processing runs the unchanged dispatch and never builds or runs a stream.
Built async on the thread pool (a dedicated
IThreadPoolWorkItem) after a fixed threshold of 4 executions; lock-free publish; falls back to a metered raw-code loop so gas and failure semantics stay exact.~7–8% over the bytecode loop on the heavy
eth_callcorpus (more under concurrent load).2. Per-fork specialized dispatch
The fork's opcode-gating flags (
EIP-145shift opcodes,EIP-3855PUSH0) are lifted to compile-timeIFlaggeneric type args, so the hot dispatch loop (RunByteCodeCore) is specialized per tip fork and the JIT folds away the per-op spec branches.The direct-dispatch switch over hot opcodes runs on the cancelable path; block processing takes the plain function-pointer table (master-parity), avoiding the switch's I-cache cost on its diverse opcode mix.
int pcand handlers takeref pcinstead ofref programCounter.RunByteCodeCoreandRunStream.Isolated arithmetic microbenchmark:
3. Per-thread EVM memory pool
EvmPooledMemoryrents/returns its backing buffer from a per-thread pool, removing cross-threadArrayPoolcontention on the allocation-heavy memory path.#11991
4. Flat-state read caching
#12035
5. Keccak cache tweak
Adjustment to the set-associative
KeccakCacheon the hashing path.#12042
6. Per-contract storage memo & Skip cell hashing on read-mostly executions
(
PersistentStorageProvider).** Consecutive SLOADs overwhelmingly hit the same contract; a one-entry memo (_lastStorageAddress→_lastStorage) removes the_storagesdictionary lookup from the per-SLOAD path. Invalidated wherever_storagesis cleared, so the pooled per-contract state is never reachable after return.The intra-block cache only holds journaled writes. On read-mostly executions (e.g.
eth_call) it is empty, so a_intraBlockCache.Count != 0guard skips hashing the 52-byte storage cell on every SLOAD before the lookup that would miss anyway.#12043
At 60 RPS
At 65 RPS
Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?