Skip to content

Commit e913d87

Browse files
benaadamsLukaszRozmejclaude
authored
perf(evm): tighten CALL hot path; fix EIP-7702 delegation-to-precompile semantics (#11547)
* perf(evm): tighten CALL hot path - delegation, gas clamp, allocation InstructionCall: discover the EIP-7702 delegation target inline with the initial GetCachedCodeInfo(followDelegation: false) call instead of doing a separate TryGetDelegation followed by a second GetCachedCodeInfo. The cold access charge for the delegated address still lands between the two reads so gas accounting is unchanged; one fewer state read in the common case. InstructionCall: replace UInt256.Min((UInt256)cap, gasLimit) plus the 256-bit >= long.MaxValue check plus (long)gasLimit with a scalar IsUint64 + u0 compare. The 63/64 cap is a non-negative long, so the clamped result is guaranteed to fit without any 256-bit math. Same numerical result; no 4-limb work on every CALL/CALLCODE/DELEGATECALL/ STATICCALL. ICodeInfoRepository.TryGetDelegatedAddress: drop the ToArray() and use the Address(ReadOnlySpan<byte>) constructor directly. Removes one byte[20] allocation per successful 7702 delegation parse. StateOverridesExtensions.UpdateCode: precompile check now passes followDelegation: false so the question matches what the override system is actually asking (is THIS account a precompile, not where it delegates). Tests: 480/480 pass in Nethermind.Evm.Test under filter Call|Gas|VM (2 pre-existing skips for issue #140). * refactor(evm): post-review cleanup pass on CALL hot path - Add GetCachedCodeInfoNoDelegation(repo, addr, spec) extension to ICodeInfoRepository so callers that explicitly do not want delegation resolution can drop the (followDelegation: false, ..., out _) ceremony. - Use the new extension at the second GetCachedCodeInfo call in InstructionCall and at StateOverridesExtensions.UpdateCode. - Add a one-line WHY comment above the second InstructionCall lookup explaining the EIP-7702 ordering constraint (delegated code must be loaded after charging cold-access gas for the delegation target). - Add Debug.Assert(gasAvailable >= 0) above the (ulong)cap cast in the 63/64 gas clamp; documents the invariant the cast relies on. * fix(evm): record delegated precompile address for EIP-7928 BAL When a 7702 EOA delegates to a precompile address and the PrecompileCachedCodeInfoRepository decorator is installed in the processing pipeline, the second GetCachedCodeInfoNoDelegation lookup in InstructionCall hits the decorator's cached precompile fast-path which returns without going through the world state. The BlockAccessList generated for EIP-7928 therefore omits the delegation target. The base CodeInfoRepository compensates by calling AddAccountRead on the precompile fast-path; the decorator does not, and other call sites that pass precompile addresses to GetCachedCodeInfo (EXTCODECOPY, EXTCODESIZE) already do AddAccountRead at the call site. Add the matching call before the second InstructionCall lookup. Pre-this-branch the read was incidentally captured because the followDelegation: true path routed through InternalGetCodeInfo -> GetCodeHash -> AddAccountRead. The CALL hot-path refactor on this branch switched to followDelegation: false + an explicit second fetch, which bypasses that chain when the target is a precompile. AddAccountRead on IWorldState is a default-interface no-op for plain world states and idempotent on TracedAccessWorldState (BlockAccessList guards with ContainsKey), so the call is safe in all configurations. Adds a regression test wiring PrecompileCachedCodeInfoRepository into the Eip7928Tests fixture and asserting the delegated Sha256 precompile address appears in the generated BAL. Verified: test FAILS without the fix (2/2 across both parallel modes), PASSES with the fix. * fix * test(evm): regression for EIP-7702 delegation-to-precompile FastCall Adds Calling_account_delegated_to_precompile_uses_FastCall_per_EIP_7702 - a unit-level regression test that specifically catches the spec violation fixed in 3b82033. Uses 0 inner gas to discriminate: real precompile execution OOGs and pushes 0; FastCall pushes 1 regardless of forwarded gas. Verified: test fails 2/2 (both parallel modes) without the spec fix, passes 2/2 with it. Also drops the now-stale "schedule background analysis if needed" comment above the codeSource GetCachedCodeInfo call (PR feedback): the phrase described the previous single-call followDelegation:true pattern and is misleading after the followDelegation:false split. The named argument followDelegation:false is self-documenting. * docs(evm): tighten comments per AGENTS.md WHY-only rule Three multi-line comments around the InstructionCall delegation block and two test XML <remarks> blocks were paragraph-style explanations. AGENTS.md asks for one-liners that capture the WHY (the EIP, the non-obvious choice) and nothing more. Compressed each to one or two lines while preserving the EIP citations that explain the corner case. Net -23 lines. * fix(evm): record code reads in BAL on PrecompileCachedCodeInfoRepository fast-path The decorator's IsPrecompile fast-path returned the cached precompile CodeInfo without calling AddAccountRead, breaking the contract that the base CodeInfoRepository upholds at CodeInfoRepository.cs:48 (where the precompile case explicitly records the read for EIP-7928 BAL inclusion). For most CALL-family opcodes the gap was masked because target == codeSource and the subsequent state.AccountExists(target) records indirectly via TracedAccessWorldState. But DELEGATECALL/CALLCODE set target = ExecutingAccount (not codeSource), so when codeSource is a precompile and the decorator's fast-path fires, the precompile address is missing from the generated BAL. Inject IWorldState into the decorator and add worldState.AddAccountRead on the fast-path; mirrors the base impl. Safe in all configurations: no-op default- interface impl on plain world states, idempotent on TracedAccessWorldState. Adds two regression tests in Eip7928Tests: - DelegateCall_to_precompile_records_codeSource_in_BAL_*: catches the real gap. Verified failing 2/2 without the fix, passing 2/2 with it. - Direct_transaction_to_precompile_records_recipient_in_BAL_*: documents that tx.to == precompile already records the recipient correctly via an existing TransactionProcessor.Execute path (passing 2/2 both before and after; not a gap, just a positive invariant). Updates 14 decorator-construction sites in PrecompileCachedCodeInfoRepository unit tests to pass Substitute.For<IWorldState>(); they assert caching, not recording. pyspec test_pointer_to_precompile: 1068/1068 pass before and after. Nethermind.Evm.Test (Call|Gas|VM|Eip7928|Eip7702): 622/622 pass. PrecompileCachedCodeInfoRepository unit tests: 14/14 pass. * test(evm): de-duplicate Eip7928Tests helpers Per AGENTS.md test guidelines (factor shared setup into helpers): - Merge InitWorldStateWithPrecompileDelegation into InitWorldState via a new optional delegationTarget parameter. Single helper now handles both the AddressD->code intermediate-hop case and direct delegation to e.g. a precompile. - Merge CreateTracedProcessorWithPrecompileCache into CreateTracedProcessor via a wrapPrecompileCache flag. - Extract BuildContractTx for the recurring templateTx + IntrinsicGasCalculator + signed-tx dance. Applied to all five call sites (the four new precompile tests plus the two pre-existing Constructs_BAL_when_processing_code* tests). No behavior change; all 142 Eip7928 tests (parallel=false and parallel=true fixtures) still pass. Net -83 lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(evm): drop XML docs from dedupe helpers to match file style Surrounding test helpers in Eip7928Tests.cs (and the rest of the file) have no /// blocks — names self-document. Removes the doc additions from the prior dedupe commit (BuildContractTx, InitWorldState, and the wrapPrecompileCache <remarks> on CreateTracedProcessor). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 67c0da6 commit e913d87

7 files changed

Lines changed: 229 additions & 61 deletions

File tree

src/Nethermind/Nethermind.Blockchain.Test/PrecompileCachedCodeInfoRepositoryTests.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void Precompile_WithCachingEnabled_IsWrappedInCachedPrecompile()
5151
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
5252

5353
// Act
54-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
54+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
5555
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
5656

5757
// Assert
@@ -81,7 +81,7 @@ public void Precompile_WithCachingDisabled_IsNotWrapped()
8181
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
8282

8383
// Act
84-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
84+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
8585
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
8686

8787
// Assert
@@ -107,7 +107,7 @@ public void IdentityPrecompile_IsNotWrapped_WhenCacheEnabled()
107107
IReleaseSpec spec = CreateSpecWithPrecompile(IdentityPrecompile.Address);
108108

109109
// Act
110-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
110+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
111111
CodeInfo codeInfo = repository.GetCachedCodeInfo(IdentityPrecompile.Address, false, spec, out _);
112112

113113
// Assert
@@ -136,7 +136,7 @@ public void CachedPrecompile_CachesResults_ForCachingEnabledPrecompile()
136136

137137
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
138138

139-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
139+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
140140
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
141141

142142
byte[] input = [1, 2, 3];
@@ -171,7 +171,7 @@ public void NonCachingPrecompile_DoesNotCacheResults()
171171

172172
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
173173

174-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
174+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
175175
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
176176

177177
byte[] input = [1, 2, 3];
@@ -205,7 +205,7 @@ public void NullCache_DoesNotWrapAnyPrecompiles()
205205
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
206206

207207
// Act - pass null cache
208-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, null);
208+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, null);
209209
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
210210

