Skip to content

Commit a728e10

Browse files
manusw7svlachakisLukaszRozmejclaude
authored
fix(eth_createAccessList): preserve reverted-frame accesses and exclude precompiles (#11431)
* fix(eth_createAccessList): enable access list tracking for pre-Berlin blocks * fix(eth_createAccessList): wip Non-Journaled Side-Log on StackAccessTracker * refactor: remove outdated comments related to eth_createAccessList warmup logic * feat(eth_createAccessList): enhance CreateAccessList to exclude precompile addresses without storage keys * feat(BlockchainBridgeTests): add CreateAccessList test to filter precompile addresses with empty storage keys * refactor(eth_createAccessList): update access tracker and precompile address handling and update comments * feat(BlockchainBridgeTests): add tests for CreateAccessList to filter precompile addresses based on storage keys * refactor(StackAccessTracker): remove default parameter and update constructor for clarity * fix(StackAccessTracker): add space for consistency in constructor formatting * fix(eth_createAccessList): drop pre-Berlin scope * fix(eth_createAccessList): fixed missing access-list entries from reverted sub-frames by avoiding restore for traced accesses * fix(BlockchainBridge): include precompiled contracts in access list * chore(BlockchainBridge): fix typo * refactor(BlockchainBridge): optimize BuildAddressesToOptimize by removing LINQ and using pre-allocated arrays * refactor(AccessTxTracerTests): de-duplicate added tests * refactor(BlockchainBridge): preallocate address filter buffer as Span<Address> in ConvergeAccessList * fix(StackAccessTracker): add explicit constructor * refactor(BlockchainBridgeTests): de-duplicate CreateAccessList tests Collapse the two precompile-filter tests differing only by `optimize` into a single parameterized test, and extract the shared mock setup into a helper used by both tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fe12014 commit a728e10

5 files changed

Lines changed: 177 additions & 13 deletions

File tree

src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
using Nethermind.Blockchain.Tracing;
99
using Nethermind.Core;
1010
using Nethermind.Core.Collections;
11+
using Nethermind.Core.Eip2930;
1112
using Nethermind.Core.Specs;
1213
using Nethermind.Core.Test.Builders;
14+
using Nethermind.Evm.State;
1315
using Nethermind.Evm.TransactionProcessing;
1416
using Nethermind.Int256;
1517
using Nethermind.Specs;
@@ -109,6 +111,86 @@ public void ReportAccess_AddressAIsSetToOptimizedAndHasStorageCell_AccessListHas
109111
Assert.That(sut.AccessList.Select(static x => x.StorageKeys), Has.Exactly(1).Contains(new UInt256(1)));
110112
}
111113

