Skip to content

Commit f3c0b27

Browse files
committed
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.
1 parent 0dd1f92 commit f3c0b27

4 files changed

Lines changed: 112 additions & 16 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
}

src/Nethermind/Nethermind.Evm.Test/Eip7928Tests.cs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,98 @@ public void Calling_account_delegated_to_precompile_uses_FastCall_per_EIP_7702()
302302
}
303303
}
304304

305+
/// <summary>
306+
/// EIP-7928 regression: DELEGATECALL to a precompile records the precompile (codeSource) in BAL.
307+
/// </summary>
308+
/// <remarks>
309+
/// For DELEGATECALL/CALLCODE, target == ExecutingAccount, so the indirect <c>AccountExists(target)</c> records
310+
/// the caller, not the precompile. The decorator's <c>IsPrecompile</c> fast-path otherwise skips AddAccountRead.
311+
/// </remarks>
312+
[Test]
313+
public void DelegateCall_to_precompile_records_codeSource_in_BAL_under_PrecompileCachedCodeInfoRepository()
314+
{
315+
Address precompileAddress = Sha256Precompile.Address;
316+
317+
InitWorldState(TestState);
318+
319+
(TracedAccessWorldState tracedState, TransactionProcessor<EthereumGasPolicy> processor) =
320+
CreateTracedProcessorWithPrecompileCache();
321+
322+
Block block = Build.A.Block.TestObject;
323+
324+
byte[] code = Prepare.EvmCode
325+
.DelegateCall(precompileAddress, 50_000)
326+
.Done;
327+
328+
Transaction templateTx = Build.A.Transaction
329+
.WithCode(code)
330+
.WithGasLimit(0)
331+
.WithValue(_testAccountBalance)
332+
.TestObject;
333+
long intrinsicGas = IntrinsicGasCalculator
334+
.Calculate(templateTx, Amsterdam.Instance, block.Header.GasLimit).MinimalGas;
335+
long gasLimit = intrinsicGas + _gasLimit;
336+
337+
Transaction tx = Build.A.Transaction
338+
.WithCode(code)
339+
.WithGasLimit(gasLimit)
340+
.WithValue(_testAccountBalance)
341+
.SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject;
342+
343+
processor.SetBlockExecutionContext(new BlockExecutionContext(block.Header, Amsterdam.Instance));
344+
TransactionResult res = processor.Execute(tx, NullTxTracer.Instance);
345+
346+
BlockAccessList bal = tracedState.GetGeneratingBlockAccessList();
347+
AccountChanges? precompileChanges = bal.GetAccountChanges(precompileAddress);
348+
349+
using (Assert.EnterMultipleScope())
350+
{
351+
Assert.That(res.TransactionExecuted, Is.True);
352+
Assert.That(precompileChanges, Is.Not.Null,
353+
"DELEGATECALL codeSource (precompile) must be recorded in BAL even when the decorator's fast-path is active");
354+
}
355+
}
356+
357+
/// <summary>
358+
/// EIP-7928 regression: top-level transaction with <c>tx.to == precompile_address</c> records the recipient in BAL.
359+
/// </summary>
360+
/// <remarks>
361+
/// <see cref="TransactionProcessor"/>'s <c>BuildExecutionEnvironment</c> only calls <c>accessTracker.WarmUp</c> (EIP-2929)
362+
/// on the recipient. With <c>tx.value == 0</c> there is no incidental <c>AddBalanceChange</c> to create the entry.
363+
/// </remarks>
364+
[Test]
365+
public void Direct_transaction_to_precompile_records_recipient_in_BAL_under_PrecompileCachedCodeInfoRepository()
366+
{
367+
Address precompileAddress = Sha256Precompile.Address;
368+
369+
InitWorldState(TestState);
370+
371+
(TracedAccessWorldState tracedState, TransactionProcessor<EthereumGasPolicy> processor) =
372+
CreateTracedProcessorWithPrecompileCache();
373+
374+
Block block = Build.A.Block.TestObject;
375+
376+
Transaction tx = Build.A.Transaction
377+
.To(precompileAddress)
378+
.WithData([1, 2, 3])
379+
.WithGasLimit(50_000)
380+
.WithValue(UInt256.Zero)
381+
.SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject;
382+
383+
processor.SetBlockExecutionContext(new BlockExecutionContext(block.Header, Amsterdam.Instance));
384+
TransactionResult res = processor.Execute(tx, NullTxTracer.Instance);
385+
386+
BlockAccessList bal = tracedState.GetGeneratingBlockAccessList();
387+
AccountChanges? precompileChanges = bal.GetAccountChanges(precompileAddress);
388+
389+
using (Assert.EnterMultipleScope())
390+
{
391+
Assert.That(res.TransactionExecuted, Is.True);
392+
Assert.That(precompileChanges, Is.Not.Null,
393+
"Top-level transaction recipient that is a precompile must be recorded in BAL");
394+
}
395+
}
396+
305397
private (TracedAccessWorldState tracedState, TransactionProcessor<EthereumGasPolicy> processor) CreateTracedProcessorWithPrecompileCache(bool? parallelOverride = null)
306398
{
307399
bool useParallel = parallelOverride ?? parallel;
@@ -311,7 +403,7 @@ public void Calling_account_delegated_to_precompile_uses_FastCall_per_EIP_7702()
311403
IBlockhashProvider blockhashProvider = new TestBlockhashProvider(SpecProvider);
312404
EthereumPrecompileProvider precompileProvider = new();
313405
EthereumCodeInfoRepository baseRepo = new(tracedState);
314-
PrecompileCachedCodeInfoRepository codeInfoRepo = new(precompileProvider, baseRepo, precompileCache: null);
406+
PrecompileCachedCodeInfoRepository codeInfoRepo = new(tracedState, precompileProvider, baseRepo, precompileCache: null);
315407
EthereumVirtualMachine machine = new(blockhashProvider, SpecProvider, logManager);
316408
TransactionProcessor<EthereumGasPolicy> processor = new(
317409
BlobBaseFeeCalculator.Instance, SpecProvider, tracedState, machine, codeInfoRepo, logManager, parallel: useParallel);

src/Nethermind/Nethermind.Init/Modules/PrewarmerModule.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ protected override void Load(ContainerBuilder builder) => builder
5858
IBlocksConfig blocksConfig = ctx.Resolve<IBlocksConfig>();
5959
PreBlockCaches preBlockCaches = ctx.Resolve<PreBlockCaches>();
6060
IPrecompileProvider precompileProvider = ctx.Resolve<IPrecompileProvider>();
61+
IWorldState worldState = ctx.Resolve<IWorldState>();
6162
// Note: The use of FrozenDictionary means that this cannot be used for other processing env also due to risk of memory leak.
62-
return new PrecompileCachedCodeInfoRepository(precompileProvider, originalCodeInfoRepository,
63+
return new PrecompileCachedCodeInfoRepository(worldState, precompileProvider, originalCodeInfoRepository,
6364
blocksConfig.CachePrecompilesOnBlockProcessing ? preBlockCaches?.PrecompileCache : null);
6465
});
6566
}

0 commit comments

Comments
 (0)