211211
// Assert - precompile should not be wrapped
@@ -231,7 +231,7 @@ public void Sha256Precompile_IsWrapped_WhenCacheEnabled()
231231
IReleaseSpec spec = CreateSpecWithPrecompile(Sha256Precompile.Address);
232232

233233
// Act
234-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
234+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
235235
CodeInfo codeInfo = repository.GetCachedCodeInfo(Sha256Precompile.Address, false, spec, out _);
236236

237237
// Assert - Sha256Precompile should be wrapped (unlike IdentityPrecompile)
@@ -264,7 +264,7 @@ public void MixedPrecompiles_OnlyCachingEnabledAreWrapped()
264264
}.ToFrozenSet());
265265

266266
// Act
267-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
267+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
268268
CodeInfo sha256CodeInfo = repository.GetCachedCodeInfo(Sha256Precompile.Address, false, spec, out _);
269269
CodeInfo identityCodeInfo = repository.GetCachedCodeInfo(IdentityPrecompile.Address, false, spec, out _);
270270

@@ -296,7 +296,7 @@ public void CachedPrecompile_DifferentInputs_CreateSeparateCacheEntries()
296296

297297
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
298298

299-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
299+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
300300
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
301301

302302
byte[] input1 = [1, 2, 3];
@@ -335,7 +335,7 @@ public void CachedPrecompile_ReturnsCachedResult_OnCacheHit()
335335