114+
[Test]
115+
public void Reverted_call_target_address_is_still_captured_in_access_list()
116+
{
117+
// Code deployed at recipient: CALL AddressC, then REVERT
118+
byte[] code = Prepare.EvmCode
119+
.Call(TestItem.AddressC, 50000)
120+
.PushData(0)
121+
.PushData(0)
122+
.Op(Instruction.REVERT)
123+
.Done;
124+
125+
AccessList list = ExecuteRevertedFrameScenario(code);
126+
127+
list.Select(static t => t.Address).Should().Contain(TestItem.AddressC,
128+
because: "addresses accessed inside reverted frames must survive for eth_createAccessList");
129+
}
130+
131+
[Test]
132+
public void Reverted_sub_frame_sload_storage_key_is_still_captured_in_access_list()
133+
{
134+
// Code at AddressC: SLOAD slot 7 then REVERT
135+
byte[] addressCCode = Prepare.EvmCode
136+
.PushData(7)
137+
.Op(Instruction.SLOAD)
138+
.PushData(0)
139+
.PushData(0)
140+
.Op(Instruction.REVERT)
141+
.Done;
142+
143+
TestState.CreateAccount(TestItem.AddressC, 0);
144+
TestState.InsertCode(TestItem.AddressC, addressCCode, SpecProvider.GenesisSpec);
145+
TestState.Commit(SpecProvider.GenesisSpec);
146+
TestState.CommitTree(0);
147+
148+
// Recipient code: CALL AddressC then STOP
149+
byte[] recipientCode = Prepare.EvmCode
150+
.Call(TestItem.AddressC, 50000)
151+
.Op(Instruction.STOP)
152+
.Done;
153+
154+
AccessList list = ExecuteRevertedFrameScenario(recipientCode);
155+
156+
// AddressC slot 7 must appear despite the REVERT inside AddressC's sub-frame
157+
list.Should().ContainSingle(e => e.Address == TestItem.AddressC)
158+
.Which.StorageKeys.Should().Contain(new UInt256(7),
159+
because: "storage cells first accessed inside a reverted sub-frame must still be captured");
160+
}
161+
162+
[Test]
163+
public void Outer_committed_and_inner_reverted_call_both_captured_in_access_list()
164+
{
165+
TestState.CreateAccount(TestItem.AddressE, 0);
166+
TestState.InsertCode(TestItem.AddressE, Prepare.EvmCode.Op(Instruction.STOP).Done, SpecProvider.GenesisSpec);
167+
168+
// AddressC code: CALL AddressE then REVERT
169+
byte[] addressCCode = Prepare.EvmCode
170+
.Call(TestItem.AddressE, 20000)
171+
.PushData(0)
172+
.PushData(0)
173+
.Op(Instruction.REVERT)
174+
.Done;
175+
176+
TestState.CreateAccount(TestItem.AddressC, 0);
177+
TestState.InsertCode(TestItem.AddressC, addressCCode, SpecProvider.GenesisSpec);
178+
TestState.Commit(SpecProvider.GenesisSpec);
179+
TestState.CommitTree(0);
180+
181+
// Recipient code: CALL AddressC (succeeds at EVM level but AddressC reverts internally)
182+
byte[] recipientCode = Prepare.EvmCode
183+
.Call(TestItem.AddressC, 50000)
184+
.Op(Instruction.STOP)
185+
.Done;
186+
187+
AccessList list = ExecuteRevertedFrameScenario(recipientCode);
188+
189+
Address[] addresses = list.Select(static t => t.Address).ToArray();
190+
addresses.Should().Contain(TestItem.AddressC, because: "committed outer CALL target must be in access list");
191+
addresses.Should().Contain(TestItem.AddressE, because: "address accessed inside inner reverted frame must still be in access list");
192+
}
193+
112194
protected override ISpecProvider SpecProvider => new TestSpecProvider(Berlin.Instance);
113195

114196
protected (AccessTxTracer trace, Block block, Transaction transaction) ExecuteAndTraceAccessCall(SenderRecipientAndMiner addresses, params byte[] code)
@@ -118,5 +200,13 @@ public void ReportAccess_AddressAIsSetToOptimizedAndHasStorageCell_AccessListHas
118200
_processor.Execute(transaction, new BlockExecutionContext(block.Header, Spec), tracer);
119201
return (tracer, block, transaction);
120202
}
203+
204+
private AccessList ExecuteRevertedFrameScenario(byte[] recipientCode)
205+
{
206+
(Block block, Transaction tx) = PrepareTx(BlockNumber, 100000, recipientCode, SenderRecipientAndMiner.Default);
207+
AccessTxTracer tracer = new(SenderRecipientAndMiner.Default.Sender, SenderRecipientAndMiner.Default.Recipient, SenderRecipientAndMiner.Default.Miner);
208+
_processor.Execute(tx, new BlockExecutionContext(block.Header, Spec), tracer);
209+
return tracer.AccessList!;
210+
}
121211
}
122212
}

src/Nethermind/Nethermind.Evm/StackAccessTracker.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111

1212
namespace Nethermind.Evm;
1313

