From 75fc1dd0f76b49076a3ecb4df1e2a278d758a3a7 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Wed, 29 Apr 2026 11:55:51 +0200 Subject: [PATCH 01/18] fix(eth_createAccessList): enable access list tracking for pre-Berlin blocks --- .../Tracing/AccessTxTracerTests.cs | 60 +++++++++++++++++++ .../GasPolicy/EthereumGasPolicy.cs | 42 +++++++------ .../Instructions/EvmInstructions.Call.cs | 2 +- .../Instructions/EvmInstructions.Create.cs | 3 +- .../TransactionProcessor.cs | 7 ++- 5 files changed, 91 insertions(+), 23 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs index 6e6c5333d61d..95bdce4444bf 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs @@ -119,4 +119,64 @@ public void ReportAccess_AddressAIsSetToOptimizedAndHasStorageCell_AccessListHas return (tracer, block, transaction); } } + + [TestFixture] + public class AccessTxTracerPreBerlinTests : VirtualMachineTestsBase + { + // Regression for issue #11209 — eth_createAccessList returned an empty access list pre-Berlin + // because tracer-driven WarmUp lived inside the `spec.UseHotAndColdStorage` gate. + protected override ISpecProvider SpecProvider => new TestSpecProvider(Istanbul.Instance); + + [Test] + public void Pre_berlin_call_address_is_captured_in_access_list() + { + byte[] code = Prepare.EvmCode + .Call(TestItem.AddressC, 50000) + .Op(Instruction.STOP) + .Done; + + AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, code); + + Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); + addresses.Should().Contain(TestItem.AddressC); + } + + [Test] + public void Pre_berlin_sstore_storage_key_is_captured_in_access_list() + { + byte[] code = Prepare.EvmCode + .PushData("0x01") + .PushData("0x69") + .Op(Instruction.SSTORE) + .Done; + + AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, code); + + tracer.AccessList!.Should().ContainEquivalentOf( + (SenderRecipientAndMiner.Default.Recipient, new UInt256[] { 105 })); + } + + [Test] + public void Pre_berlin_sload_storage_key_is_captured_in_access_list() + { + byte[] code = Prepare.EvmCode + .PushData("0x07") + .Op(Instruction.SLOAD) + .Op(Instruction.STOP) + .Done; + + AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, code); + + tracer.AccessList!.Should().ContainEquivalentOf( + (SenderRecipientAndMiner.Default.Recipient, new UInt256[] { 7 })); + } + + private AccessTxTracer ExecuteAndTraceAccessCall(SenderRecipientAndMiner addresses, params byte[] code) + { + (Block block, Transaction transaction) = PrepareTx(BlockNumber, 100000, code, addresses); + AccessTxTracer tracer = new(addresses.Sender, addresses.Recipient, addresses.Miner); + _processor.Execute(transaction, new BlockExecutionContext(block.Header, Spec), tracer); + return tracer; + } + } } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 9d18d4542423..295d57a377da 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -122,7 +122,7 @@ public static bool ConsumeAccountAccessGasWithDelegation(ref EthereumGasPolicy g Address? delegated, bool chargeForWarm = true) { - if (!spec.UseHotAndColdStorage) + if (!spec.UseHotAndColdStorage && !isTracingAccess) return true; bool notOutOfGas = ConsumeAccountAccessGas(ref gas, spec, in accessTracker, isTracingAccess, address, chargeForWarm); @@ -136,25 +136,26 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, Address address, bool chargeForWarm = true) { - bool result = true; - if (spec.UseHotAndColdStorage) + // Tracing-driven warmup runs regardless of spec so that eth_createAccessList + // captures accesses pre-Berlin (when warm/cold gas accounting is inactive). + if (isTracingAccess) { - if (isTracingAccess) - { - accessTracker.WarmUp(address); - } - - if (!spec.IsPrecompile(address) && accessTracker.WarmUp(address)) - { - result = UpdateGas(ref gas, GasCostOf.ColdAccountAccess); - } - else if (chargeForWarm) - { - result = UpdateGas(ref gas, GasCostOf.WarmStateRead); - } + accessTracker.WarmUp(address); } - return result; + if (!spec.UseHotAndColdStorage) + return true; + + if (!spec.IsPrecompile(address) && accessTracker.WarmUp(address)) + { + return UpdateGas(ref gas, GasCostOf.ColdAccountAccess); + } + if (chargeForWarm) + { + return UpdateGas(ref gas, GasCostOf.WarmStateRead); + } + + return true; } public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, @@ -164,13 +165,16 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, StorageAccessType storageAccessType, IReleaseSpec spec) { - if (!spec.UseHotAndColdStorage) - return true; + // Tracing-driven warmup runs regardless of spec so that eth_createAccessList + // captures accesses pre-Berlin (when warm/cold gas accounting is inactive). if (isTracingAccess) { accessTracker.WarmUp(in storageCell); } + if (!spec.UseHotAndColdStorage) + return true; + if (accessTracker.WarmUp(in storageCell)) return UpdateGas(ref gas, GasCostOf.ColdSLoad); if (storageAccessType == StorageAccessType.SLOAD) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 3a9f1885ec11..777e6e75f556 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -169,7 +169,7 @@ public static EvmExceptionType InstructionCall Date: Wed, 29 Apr 2026 16:37:01 +0200 Subject: [PATCH 02/18] fix(eth_createAccessList): wip Non-Journaled Side-Log on StackAccessTracker --- .../Tracing/AccessTxTracerTests.cs | 107 ++++++++++++++++++ .../Nethermind.Evm/StackAccessTracker.cs | 36 +++++- .../TransactionProcessor.cs | 4 +- 3 files changed, 141 insertions(+), 6 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs index 95bdce4444bf..a52beaafdfc9 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs @@ -179,4 +179,111 @@ private AccessTxTracer ExecuteAndTraceAccessCall(SenderRecipientAndMiner address return tracer; } } + + /// + /// Regression tests for issue #11209 – Bug 2: + /// Addresses/storage cells accessed inside a reverted sub-frame were silently dropped from the + /// access list because unwound the journaled set. + /// The non-journaled side-log on now preserves them. + /// + [TestFixture] + public class AccessTxTracerRevertedFrameTests : VirtualMachineTestsBase + { + protected override ISpecProvider SpecProvider => new TestSpecProvider(Berlin.Instance); + + // Recipient code: CALL AddressC with 50 k gas, then REVERT (regardless of CALL outcome) + // This means AddressC is accessed during the call setup (ConsumeAccountAccessGas), + // but the outer frame reverts — the address must still appear in the access list. + [Test] + public void Reverted_call_target_address_is_still_captured_in_access_list() + { + // Code deployed at recipient: CALL AddressC, then REVERT + byte[] code = Prepare.EvmCode + .Call(TestItem.AddressC, 50000) + .PushData(0) + .PushData(0) + .Op(Instruction.REVERT) + .Done; + + AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, code); + + Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); + // AddressC must appear even though the outer frame reverts after the CALL returns + addresses.Should().Contain(TestItem.AddressC, + because: "addresses accessed inside reverted frames must survive for eth_createAccessList"); + } + + // AddressC's code does an SLOAD then REVERTs. + // The storage key must appear even though the frame reverts. + [Test] + public void Reverted_sub_frame_sload_storage_key_is_still_captured_in_access_list() + { + // Code at AddressC: SLOAD slot 7 then REVERT + byte[] addressCCode = Prepare.EvmCode + .PushData(7) + .Op(Instruction.SLOAD) + .PushData(0) + .PushData(0) + .Op(Instruction.REVERT) + .Done; + + TestState.CreateAccount(TestItem.AddressC, 0); + TestState.InsertCode(TestItem.AddressC, addressCCode, SpecProvider.GenesisSpec); + TestState.Commit(SpecProvider.GenesisSpec); + TestState.CommitTree(0); + + // Recipient code: CALL AddressC then STOP + byte[] recipientCode = Prepare.EvmCode + .Call(TestItem.AddressC, 50000) + .Op(Instruction.STOP) + .Done; + + AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, recipientCode); + + IEnumerable<(Address Address, IEnumerable StorageKeys)> list = tracer.AccessList!; + // AddressC slot 7 must appear despite the REVERT inside AddressC's sub-frame + list.Should().ContainSingle(e => e.Address == TestItem.AddressC) + .Which.StorageKeys.Should().Contain(new UInt256(7), + because: "storage cells first accessed inside a reverted sub-frame must still be captured"); + } + + // Two-level scenario: outer frame COMMITs, inner frame REVERTs. + // Both the outer target (AddressC) and the inner target (AddressD) must appear. + [Test] + public void Outer_committed_and_inner_reverted_call_both_captured_in_access_list() + { + // AddressC code: CALL AddressD then REVERT + byte[] addressCCode = Prepare.EvmCode + .Call(TestItem.AddressD, 20000) + .PushData(0) + .PushData(0) + .Op(Instruction.REVERT) + .Done; + + TestState.CreateAccount(TestItem.AddressC, 0); + TestState.InsertCode(TestItem.AddressC, addressCCode, SpecProvider.GenesisSpec); + TestState.Commit(SpecProvider.GenesisSpec); + TestState.CommitTree(0); + + // Recipient code: CALL AddressC (this succeeds at the EVM level but AddressC reverts internally) + byte[] recipientCode = Prepare.EvmCode + .Call(TestItem.AddressC, 50000) + .Op(Instruction.STOP) + .Done; + + AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, recipientCode); + + Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); + addresses.Should().Contain(TestItem.AddressC, because: "committed outer CALL target must be in access list"); + addresses.Should().Contain(TestItem.AddressD, because: "address accessed inside inner reverted frame must still be in access list"); + } + + private AccessTxTracer ExecuteAndTraceAccessCall(SenderRecipientAndMiner addresses, byte[] code) + { + (Block block, Transaction transaction) = PrepareTx(BlockNumber, 100000, code, addresses); + AccessTxTracer tracer = new(addresses.Sender, addresses.Recipient, addresses.Miner); + _processor.Execute(transaction, new BlockExecutionContext(block.Header, Spec), tracer); + return tracer; + } + } } diff --git a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs index 78b840dc5363..5f1d32bf91d3 100644 --- a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs +++ b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs @@ -11,7 +11,7 @@ namespace Nethermind.Evm; -public struct StackAccessTracker() : IDisposable +public struct StackAccessTracker(bool isTracingAccess = false) : IDisposable { public readonly JournalSet
AccessedAddresses => _trackingState.AccessedAddresses; public readonly JournalSet AccessedStorageCells => _trackingState.AccessedStorageCells; @@ -19,6 +19,15 @@ public struct StackAccessTracker() : IDisposable public readonly JournalSet
DestroyList => _trackingState.DestroyList; public readonly HashSet CreateList => _trackingState.CreateList; + /// + /// Non-journaled sets populated only when is true. + /// These survive sub-frame reverts so that eth_createAccessList captures + /// all accessed addresses/storage cells, matching Geth behavior. + /// + public readonly HashSet
AllAccessedAddresses => _trackingState.AllAccessedAddresses; + public readonly HashSet AllAccessedStorageCells => _trackingState.AllAccessedStorageCells; + + private readonly bool _isTracingAccess = isTracingAccess; private TrackingState _trackingState = TrackingState.RentState(); private int _addressesSnapshots; @@ -31,10 +40,18 @@ public struct StackAccessTracker() : IDisposable public readonly bool IsCold(in StorageCell storageCell) => !_trackingState.AccessedStorageCells.Contains(storageCell); public readonly bool WarmUp(Address address) - => _trackingState.AccessedAddresses.Add(address); + { + if (_isTracingAccess) + _trackingState.AllAccessedAddresses.Add(address); + return _trackingState.AccessedAddresses.Add(address); + } public readonly bool WarmUp(in StorageCell storageCell) - => _trackingState.AccessedStorageCells.Add(storageCell); + { + if (_isTracingAccess) + _trackingState.AllAccessedStorageCells.Add(storageCell); + return _trackingState.AccessedStorageCells.Add(storageCell); + } public readonly void WarmUp(AccessList? accessList) { @@ -43,9 +60,14 @@ public readonly void WarmUp(AccessList? accessList) foreach ((Address address, AccessList.StorageKeysEnumerable storages) in accessList) { _trackingState.AccessedAddresses.Add(address); + if (_isTracingAccess) + _trackingState.AllAccessedAddresses.Add(address); foreach (UInt256 storage in storages) { - _trackingState.AccessedStorageCells.Add(new StorageCell(address, in storage)); + StorageCell cell = new(address, in storage); + _trackingState.AccessedStorageCells.Add(cell); + if (_isTracingAccess) + _trackingState.AllAccessedStorageCells.Add(cell); } } } @@ -95,6 +117,10 @@ public static void ResetAndReturn(TrackingState state) public JournalSet
DestroyList { get; } = new(Address.EqualityComparer); public HashSet CreateList { get; } = new(AddressAsKey.EqualityComparer); + // Non-journaled sets for access-list tracing — never restored on sub-frame revert. + public HashSet
AllAccessedAddresses { get; } = new(Address.EqualityComparer); + public HashSet AllAccessedStorageCells { get; } = new(StorageCell.EqualityComparer); + private void Clear() { AccessedAddresses.Clear(); @@ -102,6 +128,8 @@ private void Clear() Logs.Clear(); DestroyList.Clear(); CreateList.Clear(); + AllAccessedAddresses.Clear(); + AllAccessedStorageCells.Clear(); } } } diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 40bd490913d1..2e32e9fcc96d 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -228,7 +228,7 @@ protected virtual TransactionResult Execute(Transaction tx, ITxTracer tracer, Ex if (commit) WorldState.Commit(spec, tracer.IsTracingState ? tracer : NullTxTracer.Instance, commitRoots: false); // substate.Logs contains a reference to accessTracker.Logs so we can't Dispose until end of the method - using StackAccessTracker accessTracker = new(); + using StackAccessTracker accessTracker = new(tracer.IsTracingAccess); int delegationRefunds = !spec.IsEip7702Enabled || !tx.HasAuthorizationList ? 0 : ProcessDelegations(tx, spec, accessTracker); @@ -825,7 +825,7 @@ private int ExecuteEvmCall( if (tracer.IsTracingAccess) { - tracer.ReportAccess(accessedItems.AccessedAddresses, accessedItems.AccessedStorageCells); + tracer.ReportAccess(accessedItems.AllAccessedAddresses, accessedItems.AllAccessedStorageCells); } if (substate.ShouldRevert || substate.IsError) From 24e5508d91d2867523345f9f1309a8a7690c9d06 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 30 Apr 2026 10:08:58 +0200 Subject: [PATCH 03/18] refactor: remove outdated comments related to eth_createAccessList warmup logic --- .../Tracing/AccessTxTracerTests.cs | 32 ++++++++----------- .../GasPolicy/EthereumGasPolicy.cs | 4 --- .../Nethermind.Evm/StackAccessTracker.cs | 10 ++---- .../TransactionProcessor.cs | 2 -- 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs index a52beaafdfc9..fb280f50dcc6 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs @@ -8,8 +8,10 @@ using Nethermind.Blockchain.Tracing; using Nethermind.Core; using Nethermind.Core.Collections; +using Nethermind.Core.Eip2930; using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; +using Nethermind.Evm.State; using Nethermind.Evm.TransactionProcessing; using Nethermind.Int256; using Nethermind.Specs; @@ -123,8 +125,6 @@ public void ReportAccess_AddressAIsSetToOptimizedAndHasStorageCell_AccessListHas [TestFixture] public class AccessTxTracerPreBerlinTests : VirtualMachineTestsBase { - // Regression for issue #11209 — eth_createAccessList returned an empty access list pre-Berlin - // because tracer-driven WarmUp lived inside the `spec.UseHotAndColdStorage` gate. protected override ISpecProvider SpecProvider => new TestSpecProvider(Istanbul.Instance); [Test] @@ -180,20 +180,11 @@ private AccessTxTracer ExecuteAndTraceAccessCall(SenderRecipientAndMiner address } } - /// - /// Regression tests for issue #11209 – Bug 2: - /// Addresses/storage cells accessed inside a reverted sub-frame were silently dropped from the - /// access list because unwound the journaled set. - /// The non-journaled side-log on now preserves them. - /// [TestFixture] public class AccessTxTracerRevertedFrameTests : VirtualMachineTestsBase { protected override ISpecProvider SpecProvider => new TestSpecProvider(Berlin.Instance); - // Recipient code: CALL AddressC with 50 k gas, then REVERT (regardless of CALL outcome) - // This means AddressC is accessed during the call setup (ConsumeAccountAccessGas), - // but the outer frame reverts — the address must still appear in the access list. [Test] public void Reverted_call_target_address_is_still_captured_in_access_list() { @@ -213,8 +204,6 @@ public void Reverted_call_target_address_is_still_captured_in_access_list() because: "addresses accessed inside reverted frames must survive for eth_createAccessList"); } - // AddressC's code does an SLOAD then REVERTs. - // The storage key must appear even though the frame reverts. [Test] public void Reverted_sub_frame_sload_storage_key_is_still_captured_in_access_list() { @@ -240,21 +229,26 @@ public void Reverted_sub_frame_sload_storage_key_is_still_captured_in_access_lis AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, recipientCode); - IEnumerable<(Address Address, IEnumerable StorageKeys)> list = tracer.AccessList!; + AccessList list = tracer.AccessList!; // AddressC slot 7 must appear despite the REVERT inside AddressC's sub-frame list.Should().ContainSingle(e => e.Address == TestItem.AddressC) .Which.StorageKeys.Should().Contain(new UInt256(7), because: "storage cells first accessed inside a reverted sub-frame must still be captured"); } - // Two-level scenario: outer frame COMMITs, inner frame REVERTs. - // Both the outer target (AddressC) and the inner target (AddressD) must appear. [Test] public void Outer_committed_and_inner_reverted_call_both_captured_in_access_list() { - // AddressC code: CALL AddressD then REVERT + byte[] addressECode = Prepare.EvmCode + .Op(Instruction.STOP) + .Done; + + TestState.CreateAccount(TestItem.AddressE, 0); + TestState.InsertCode(TestItem.AddressE, addressECode, SpecProvider.GenesisSpec); + + // AddressC code: CALL AddressE then REVERT byte[] addressCCode = Prepare.EvmCode - .Call(TestItem.AddressD, 20000) + .Call(TestItem.AddressE, 20000) .PushData(0) .PushData(0) .Op(Instruction.REVERT) @@ -275,7 +269,7 @@ public void Outer_committed_and_inner_reverted_call_both_captured_in_access_list Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); addresses.Should().Contain(TestItem.AddressC, because: "committed outer CALL target must be in access list"); - addresses.Should().Contain(TestItem.AddressD, because: "address accessed inside inner reverted frame must still be in access list"); + addresses.Should().Contain(TestItem.AddressE, because: "address accessed inside inner reverted frame must still be in access list"); } private AccessTxTracer ExecuteAndTraceAccessCall(SenderRecipientAndMiner addresses, byte[] code) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 295d57a377da..20511064d44b 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -136,8 +136,6 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, Address address, bool chargeForWarm = true) { - // Tracing-driven warmup runs regardless of spec so that eth_createAccessList - // captures accesses pre-Berlin (when warm/cold gas accounting is inactive). if (isTracingAccess) { accessTracker.WarmUp(address); @@ -165,8 +163,6 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, StorageAccessType storageAccessType, IReleaseSpec spec) { - // Tracing-driven warmup runs regardless of spec so that eth_createAccessList - // captures accesses pre-Berlin (when warm/cold gas accounting is inactive). if (isTracingAccess) { accessTracker.WarmUp(in storageCell); diff --git a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs index 5f1d32bf91d3..8a86d3c39b38 100644 --- a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs +++ b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs @@ -15,18 +15,12 @@ public struct StackAccessTracker(bool isTracingAccess = false) : IDisposable { public readonly JournalSet
AccessedAddresses => _trackingState.AccessedAddresses; public readonly JournalSet AccessedStorageCells => _trackingState.AccessedStorageCells; + public readonly HashSet
AllAccessedAddresses => _trackingState.AllAccessedAddresses; + public readonly HashSet AllAccessedStorageCells => _trackingState.AllAccessedStorageCells; public readonly JournalCollection Logs => _trackingState.Logs; public readonly JournalSet
DestroyList => _trackingState.DestroyList; public readonly HashSet CreateList => _trackingState.CreateList; - /// - /// Non-journaled sets populated only when is true. - /// These survive sub-frame reverts so that eth_createAccessList captures - /// all accessed addresses/storage cells, matching Geth behavior. - /// - public readonly HashSet
AllAccessedAddresses => _trackingState.AllAccessedAddresses; - public readonly HashSet AllAccessedStorageCells => _trackingState.AllAccessedStorageCells; - private readonly bool _isTracingAccess = isTracingAccess; private TrackingState _trackingState = TrackingState.RentState(); diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 2e32e9fcc96d..ac24217a59b1 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -735,8 +735,6 @@ private TransactionResult BuildExecutionEnvironment( accessTracker.WarmUp(delegationAddress); } - // Pre-tx warmup runs when EIP-2929 is active OR when access-list tracing is requested, - // so eth_createAccessList captures the same baseline pre-Berlin as Geth. if (spec.UseHotAndColdStorage || isTracingAccess) { if (spec.UseTxAccessLists) From 1034add2fd3b6daace3b66f5aeb7ab21052ed43b Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 30 Apr 2026 10:10:40 +0200 Subject: [PATCH 04/18] feat(eth_createAccessList): enhance CreateAccessList to exclude precompile addresses without storage keys --- .../Nethermind.Facade/BlockchainBridge.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs index 246e293d2995..4808bb1115ab 100644 --- a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs +++ b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Nethermind.Blockchain; using Nethermind.Blockchain.Filters; using Nethermind.Blockchain.Find; @@ -220,10 +221,17 @@ public CallOutput EstimateGas(BlockHeader header, Transaction tx, int errorMargi public CallOutput CreateAccessList(BlockHeader header, Transaction tx, Dictionary? stateOverride, bool optimize, UInt256? blobBaseFeeOverride, CancellationToken cancellationToken) { + // Collect active precompile addresses so that they can + // be excluded from the access list if they carry no storage keys + IReleaseSpec releaseSpec = specProvider.GetSpec(header); + Address[] precompileAddresses = [.. releaseSpec.Precompiles.Select(static p => (Address)p)]; + AccessTxTracer accessTxTracer = optimize - ? new(tx.SenderAddress, - tx.GetRecipient(tx.IsContractCreation ? stateReader.GetNonce(header, tx.SenderAddress) : 0), header.GasBeneficiary) - : new(header.GasBeneficiary); + ? new([tx.SenderAddress, + tx.GetRecipient(tx.IsContractCreation ? stateReader.GetNonce(header, tx.SenderAddress) : 0), + header.GasBeneficiary, + .. precompileAddresses]) + : new([header.GasBeneficiary, .. precompileAddresses]); CallOutputTracer callOutputTracer = new(); From 5246b456baedc847b86c80f40b338acec2ac62d2 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 30 Apr 2026 10:31:29 +0200 Subject: [PATCH 05/18] feat(BlockchainBridgeTests): add CreateAccessList test to filter precompile addresses with empty storage keys --- .../BlockchainBridgeTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs b/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs index 33cc9565d0c1..cb104a84533e 100644 --- a/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs +++ b/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs @@ -28,6 +28,7 @@ using Nethermind.Facade.Proxy.Models.Simulate; using Nethermind.Facade.Simulate; using Nethermind.Core.Specs; +using Nethermind.Core.Precompiles; using Nethermind.State; namespace Nethermind.Facade.Test; @@ -311,6 +312,33 @@ public void BlobBaseFee_is_set_for_non_blob_transaction([ValueSource(nameof(Brid Arg.Is(blkCtx => blkCtx.BlobBaseFee == expectedBlobBaseFeeHash)); } + [Test] + public void CreateAccessList_filters_precompile_addresses_with_empty_storage_keys() + { + BlockHeader header = Build.A.BlockHeader.TestObject; + Transaction tx = Build.A.Transaction + .WithSenderAddress(TestItem.AddressA) + .WithTo(TestItem.AddressB) + .TestObject; + + _transactionProcessor.CallAndRestore(Arg.Any(), Arg.Any()) + .Returns(callInfo => + { + ITxTracer tracer = callInfo.ArgAt(1); + tracer.ReportAccess( + [PrecompiledAddresses.ECRecover, TestItem.AddressC], + [new StorageCell(TestItem.AddressC, UInt256.One)]); + tracer.MarkAsSuccess(TestItem.AddressB, new GasConsumed(21000, 0), Array.Empty(), Array.Empty()); + return TransactionResult.Ok; + }); + + CallOutput callOutput = _blockchainBridge.CreateAccessList(header, tx, null, true, null, default); + + callOutput.AccessList.Should().NotBeNull(); + callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeFalse(); + callOutput.AccessList.Any(e => e.Address == TestItem.AddressC).Should().BeTrue(); + } + [Test] public void Call_tx_returns_InsufficientSenderBalanceError() { From f3bd06b96e960361c2c7048b3dde40a39f1dce24 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 30 Apr 2026 10:51:46 +0200 Subject: [PATCH 06/18] refactor(eth_createAccessList): update access tracker and precompile address handling and update comments --- .../Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs | 3 +++ .../Nethermind.Evm/Instructions/EvmInstructions.Create.cs | 2 +- src/Nethermind/Nethermind.Evm/StackAccessTracker.cs | 4 ++-- src/Nethermind/Nethermind.Facade/BlockchainBridge.cs | 6 ++++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 20511064d44b..15e7fbd11489 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -144,6 +144,9 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, if (!spec.UseHotAndColdStorage) return true; + // Note: isTracingAccess pre-warms the address above, so this second WarmUp returns false + // (already warm). Cold gas is intentionally not charged here — the caller will include + // these addresses in an EIP-2930 access list, making them warm on actual execution. if (!spec.IsPrecompile(address) && accessTracker.WarmUp(address)) { return UpdateGas(ref gas, GasCostOf.ColdAccountAccess); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs index 94ab2323aa36..87f9ae6c8e93 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs @@ -192,7 +192,7 @@ public static EvmExceptionType InstructionCreate AccessedAddresses => _trackingState.AccessedAddresses; public readonly JournalSet AccessedStorageCells => _trackingState.AccessedStorageCells; - public readonly HashSet
AllAccessedAddresses => _trackingState.AllAccessedAddresses; - public readonly HashSet AllAccessedStorageCells => _trackingState.AllAccessedStorageCells; + public readonly IReadOnlyCollection
AllAccessedAddresses => _trackingState.AllAccessedAddresses; + public readonly IReadOnlyCollection AllAccessedStorageCells => _trackingState.AllAccessedStorageCells; public readonly JournalCollection Logs => _trackingState.Logs; public readonly JournalSet
DestroyList => _trackingState.DestroyList; public readonly HashSet CreateList => _trackingState.CreateList; diff --git a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs index 4808bb1115ab..0405b0208335 100644 --- a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs +++ b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Nethermind.Blockchain; using Nethermind.Blockchain.Filters; using Nethermind.Blockchain.Find; @@ -224,7 +223,10 @@ public CallOutput CreateAccessList(BlockHeader header, Transaction tx, Dictionar // Collect active precompile addresses so that they can // be excluded from the access list if they carry no storage keys IReleaseSpec releaseSpec = specProvider.GetSpec(header); - Address[] precompileAddresses = [.. releaseSpec.Precompiles.Select(static p => (Address)p)]; + Address[] precompileAddresses = new Address[releaseSpec.Precompiles.Count]; + int idx = 0; + foreach (AddressAsKey p in releaseSpec.Precompiles) + precompileAddresses[idx++] = (Address)p; AccessTxTracer accessTxTracer = optimize ? new([tx.SenderAddress, From 1aebd8b233fb9336549ef005556bce7b9b9568db Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 30 Apr 2026 10:56:21 +0200 Subject: [PATCH 07/18] feat(BlockchainBridgeTests): add tests for CreateAccessList to filter precompile addresses based on storage keys --- .../BlockchainBridgeTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs b/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs index cb104a84533e..a068ac1da2a2 100644 --- a/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs +++ b/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs @@ -339,6 +339,59 @@ public void CreateAccessList_filters_precompile_addresses_with_empty_storage_key callOutput.AccessList.Any(e => e.Address == TestItem.AddressC).Should().BeTrue(); } + [Test] + public void CreateAccessList_filters_precompile_addresses_with_empty_storage_keys_when_optimize_is_false() + { + BlockHeader header = Build.A.BlockHeader.TestObject; + Transaction tx = Build.A.Transaction + .WithSenderAddress(TestItem.AddressA) + .WithTo(TestItem.AddressB) + .TestObject; + + _transactionProcessor.CallAndRestore(Arg.Any(), Arg.Any()) + .Returns(callInfo => + { + ITxTracer tracer = callInfo.ArgAt(1); + tracer.ReportAccess( + [PrecompiledAddresses.ECRecover, TestItem.AddressC], + [new StorageCell(TestItem.AddressC, UInt256.One)]); + tracer.MarkAsSuccess(TestItem.AddressB, new GasConsumed(21000, 0), Array.Empty(), Array.Empty()); + return TransactionResult.Ok; + }); + + CallOutput callOutput = _blockchainBridge.CreateAccessList(header, tx, null, false, null, default); + + callOutput.AccessList.Should().NotBeNull(); + callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeFalse(); + callOutput.AccessList.Any(e => e.Address == TestItem.AddressC).Should().BeTrue(); + } + + [Test] + public void CreateAccessList_keeps_precompile_address_when_storage_key_is_present() + { + BlockHeader header = Build.A.BlockHeader.TestObject; + Transaction tx = Build.A.Transaction + .WithSenderAddress(TestItem.AddressA) + .WithTo(TestItem.AddressB) + .TestObject; + + _transactionProcessor.CallAndRestore(Arg.Any(), Arg.Any()) + .Returns(callInfo => + { + ITxTracer tracer = callInfo.ArgAt(1); + tracer.ReportAccess( + [PrecompiledAddresses.ECRecover], + [new StorageCell(PrecompiledAddresses.ECRecover, UInt256.One)]); + tracer.MarkAsSuccess(TestItem.AddressB, new GasConsumed(21000, 0), Array.Empty(), Array.Empty()); + return TransactionResult.Ok; + }); + + CallOutput callOutput = _blockchainBridge.CreateAccessList(header, tx, null, false, null, default); + + callOutput.AccessList.Should().NotBeNull(); + callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeTrue(); + } + [Test] public void Call_tx_returns_InsufficientSenderBalanceError() { From 047a9c70f0e0ca90bd94293fbb3480882c8eb5eb Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 30 Apr 2026 11:13:33 +0200 Subject: [PATCH 08/18] refactor(StackAccessTracker): remove default parameter and update constructor for clarity --- src/Nethermind/Nethermind.Evm/StackAccessTracker.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs index 7a48280f8399..7baf6ebf04b7 100644 --- a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs +++ b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs @@ -11,10 +11,8 @@ namespace Nethermind.Evm; -public struct StackAccessTracker(bool isTracingAccess = false) : IDisposable +public struct StackAccessTracker(bool isTracingAccess) : IDisposable { - public readonly JournalSet
AccessedAddresses => _trackingState.AccessedAddresses; - public readonly JournalSet AccessedStorageCells => _trackingState.AccessedStorageCells; public readonly IReadOnlyCollection
AllAccessedAddresses => _trackingState.AllAccessedAddresses; public readonly IReadOnlyCollection AllAccessedStorageCells => _trackingState.AllAccessedStorageCells; public readonly JournalCollection Logs => _trackingState.Logs; @@ -29,6 +27,8 @@ public struct StackAccessTracker(bool isTracingAccess = false) : IDisposable private int _destroyListSnapshots; private int _logsSnapshots; + public StackAccessTracker() : this(false) {} + public readonly bool IsCold(Address? address) => !_trackingState.AccessedAddresses.Contains(address); public readonly bool IsCold(in StorageCell storageCell) => !_trackingState.AccessedStorageCells.Contains(storageCell); From dd0bcdf89b2e148763f954d47aaa56d4746190e3 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 30 Apr 2026 11:16:14 +0200 Subject: [PATCH 09/18] fix(StackAccessTracker): add space for consistency in constructor formatting --- src/Nethermind/Nethermind.Evm/StackAccessTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs index 7baf6ebf04b7..e08fcf6339cb 100644 --- a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs +++ b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs @@ -27,7 +27,7 @@ public struct StackAccessTracker(bool isTracingAccess) : IDisposable private int _destroyListSnapshots; private int _logsSnapshots; - public StackAccessTracker() : this(false) {} + public StackAccessTracker() : this(false) { } public readonly bool IsCold(Address? address) => !_trackingState.AccessedAddresses.Contains(address); From b9d46906c16464c38e4bbbcbf752570f30452740 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Wed, 6 May 2026 16:38:56 +0200 Subject: [PATCH 10/18] fix(eth_createAccessList): drop pre-Berlin scope --- .../Tracing/AccessTxTracerTests.cs | 59 ------------------- .../GasPolicy/EthereumGasPolicy.cs | 41 ++++++------- .../Instructions/EvmInstructions.Call.cs | 2 +- .../Instructions/EvmInstructions.Create.cs | 3 +- .../TransactionProcessor.cs | 5 +- 5 files changed, 23 insertions(+), 87 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs index fb280f50dcc6..c316633c71a1 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs @@ -11,7 +11,6 @@ using Nethermind.Core.Eip2930; using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; -using Nethermind.Evm.State; using Nethermind.Evm.TransactionProcessing; using Nethermind.Int256; using Nethermind.Specs; @@ -122,64 +121,6 @@ public void ReportAccess_AddressAIsSetToOptimizedAndHasStorageCell_AccessListHas } } - [TestFixture] - public class AccessTxTracerPreBerlinTests : VirtualMachineTestsBase - { - protected override ISpecProvider SpecProvider => new TestSpecProvider(Istanbul.Instance); - - [Test] - public void Pre_berlin_call_address_is_captured_in_access_list() - { - byte[] code = Prepare.EvmCode - .Call(TestItem.AddressC, 50000) - .Op(Instruction.STOP) - .Done; - - AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, code); - - Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); - addresses.Should().Contain(TestItem.AddressC); - } - - [Test] - public void Pre_berlin_sstore_storage_key_is_captured_in_access_list() - { - byte[] code = Prepare.EvmCode - .PushData("0x01") - .PushData("0x69") - .Op(Instruction.SSTORE) - .Done; - - AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, code); - - tracer.AccessList!.Should().ContainEquivalentOf( - (SenderRecipientAndMiner.Default.Recipient, new UInt256[] { 105 })); - } - - [Test] - public void Pre_berlin_sload_storage_key_is_captured_in_access_list() - { - byte[] code = Prepare.EvmCode - .PushData("0x07") - .Op(Instruction.SLOAD) - .Op(Instruction.STOP) - .Done; - - AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, code); - - tracer.AccessList!.Should().ContainEquivalentOf( - (SenderRecipientAndMiner.Default.Recipient, new UInt256[] { 7 })); - } - - private AccessTxTracer ExecuteAndTraceAccessCall(SenderRecipientAndMiner addresses, params byte[] code) - { - (Block block, Transaction transaction) = PrepareTx(BlockNumber, 100000, code, addresses); - AccessTxTracer tracer = new(addresses.Sender, addresses.Recipient, addresses.Miner); - _processor.Execute(transaction, new BlockExecutionContext(block.Header, Spec), tracer); - return tracer; - } - } - [TestFixture] public class AccessTxTracerRevertedFrameTests : VirtualMachineTestsBase { diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 15e7fbd11489..9d18d4542423 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -122,7 +122,7 @@ public static bool ConsumeAccountAccessGasWithDelegation(ref EthereumGasPolicy g Address? delegated, bool chargeForWarm = true) { - if (!spec.UseHotAndColdStorage && !isTracingAccess) + if (!spec.UseHotAndColdStorage) return true; bool notOutOfGas = ConsumeAccountAccessGas(ref gas, spec, in accessTracker, isTracingAccess, address, chargeForWarm); @@ -136,27 +136,25 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, Address address, bool chargeForWarm = true) { - if (isTracingAccess) + bool result = true; + if (spec.UseHotAndColdStorage) { - accessTracker.WarmUp(address); + if (isTracingAccess) + { + accessTracker.WarmUp(address); + } + + if (!spec.IsPrecompile(address) && accessTracker.WarmUp(address)) + { + result = UpdateGas(ref gas, GasCostOf.ColdAccountAccess); + } + else if (chargeForWarm) + { + result = UpdateGas(ref gas, GasCostOf.WarmStateRead); + } } - if (!spec.UseHotAndColdStorage) - return true; - - // Note: isTracingAccess pre-warms the address above, so this second WarmUp returns false - // (already warm). Cold gas is intentionally not charged here — the caller will include - // these addresses in an EIP-2930 access list, making them warm on actual execution. - if (!spec.IsPrecompile(address) && accessTracker.WarmUp(address)) - { - return UpdateGas(ref gas, GasCostOf.ColdAccountAccess); - } - if (chargeForWarm) - { - return UpdateGas(ref gas, GasCostOf.WarmStateRead); - } - - return true; + return result; } public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, @@ -166,14 +164,13 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, StorageAccessType storageAccessType, IReleaseSpec spec) { + if (!spec.UseHotAndColdStorage) + return true; if (isTracingAccess) { accessTracker.WarmUp(in storageCell); } - if (!spec.UseHotAndColdStorage) - return true; - if (accessTracker.WarmUp(in storageCell)) return UpdateGas(ref gas, GasCostOf.ColdSLoad); if (storageAccessType == StorageAccessType.SLOAD) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 777e6e75f556..3a9f1885ec11 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -169,7 +169,7 @@ public static EvmExceptionType InstructionCall Date: Wed, 6 May 2026 17:16:45 +0200 Subject: [PATCH 11/18] fix(eth_createAccessList): fixed missing access-list entries from reverted sub-frames by avoiding restore for traced accesses --- .../Tracing/AccessTxTracerTests.cs | 36 +++++++--------- .../Nethermind.Evm/StackAccessTracker.cs | 42 ++++++------------- .../TransactionProcessor.cs | 2 +- 3 files changed, 29 insertions(+), 51 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs index c316633c71a1..24f88703e65b 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs @@ -11,6 +11,7 @@ using Nethermind.Core.Eip2930; using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; +using Nethermind.Evm.State; using Nethermind.Evm.TransactionProcessing; using Nethermind.Int256; using Nethermind.Specs; @@ -110,21 +111,6 @@ public void ReportAccess_AddressAIsSetToOptimizedAndHasStorageCell_AccessListHas Assert.That(sut.AccessList.Select(static x => x.StorageKeys), Has.Exactly(1).Contains(new UInt256(1))); } - protected override ISpecProvider SpecProvider => new TestSpecProvider(Berlin.Instance); - - protected (AccessTxTracer trace, Block block, Transaction transaction) ExecuteAndTraceAccessCall(SenderRecipientAndMiner addresses, params byte[] code) - { - (Block block, Transaction transaction) = PrepareTx(BlockNumber, 100000, code, addresses); - AccessTxTracer tracer = new(); - _processor.Execute(transaction, new BlockExecutionContext(block.Header, Spec), tracer); - return (tracer, block, transaction); - } - } - - [TestFixture] - public class AccessTxTracerRevertedFrameTests : VirtualMachineTestsBase - { - protected override ISpecProvider SpecProvider => new TestSpecProvider(Berlin.Instance); [Test] public void Reverted_call_target_address_is_still_captured_in_access_list() @@ -137,7 +123,9 @@ public void Reverted_call_target_address_is_still_captured_in_access_list() .Op(Instruction.REVERT) .Done; - AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, code); + (Block block, Transaction tx) = PrepareTx(BlockNumber, 100000, code, SenderRecipientAndMiner.Default); + AccessTxTracer tracer = new(SenderRecipientAndMiner.Default.Sender, SenderRecipientAndMiner.Default.Recipient, SenderRecipientAndMiner.Default.Miner); + _processor.Execute(tx, new BlockExecutionContext(block.Header, Spec), tracer); Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); // AddressC must appear even though the outer frame reverts after the CALL returns @@ -168,7 +156,9 @@ public void Reverted_sub_frame_sload_storage_key_is_still_captured_in_access_lis .Op(Instruction.STOP) .Done; - AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, recipientCode); + (Block block, Transaction tx) = PrepareTx(BlockNumber, 100000, recipientCode, SenderRecipientAndMiner.Default); + AccessTxTracer tracer = new(SenderRecipientAndMiner.Default.Sender, SenderRecipientAndMiner.Default.Recipient, SenderRecipientAndMiner.Default.Miner); + _processor.Execute(tx, new BlockExecutionContext(block.Header, Spec), tracer); AccessList list = tracer.AccessList!; // AddressC slot 7 must appear despite the REVERT inside AddressC's sub-frame @@ -206,19 +196,23 @@ public void Outer_committed_and_inner_reverted_call_both_captured_in_access_list .Op(Instruction.STOP) .Done; - AccessTxTracer tracer = ExecuteAndTraceAccessCall(SenderRecipientAndMiner.Default, recipientCode); + (Block block, Transaction tx) = PrepareTx(BlockNumber, 100000, recipientCode, SenderRecipientAndMiner.Default); + AccessTxTracer tracer = new(SenderRecipientAndMiner.Default.Sender, SenderRecipientAndMiner.Default.Recipient, SenderRecipientAndMiner.Default.Miner); + _processor.Execute(tx, new BlockExecutionContext(block.Header, Spec), tracer); Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); addresses.Should().Contain(TestItem.AddressC, because: "committed outer CALL target must be in access list"); addresses.Should().Contain(TestItem.AddressE, because: "address accessed inside inner reverted frame must still be in access list"); } - private AccessTxTracer ExecuteAndTraceAccessCall(SenderRecipientAndMiner addresses, byte[] code) + protected override ISpecProvider SpecProvider => new TestSpecProvider(Berlin.Instance); + + protected (AccessTxTracer trace, Block block, Transaction transaction) ExecuteAndTraceAccessCall(SenderRecipientAndMiner addresses, params byte[] code) { (Block block, Transaction transaction) = PrepareTx(BlockNumber, 100000, code, addresses); - AccessTxTracer tracer = new(addresses.Sender, addresses.Recipient, addresses.Miner); + AccessTxTracer tracer = new(); _processor.Execute(transaction, new BlockExecutionContext(block.Header, Spec), tracer); - return tracer; + return (tracer, block, transaction); } } } diff --git a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs index e08fcf6339cb..1428f3e92d1a 100644 --- a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs +++ b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs @@ -11,10 +11,10 @@ namespace Nethermind.Evm; -public struct StackAccessTracker(bool isTracingAccess) : IDisposable +public struct StackAccessTracker(bool isTracingAccess = false) : IDisposable { - public readonly IReadOnlyCollection
AllAccessedAddresses => _trackingState.AllAccessedAddresses; - public readonly IReadOnlyCollection AllAccessedStorageCells => _trackingState.AllAccessedStorageCells; + public readonly JournalSet
AccessedAddresses => _trackingState.AccessedAddresses; + public readonly JournalSet AccessedStorageCells => _trackingState.AccessedStorageCells; public readonly JournalCollection Logs => _trackingState.Logs; public readonly JournalSet
DestroyList => _trackingState.DestroyList; public readonly HashSet CreateList => _trackingState.CreateList; @@ -27,25 +27,15 @@ public struct StackAccessTracker(bool isTracingAccess) : IDisposable private int _destroyListSnapshots; private int _logsSnapshots; - public StackAccessTracker() : this(false) { } - public readonly bool IsCold(Address? address) => !_trackingState.AccessedAddresses.Contains(address); public readonly bool IsCold(in StorageCell storageCell) => !_trackingState.AccessedStorageCells.Contains(storageCell); public readonly bool WarmUp(Address address) - { - if (_isTracingAccess) - _trackingState.AllAccessedAddresses.Add(address); - return _trackingState.AccessedAddresses.Add(address); - } + => _trackingState.AccessedAddresses.Add(address); public readonly bool WarmUp(in StorageCell storageCell) - { - if (_isTracingAccess) - _trackingState.AllAccessedStorageCells.Add(storageCell); - return _trackingState.AccessedStorageCells.Add(storageCell); - } + => _trackingState.AccessedStorageCells.Add(storageCell); public readonly void WarmUp(AccessList? accessList) { @@ -54,14 +44,9 @@ public readonly void WarmUp(AccessList? accessList) foreach ((Address address, AccessList.StorageKeysEnumerable storages) in accessList) { _trackingState.AccessedAddresses.Add(address); - if (_isTracingAccess) - _trackingState.AllAccessedAddresses.Add(address); foreach (UInt256 storage in storages) { - StorageCell cell = new(address, in storage); - _trackingState.AccessedStorageCells.Add(cell); - if (_isTracingAccess) - _trackingState.AllAccessedStorageCells.Add(cell); + _trackingState.AccessedStorageCells.Add(new StorageCell(address, in storage)); } } } @@ -81,8 +66,13 @@ public void TakeSnapshot() public readonly void Restore() { - _trackingState.AccessedAddresses.Restore(_addressesSnapshots); - _trackingState.AccessedStorageCells.Restore(_storageKeysSnapshots); + // When tracing access, don't restore the access sets on sub-frame revert. + // The generated list will pre-warm all touched addresses. + if (!_isTracingAccess) + { + _trackingState.AccessedAddresses.Restore(_addressesSnapshots); + _trackingState.AccessedStorageCells.Restore(_storageKeysSnapshots); + } _trackingState.DestroyList.Restore(_destroyListSnapshots); _trackingState.Logs.Restore(_logsSnapshots); } @@ -111,10 +101,6 @@ public static void ResetAndReturn(TrackingState state) public JournalSet
DestroyList { get; } = new(Address.EqualityComparer); public HashSet CreateList { get; } = new(AddressAsKey.EqualityComparer); - // Non-journaled sets for access-list tracing — never restored on sub-frame revert. - public HashSet
AllAccessedAddresses { get; } = new(Address.EqualityComparer); - public HashSet AllAccessedStorageCells { get; } = new(StorageCell.EqualityComparer); - private void Clear() { AccessedAddresses.Clear(); @@ -122,8 +108,6 @@ private void Clear() Logs.Clear(); DestroyList.Clear(); CreateList.Clear(); - AllAccessedAddresses.Clear(); - AllAccessedStorageCells.Clear(); } } } diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 75a3765b7d37..dd33a687d320 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -775,7 +775,7 @@ private int ExecuteEvmCall( if (tracer.IsTracingAccess) { - tracer.ReportAccess(accessedItems.AllAccessedAddresses, accessedItems.AllAccessedStorageCells); + tracer.ReportAccess(accessedItems.AccessedAddresses, accessedItems.AccessedStorageCells); } if (substate.ShouldRevert || substate.IsError) From 3dd6355896ea90acbf08aa8047b04b01a489eef2 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Wed, 6 May 2026 17:40:58 +0200 Subject: [PATCH 12/18] fix(BlockchainBridge): include precompiled contracts in access list --- src/Nethermind/Nethermind.Facade/BlockchainBridge.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs index ead87d85047d..773b0bf3a015 100644 --- a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs +++ b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs @@ -19,6 +19,7 @@ using Nethermind.TxPool; using Block = Nethermind.Core.Block; using System.Threading; +using System.Linq; using Nethermind.Core.Specs; using Nethermind.Evm.TransactionProcessing; using Nethermind.Facade.Filters; @@ -275,21 +276,23 @@ private CallOutput ConvergeAccessList(BlockProcessingComponents components, Bloc private Address[] BuildAddressesToOptimize(BlockHeader header, Transaction tx, bool optimize) { + IEnumerable
precompiles = specProvider.GetSpec(header).Precompiles.Select(static p => (Address)p); + if (!optimize) - return [header.GasBeneficiary]; + return [header.GasBeneficiary, .. precompiles]; // EIP-2930: sender, recipient and gas beneficiary are implicitly accessed, // so excluding them keeps the returned access list minimal. UInt256 senderNonce = tx.IsContractCreation ? stateReader.GetNonce(header, tx.SenderAddress) : UInt256.Zero; Address recipient = tx.GetRecipient(senderNonce); - return [tx.SenderAddress, recipient, header.GasBeneficiary]; + return [tx.SenderAddress, recipient, header.GasBeneficiary, .. precompiles]; } private static bool HasConverged(AccessList? previous, AccessList? discovered) { // Count comparison is sufficient because WarmUp(tx.AccessList) pre-populates the warm-address // set with all of `previous`'s entries before execution, making `discovered` monotonically - // non-decreasing (discovered ⊇ previous). Equal counts therefore imply equal content. + // non-decreasing (discovere`d ⊇ previous). Equal counts therefore imply equal content. (int addrs, int keys) previousCount = previous?.Count ?? (0, 0); (int addrs, int keys) discoveredCount = discovered?.Count ?? (0, 0); return previousCount == discoveredCount; From fc271ce0c3892e5d8a21ef7422037189ef33988d Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Wed, 6 May 2026 17:44:01 +0200 Subject: [PATCH 13/18] chore(BlockchainBridge): fix typo --- src/Nethermind/Nethermind.Facade/BlockchainBridge.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs index 773b0bf3a015..9a7677ab382e 100644 --- a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs +++ b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs @@ -292,7 +292,7 @@ private static bool HasConverged(AccessList? previous, AccessList? discovered) { // Count comparison is sufficient because WarmUp(tx.AccessList) pre-populates the warm-address // set with all of `previous`'s entries before execution, making `discovered` monotonically - // non-decreasing (discovere`d ⊇ previous). Equal counts therefore imply equal content. + // non-decreasing (discovered ⊇ previous). Equal counts therefore imply equal content. (int addrs, int keys) previousCount = previous?.Count ?? (0, 0); (int addrs, int keys) discoveredCount = discovered?.Count ?? (0, 0); return previousCount == discoveredCount; From 726944334964300d5ee3c6f5e882b847a2a9013e Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Wed, 6 May 2026 18:02:18 +0200 Subject: [PATCH 14/18] refactor(BlockchainBridge): optimize BuildAddressesToOptimize by removing LINQ and using pre-allocated arrays --- .../Nethermind.Facade/BlockchainBridge.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs index 9a7677ab382e..c581bf65194f 100644 --- a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs +++ b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Nethermind.Blockchain; @@ -19,7 +20,6 @@ using Nethermind.TxPool; using Block = Nethermind.Core.Block; using System.Threading; -using System.Linq; using Nethermind.Core.Specs; using Nethermind.Evm.TransactionProcessing; using Nethermind.Facade.Filters; @@ -276,16 +276,31 @@ private CallOutput ConvergeAccessList(BlockProcessingComponents components, Bloc private Address[] BuildAddressesToOptimize(BlockHeader header, Transaction tx, bool optimize) { - IEnumerable
precompiles = specProvider.GetSpec(header).Precompiles.Select(static p => (Address)p); + FrozenSet precompiles = specProvider.GetSpec(header).Precompiles; + int precompileCount = precompiles.Count; if (!optimize) - return [header.GasBeneficiary, .. precompiles]; + { + Address[] result = new Address[1 + precompileCount]; + result[0] = header.GasBeneficiary; + int i = 1; + foreach (AddressAsKey p in precompiles) + result[i++] = p.Value; + return result; + } // EIP-2930: sender, recipient and gas beneficiary are implicitly accessed, // so excluding them keeps the returned access list minimal. UInt256 senderNonce = tx.IsContractCreation ? stateReader.GetNonce(header, tx.SenderAddress) : UInt256.Zero; Address recipient = tx.GetRecipient(senderNonce); - return [tx.SenderAddress, recipient, header.GasBeneficiary, .. precompiles]; + Address[] resultOpt = new Address[3 + precompileCount]; + resultOpt[0] = tx.SenderAddress; + resultOpt[1] = recipient; + resultOpt[2] = header.GasBeneficiary; + int j = 3; + foreach (AddressAsKey p in precompiles) + resultOpt[j++] = p.Value; + return resultOpt; } private static bool HasConverged(AccessList? previous, AccessList? discovered) From bf5ab54289d59d1aaa1ab3a9f6a07bb3e1c5cae9 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 7 May 2026 23:46:36 +0200 Subject: [PATCH 15/18] refactor(AccessTxTracerTests): de-duplicate added tests --- .../Tracing/AccessTxTracerTests.cs | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs index 24f88703e65b..8c631eb5e7be 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Tracing/AccessTxTracerTests.cs @@ -111,7 +111,6 @@ public void ReportAccess_AddressAIsSetToOptimizedAndHasStorageCell_AccessListHas Assert.That(sut.AccessList.Select(static x => x.StorageKeys), Has.Exactly(1).Contains(new UInt256(1))); } - [Test] public void Reverted_call_target_address_is_still_captured_in_access_list() { @@ -123,13 +122,9 @@ public void Reverted_call_target_address_is_still_captured_in_access_list() .Op(Instruction.REVERT) .Done; - (Block block, Transaction tx) = PrepareTx(BlockNumber, 100000, code, SenderRecipientAndMiner.Default); - AccessTxTracer tracer = new(SenderRecipientAndMiner.Default.Sender, SenderRecipientAndMiner.Default.Recipient, SenderRecipientAndMiner.Default.Miner); - _processor.Execute(tx, new BlockExecutionContext(block.Header, Spec), tracer); + AccessList list = ExecuteRevertedFrameScenario(code); - Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); - // AddressC must appear even though the outer frame reverts after the CALL returns - addresses.Should().Contain(TestItem.AddressC, + list.Select(static t => t.Address).Should().Contain(TestItem.AddressC, because: "addresses accessed inside reverted frames must survive for eth_createAccessList"); } @@ -156,11 +151,8 @@ public void Reverted_sub_frame_sload_storage_key_is_still_captured_in_access_lis .Op(Instruction.STOP) .Done; - (Block block, Transaction tx) = PrepareTx(BlockNumber, 100000, recipientCode, SenderRecipientAndMiner.Default); - AccessTxTracer tracer = new(SenderRecipientAndMiner.Default.Sender, SenderRecipientAndMiner.Default.Recipient, SenderRecipientAndMiner.Default.Miner); - _processor.Execute(tx, new BlockExecutionContext(block.Header, Spec), tracer); + AccessList list = ExecuteRevertedFrameScenario(recipientCode); - AccessList list = tracer.AccessList!; // AddressC slot 7 must appear despite the REVERT inside AddressC's sub-frame list.Should().ContainSingle(e => e.Address == TestItem.AddressC) .Which.StorageKeys.Should().Contain(new UInt256(7), @@ -170,12 +162,8 @@ public void Reverted_sub_frame_sload_storage_key_is_still_captured_in_access_lis [Test] public void Outer_committed_and_inner_reverted_call_both_captured_in_access_list() { - byte[] addressECode = Prepare.EvmCode - .Op(Instruction.STOP) - .Done; - TestState.CreateAccount(TestItem.AddressE, 0); - TestState.InsertCode(TestItem.AddressE, addressECode, SpecProvider.GenesisSpec); + TestState.InsertCode(TestItem.AddressE, Prepare.EvmCode.Op(Instruction.STOP).Done, SpecProvider.GenesisSpec); // AddressC code: CALL AddressE then REVERT byte[] addressCCode = Prepare.EvmCode @@ -190,17 +178,15 @@ public void Outer_committed_and_inner_reverted_call_both_captured_in_access_list TestState.Commit(SpecProvider.GenesisSpec); TestState.CommitTree(0); - // Recipient code: CALL AddressC (this succeeds at the EVM level but AddressC reverts internally) + // Recipient code: CALL AddressC (succeeds at EVM level but AddressC reverts internally) byte[] recipientCode = Prepare.EvmCode .Call(TestItem.AddressC, 50000) .Op(Instruction.STOP) .Done; - (Block block, Transaction tx) = PrepareTx(BlockNumber, 100000, recipientCode, SenderRecipientAndMiner.Default); - AccessTxTracer tracer = new(SenderRecipientAndMiner.Default.Sender, SenderRecipientAndMiner.Default.Recipient, SenderRecipientAndMiner.Default.Miner); - _processor.Execute(tx, new BlockExecutionContext(block.Header, Spec), tracer); + AccessList list = ExecuteRevertedFrameScenario(recipientCode); - Address[] addresses = tracer.AccessList!.Select(static t => t.Address).ToArray(); + Address[] addresses = list.Select(static t => t.Address).ToArray(); addresses.Should().Contain(TestItem.AddressC, because: "committed outer CALL target must be in access list"); addresses.Should().Contain(TestItem.AddressE, because: "address accessed inside inner reverted frame must still be in access list"); } @@ -214,5 +200,13 @@ public void Outer_committed_and_inner_reverted_call_both_captured_in_access_list _processor.Execute(transaction, new BlockExecutionContext(block.Header, Spec), tracer); return (tracer, block, transaction); } + + private AccessList ExecuteRevertedFrameScenario(byte[] recipientCode) + { + (Block block, Transaction tx) = PrepareTx(BlockNumber, 100000, recipientCode, SenderRecipientAndMiner.Default); + AccessTxTracer tracer = new(SenderRecipientAndMiner.Default.Sender, SenderRecipientAndMiner.Default.Recipient, SenderRecipientAndMiner.Default.Miner); + _processor.Execute(tx, new BlockExecutionContext(block.Header, Spec), tracer); + return tracer.AccessList!; + } } } From 0eb138957f8fd1053da73745e97ce3c31d9a01c2 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Thu, 7 May 2026 23:47:50 +0200 Subject: [PATCH 16/18] refactor(BlockchainBridge): preallocate address filter buffer as Span
in ConvergeAccessList --- .../Nethermind.Facade/BlockchainBridge.cs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs index c581bf65194f..14752a3a02e6 100644 --- a/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs +++ b/src/Nethermind/Nethermind.Facade/BlockchainBridge.cs @@ -245,9 +245,13 @@ private CallOutput ConvergeAccessList(BlockProcessingComponents components, Bloc { // Loop-invariant: the addresses to filter from the discovered AL depend only on header // and tx, neither of which change between iterations. Compute once and reuse. - Address[] addressesToOptimize = BuildAddressesToOptimize(header, tx, optimize); + FrozenSet precompiles = specProvider.GetSpec(header).Precompiles; + int bufferSize = (optimize ? 3 : 1) + precompiles.Count; + Address[] addressBuffer = new Address[bufferSize]; + FillAddressesToOptimize(addressBuffer, header, tx, optimize, precompiles); + AccessList? previousAccessList = tx.AccessList; - AccessTxTracer accessTracer = new(addressesToOptimize); + AccessTxTracer accessTracer = new(addressBuffer); CallOutputTracer outputTracer = new(); CancellationTxTracer tracer = new CompositeTxTracer(outputTracer, accessTracer).WithCancellation(cancellationToken); TransactionResult result; @@ -274,33 +278,27 @@ private CallOutput ConvergeAccessList(BlockProcessingComponents components, Bloc }; } - private Address[] BuildAddressesToOptimize(BlockHeader header, Transaction tx, bool optimize) + private void FillAddressesToOptimize(Span
buffer, BlockHeader header, Transaction tx, bool optimize, FrozenSet precompiles) { - FrozenSet precompiles = specProvider.GetSpec(header).Precompiles; - int precompileCount = precompiles.Count; - + int idx; if (!optimize) { - Address[] result = new Address[1 + precompileCount]; - result[0] = header.GasBeneficiary; - int i = 1; - foreach (AddressAsKey p in precompiles) - result[i++] = p.Value; - return result; + buffer[0] = header.GasBeneficiary; + idx = 1; + } + else + { + // EIP-2930: sender, recipient and gas beneficiary are implicitly accessed, + // so excluding them keeps the returned access list minimal. + UInt256 senderNonce = tx.IsContractCreation ? stateReader.GetNonce(header, tx.SenderAddress) : UInt256.Zero; + buffer[0] = tx.SenderAddress; + buffer[1] = tx.GetRecipient(senderNonce); + buffer[2] = header.GasBeneficiary; + idx = 3; } - // EIP-2930: sender, recipient and gas beneficiary are implicitly accessed, - // so excluding them keeps the returned access list minimal. - UInt256 senderNonce = tx.IsContractCreation ? stateReader.GetNonce(header, tx.SenderAddress) : UInt256.Zero; - Address recipient = tx.GetRecipient(senderNonce); - Address[] resultOpt = new Address[3 + precompileCount]; - resultOpt[0] = tx.SenderAddress; - resultOpt[1] = recipient; - resultOpt[2] = header.GasBeneficiary; - int j = 3; foreach (AddressAsKey p in precompiles) - resultOpt[j++] = p.Value; - return resultOpt; + buffer[idx++] = p.Value; } private static bool HasConverged(AccessList? previous, AccessList? discovered) From 84f07e20efdc6db77c85dedc2a584f348c8748d5 Mon Sep 17 00:00:00 2001 From: ManuelArto Date: Fri, 8 May 2026 13:17:10 +0200 Subject: [PATCH 17/18] fix(StackAccessTracker): add explicit constructor --- src/Nethermind/Nethermind.Evm/StackAccessTracker.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs index 1428f3e92d1a..e9f37761f45d 100644 --- a/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs +++ b/src/Nethermind/Nethermind.Evm/StackAccessTracker.cs @@ -11,8 +11,10 @@ namespace Nethermind.Evm; -public struct StackAccessTracker(bool isTracingAccess = false) : IDisposable +public struct StackAccessTracker(bool isTracingAccess) : IDisposable { + public StackAccessTracker() : this(false) { } + public readonly JournalSet
AccessedAddresses => _trackingState.AccessedAddresses; public readonly JournalSet AccessedStorageCells => _trackingState.AccessedStorageCells; public readonly JournalCollection Logs => _trackingState.Logs; From 31f98a48838fadf239871a72eab050406361cc9f Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Sat, 9 May 2026 10:20:19 +0200 Subject: [PATCH 18/18] 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) --- .../BlockchainBridgeTests.cs | 69 +++++-------------- 1 file changed, 19 insertions(+), 50 deletions(-) diff --git a/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs b/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs index a068ac1da2a2..db5f4dc9747b 100644 --- a/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs +++ b/src/Nethermind/Nethermind.Facade.Test/BlockchainBridgeTests.cs @@ -312,27 +312,14 @@ public void BlobBaseFee_is_set_for_non_blob_transaction([ValueSource(nameof(Brid Arg.Is(blkCtx => blkCtx.BlobBaseFee == expectedBlobBaseFeeHash)); } - [Test] - public void CreateAccessList_filters_precompile_addresses_with_empty_storage_keys() + [TestCase(true)] + [TestCase(false)] + public void CreateAccessList_filters_precompile_addresses_with_empty_storage_keys(bool optimize) { - BlockHeader header = Build.A.BlockHeader.TestObject; - Transaction tx = Build.A.Transaction - .WithSenderAddress(TestItem.AddressA) - .WithTo(TestItem.AddressB) - .TestObject; - - _transactionProcessor.CallAndRestore(Arg.Any(), Arg.Any()) - .Returns(callInfo => - { - ITxTracer tracer = callInfo.ArgAt(1); - tracer.ReportAccess( - [PrecompiledAddresses.ECRecover, TestItem.AddressC], - [new StorageCell(TestItem.AddressC, UInt256.One)]); - tracer.MarkAsSuccess(TestItem.AddressB, new GasConsumed(21000, 0), Array.Empty(), Array.Empty()); - return TransactionResult.Ok; - }); - - CallOutput callOutput = _blockchainBridge.CreateAccessList(header, tx, null, true, null, default); + CallOutput callOutput = InvokeCreateAccessListWithMockedAccess( + optimize, + [PrecompiledAddresses.ECRecover, TestItem.AddressC], + [new StorageCell(TestItem.AddressC, UInt256.One)]); callOutput.AccessList.Should().NotBeNull(); callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeFalse(); @@ -340,34 +327,21 @@ public void CreateAccessList_filters_precompile_addresses_with_empty_storage_key } [Test] - public void CreateAccessList_filters_precompile_addresses_with_empty_storage_keys_when_optimize_is_false() + public void CreateAccessList_keeps_precompile_address_when_storage_key_is_present() { - BlockHeader header = Build.A.BlockHeader.TestObject; - Transaction tx = Build.A.Transaction - .WithSenderAddress(TestItem.AddressA) - .WithTo(TestItem.AddressB) - .TestObject; - - _transactionProcessor.CallAndRestore(Arg.Any(), Arg.Any()) - .Returns(callInfo => - { - ITxTracer tracer = callInfo.ArgAt(1); - tracer.ReportAccess( - [PrecompiledAddresses.ECRecover, TestItem.AddressC], - [new StorageCell(TestItem.AddressC, UInt256.One)]); - tracer.MarkAsSuccess(TestItem.AddressB, new GasConsumed(21000, 0), Array.Empty(), Array.Empty()); - return TransactionResult.Ok; - }); - - CallOutput callOutput = _blockchainBridge.CreateAccessList(header, tx, null, false, null, default); + CallOutput callOutput = InvokeCreateAccessListWithMockedAccess( + optimize: false, + [PrecompiledAddresses.ECRecover], + [new StorageCell(PrecompiledAddresses.ECRecover, UInt256.One)]); callOutput.AccessList.Should().NotBeNull(); - callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeFalse(); - callOutput.AccessList.Any(e => e.Address == TestItem.AddressC).Should().BeTrue(); + callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeTrue(); } - [Test] - public void CreateAccessList_keeps_precompile_address_when_storage_key_is_present() + private CallOutput InvokeCreateAccessListWithMockedAccess( + bool optimize, + Address[] accessedAddresses, + StorageCell[] accessedCells) { BlockHeader header = Build.A.BlockHeader.TestObject; Transaction tx = Build.A.Transaction @@ -379,17 +353,12 @@ public void CreateAccessList_keeps_precompile_address_when_storage_key_is_presen .Returns(callInfo => { ITxTracer tracer = callInfo.ArgAt(1); - tracer.ReportAccess( - [PrecompiledAddresses.ECRecover], - [new StorageCell(PrecompiledAddresses.ECRecover, UInt256.One)]); + tracer.ReportAccess(accessedAddresses, accessedCells); tracer.MarkAsSuccess(TestItem.AddressB, new GasConsumed(21000, 0), Array.Empty(), Array.Empty()); return TransactionResult.Ok; }); - CallOutput callOutput = _blockchainBridge.CreateAccessList(header, tx, null, false, null, default); - - callOutput.AccessList.Should().NotBeNull(); - callOutput.AccessList!.Any(e => e.Address == PrecompiledAddresses.ECRecover).Should().BeTrue(); + return _blockchainBridge.CreateAccessList(header, tx, null, optimize, null, default); } [Test]