336336
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
337337

338-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
338+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
339339
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
340340

341341
byte[] input = [1, 2, 3];
@@ -369,7 +369,7 @@ public void Sha256Precompile_CachesResults_WithRealComputation()
369369

370370
IReleaseSpec spec = CreateSpecWithPrecompile(Sha256Precompile.Address);
371371

372-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
372+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
373373
CodeInfo codeInfo = repository.GetCachedCodeInfo(Sha256Precompile.Address, false, spec, out _);
374374

375375
byte[] input = [1, 2, 3, 4, 5];
@@ -402,7 +402,7 @@ public void IdentityPrecompile_DoesNotCache_WithRealComputation()
402402

403403
IReleaseSpec spec = CreateSpecWithPrecompile(IdentityPrecompile.Address);
404404

405-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
405+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
406406
CodeInfo codeInfo = repository.GetCachedCodeInfo(IdentityPrecompile.Address, false, spec, out _);
407407

408408
byte[] input = [1, 2, 3, 4, 5];
@@ -439,7 +439,7 @@ public void CachedPrecompile_WithNormalizeInputOverride_DeduplicatesOversizedInp
439439

440440
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
441441

442-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
442+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
443443
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
444444

445445
// Same first 4 bytes, different suffixes — both calls should map to the same cache key.
@@ -477,7 +477,7 @@ public void CachedPrecompile_DoesNotCache_InvalidLengthResults()
477477

478478
IReleaseSpec spec = CreateSpecWithPrecompile(precompileAddress);
479479

480-
PrecompileCachedCodeInfoRepository repository = new(precompileProvider, baseRepository, cache);
480+
PrecompileCachedCodeInfoRepository repository = new(Substitute.For<IWorldState>(), precompileProvider, baseRepository, cache);
481481
CodeInfo codeInfo = repository.GetCachedCodeInfo(precompileAddress, false, spec, out _);
482482

483483
byte[] input1 = [1, 2, 3]; // length 3, not 4

src/Nethermind/Nethermind.Blockchain/PrecompileCachedCodeInfoRepository.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
namespace Nethermind.Blockchain;
1717

1818
public class PrecompileCachedCodeInfoRepository(
19+
IWorldState worldState,
1920
IPrecompileProvider precompileProvider,
2021
ICodeInfoRepository baseCodeInfoRepository,
2122
ConcurrentDictionary<PreBlockCaches.PrecompileCacheKey, Result<byte[]>>? precompileCache) : ICodeInfoRepository
@@ -29,6 +30,8 @@ public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IRe
2930
{
3031
if (vmSpec.IsPrecompile(codeSource) && _cachedPrecompile.TryGetValue(codeSource, out CodeInfo cachedCodeInfo))
3132
{
33+
// EIP-7928: mirror base CodeInfoRepository.GetCachedCodeInfo precompile path so the read lands in the BAL.
34+
worldState.AddAccountRead(codeSource);
3235
delegationAddress = null;
3336
return cachedCodeInfo;
3437
}

0 commit comments

Comments
 (0)