14-
public struct StackAccessTracker() : IDisposable
14+
public struct StackAccessTracker(bool isTracingAccess) : IDisposable
1515
{
16+
public StackAccessTracker() : this(false) { }
17+
1618
public readonly JournalSet<Address> AccessedAddresses => _trackingState.AccessedAddresses;
1719
public readonly JournalSet<StorageCell> AccessedStorageCells => _trackingState.AccessedStorageCells;
1820
public readonly JournalCollection<LogEntry> Logs => _trackingState.Logs;
1921
public readonly JournalSet<Address> DestroyList => _trackingState.DestroyList;
2022
public readonly HashSet<AddressAsKey> CreateList => _trackingState.CreateList;
2123

24+
private readonly bool _isTracingAccess = isTracingAccess;
2225
private TrackingState _trackingState = TrackingState.RentState();
2326

2427
private int _addressesSnapshots;
@@ -65,8 +68,13 @@ public void TakeSnapshot()
6568

6669
public readonly void Restore()
6770
{
68-
_trackingState.AccessedAddresses.Restore(_addressesSnapshots);
69-
_trackingState.AccessedStorageCells.Restore(_storageKeysSnapshots);
71+
// When tracing access, don't restore the access sets on sub-frame revert.
72+
// The generated list will pre-warm all touched addresses.
73+
if (!_isTracingAccess)
74+
{
75+
_trackingState.AccessedAddresses.Restore(_addressesSnapshots);
76+
_trackingState.AccessedStorageCells.Restore(_storageKeysSnapshots);
77+
}
7078
_trackingState.DestroyList.Restore(_destroyListSnapshots);
7179
_trackingState.Logs.Restore(_logsSnapshots);
7280
}

src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ private TransactionResult Execute(Transaction tx, ITxTracer tracer, ExecutionOpt
216216
if (commit) WorldState.Commit(spec, tracer.IsTracingState ? tracer : NullTxTracer.Instance, commitRoots: false);
217217

218218
// substate.Logs contains a reference to accessTracker.Logs so we can't Dispose until end of the method
219-
using StackAccessTracker accessTracker = new();
219+
using StackAccessTracker accessTracker = new(tracer.IsTracingAccess);
220220

221221
int delegationRefunds = !spec.IsEip7702Enabled || !tx.HasAuthorizationList ? 0 : ProcessDelegations(tx, spec, accessTracker);
222222

src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
using Nethermind.Facade.Proxy.Models.Simulate;
2929
using Nethermind.Facade.Simulate;
3030
using Nethermind.Core.Specs;
31+
using Nethermind.Core.Precompiles;
3132
using Nethermind.State;
3233

3334
namespace Nethermind.Facade.Test;
@@ -335,6 +336,55 @@ public void BlobBaseFee_is_set_for_non_blob_transaction([ValueSource(nameof(Brid
335336
Arg.Is<BlockExecutionContext>(blkCtx => blkCtx.BlobBaseFee == expectedBlobBaseFeeHash));
336337
}
337338

339+
[TestCase(true)]
340+
[TestCase(false)]
341+
public void CreateAccessList_filters_precompile_addresses_with_empty_storage_keys(bool optimize)
342+
{
343+
CallOutput callOutput = InvokeCreateAccessListWithMockedAccess(
344+
optimize,
345+
[PrecompiledAddresses.ECRecover, TestItem.AddressC],
346+
[new StorageCell(TestItem.AddressC, UInt256.One)]);
347+
348+
callOutput.AccessList.Should().NotBeNull();
349+
callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeFalse();
350+
callOutput.AccessList.Any(e => e.Address == TestItem.AddressC).Should().BeTrue();
351+
}
352+
353+
[Test]
354+
public void CreateAccessList_keeps_precompile_address_when_storage_key_is_present()
355+
{
356+
CallOutput callOutput = InvokeCreateAccessListWithMockedAccess(
357+
optimize: false,
358+
[PrecompiledAddresses.ECRecover],
359+
[new StorageCell(PrecompiledAddresses.ECRecover, UInt256.One)]);
360+
361+
callOutput.AccessList.Should().NotBeNull();
362+
callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeTrue();
363+
}
364+
365+
private CallOutput InvokeCreateAccessListWithMockedAccess(
366+
bool optimize,
367+
Address[] accessedAddresses,
368+
StorageCell[] accessedCells)
369+
{
370+
BlockHeader header = Build.A.BlockHeader.TestObject;
371+
Transaction tx = Build.A.Transaction
372+
.WithSenderAddress(TestItem.AddressA)
373+
.WithTo(TestItem.AddressB)
374+
.TestObject;
375+
376+
_transactionProcessor.CallAndRestore(Arg.Any<Transaction>(), Arg.Any<ITxTracer>())
377+
.Returns(callInfo =>
378+
{
379+
ITxTracer tracer = callInfo.ArgAt<ITxTracer>(1);
380+
tracer.ReportAccess(accessedAddresses, accessedCells);
381+
tracer.MarkAsSuccess(TestItem.AddressB, new GasConsumed(21000, 0), Array.Empty<byte>(), Array.Empty<LogEntry>());
382+
return TransactionResult.Ok;
383+
});
384+
385+
return _blockchainBridge.CreateAccessList(header, tx, null, optimize, null, default);
386+
}
387+
338388
[Test]
339389
public void Call_tx_returns_InsufficientSenderBalanceError()
340390
{

src/Nethermind/Nethermind.Facade/BlockchainBridge.cs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: LGPL-3.0-only
33

44
using System;
5+
using System.Collections.Frozen;
56
using System.Collections.Generic;
67
using System.Diagnostics.CodeAnalysis;
78
using Nethermind.Blockchain;
@@ -244,9 +245,13 @@ private CallOutput ConvergeAccessList(BlockProcessingComponents components, Bloc
244245
{
245246
// Loop-invariant: the addresses to filter from the discovered AL depend only on header
246247
// and tx, neither of which change between iterations. Compute once and reuse.
247-
Address[] addressesToOptimize = BuildAddressesToOptimize(header, tx, optimize);
248+
FrozenSet<AddressAsKey> precompiles = specProvider.GetSpec(header).Precompiles;
249+
int bufferSize = (optimize ? 3 : 1) + precompiles.Count;
250+
Address[] addressBuffer = new Address[bufferSize];
251+
FillAddressesToOptimize(addressBuffer, header, tx, optimize, precompiles);
252+
248253
AccessList? previousAccessList = tx.AccessList;
249-
AccessTxTracer accessTracer = new(addressesToOptimize);
254+
AccessTxTracer accessTracer = new(addressBuffer);
250255
CallOutputTracer outputTracer = new();
251256
CancellationTxTracer tracer = new CompositeTxTracer(outputTracer, accessTracer).WithCancellation(cancellationToken);
252257
TransactionResult result;
@@ -273,16 +278,27 @@ private CallOutput ConvergeAccessList(BlockProcessingComponents components, Bloc
273278
};
274279
}
275280

276-
private Address[] BuildAddressesToOptimize(BlockHeader header, Transaction tx, bool optimize)
281+
private void FillAddressesToOptimize(Span<Address> buffer, BlockHeader header, Transaction tx, bool optimize, FrozenSet<AddressAsKey> precompiles)
277282
{
283+
int idx;
278284
if (!optimize)
279-
return [header.GasBeneficiary];
285+
{
286+
buffer[0] = header.GasBeneficiary;
287+
idx = 1;
288+
}
289+
else
290+
{
291+
// EIP-2930: sender, recipient and gas beneficiary are implicitly accessed,
292+
// so excluding them keeps the returned access list minimal.
293+
UInt256 senderNonce = tx.IsContractCreation ? stateReader.GetNonce(header, tx.SenderAddress) : UInt256.Zero;
294+
buffer[0] = tx.SenderAddress;
295+
buffer[1] = tx.GetRecipient(senderNonce);
296+
buffer[2] = header.GasBeneficiary;
297+
idx = 3;
298+
}
280299

281-
// EIP-2930: sender, recipient and gas beneficiary are implicitly accessed,
282-
// so excluding them keeps the returned access list minimal.
283-
UInt256 senderNonce = tx.IsContractCreation ? stateReader.GetNonce(header, tx.SenderAddress) : UInt256.Zero;
284-
Address recipient = tx.GetRecipient(senderNonce);
285-
return [tx.SenderAddress, recipient, header.GasBeneficiary];
300+
foreach (AddressAsKey p in precompiles)
301+
buffer[idx++] = p.Value;
286302
}
287303

288304
private static bool HasConverged(AccessList? previous, AccessList? discovered)

0 commit comments

Comments
 (0)