From b540d086f7238dbd5a38012c33c3b509764c7d69 Mon Sep 17 00:00:00 2001 From: Alexey Osipov Date: Tue, 23 Jun 2026 13:48:25 +0300 Subject: [PATCH] Optimise precompile and create allocations --- .../EthereumPrecompileProviderTests.cs | 24 ++++++++++++++ .../EthereumPrecompileProvider.cs | 6 ++-- .../Nethermind.Evm/VirtualMachine.cs | 31 +++++++++++++++++-- 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 src/Nethermind/Nethermind.Blockchain.Test/EthereumPrecompileProviderTests.cs diff --git a/src/Nethermind/Nethermind.Blockchain.Test/EthereumPrecompileProviderTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/EthereumPrecompileProviderTests.cs new file mode 100644 index 000000000000..c25ac9b9054a --- /dev/null +++ b/src/Nethermind/Nethermind.Blockchain.Test/EthereumPrecompileProviderTests.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Frozen; +using Nethermind.Core; +using Nethermind.Evm.CodeAnalysis; +using NUnit.Framework; + +namespace Nethermind.Blockchain.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class EthereumPrecompileProviderTests +{ + [Test] + public void GetPrecompiles_ReturnsCachedDictionary() + { + EthereumPrecompileProvider provider = new(); + FrozenDictionary precompiles = provider.GetPrecompiles(); + FrozenDictionary precompilesAgain = provider.GetPrecompiles(); + + Assert.That(precompilesAgain, Is.SameAs(precompiles)); + } +} diff --git a/src/Nethermind/Nethermind.Blockchain/EthereumPrecompileProvider.cs b/src/Nethermind/Nethermind.Blockchain/EthereumPrecompileProvider.cs index 583406b3b832..f5f46fec9bbd 100644 --- a/src/Nethermind/Nethermind.Blockchain/EthereumPrecompileProvider.cs +++ b/src/Nethermind/Nethermind.Blockchain/EthereumPrecompileProvider.cs @@ -12,9 +12,8 @@ namespace Nethermind.Blockchain; public class EthereumPrecompileProvider() : IPrecompileProvider { - private static FrozenDictionary Precompiles - { - get => new Dictionary + private static readonly FrozenDictionary Precompiles = + new Dictionary { [ECRecoverPrecompile.Address] = new(ECRecoverPrecompile.Instance), [Sha256Precompile.Address] = new(Sha256Precompile.Instance), @@ -40,7 +39,6 @@ private static FrozenDictionary Precompiles [SecP256r1Precompile.Address] = new(SecP256r1Precompile.Instance), }.ToFrozenDictionary(); - } public FrozenDictionary GetPrecompiles() => Precompiles; } diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 3dff3674a4de..7bded3c1db89 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -92,6 +92,7 @@ public unsafe partial class VirtualMachine( private ReadOnlyMemory _returnDataBuffer = Array.Empty(); protected VmState _currentState; protected ReadOnlyMemory? _previousCallResult; + protected Address? _previousCallAddressResult; protected UInt256 _previousCallOutputDestination; public ILogger Logger => _logger; @@ -158,6 +159,7 @@ public virtual TransactionSubstate ExecuteTransaction( _codeInfoRepository = TxExecutionContext.CodeInfoRepository; _currentState = vmState; _previousCallResult = null; + _previousCallAddressResult = null; _previousCallOutputDestination = UInt256.Zero; ZeroPaddedSpan previousCallOutput = ZeroPaddedSpan.Empty; @@ -204,6 +206,7 @@ public virtual TransactionSubstate ExecuteTransaction( { callResult = ExecuteCall( _previousCallResult, + _previousCallAddressResult, previousCallOutput, _previousCallOutputDestination); } @@ -335,7 +338,8 @@ public TransactionSubstate ExecuteTransaction(VmState vmState, IWorl protected void PrepareCreateData(VmState previousState, ref ZeroPaddedSpan previousCallOutput) { - _previousCallResult = previousState.Env.ExecutingAccount.Bytes.ToArray(); + _previousCallResult = null; + _previousCallAddressResult = previousState.Env.ExecutingAccount; _previousCallOutputDestination = UInt256.Zero; ReturnDataBuffer = Array.Empty(); previousCallOutput = ZeroPaddedSpan.Empty; @@ -346,6 +350,7 @@ protected ZeroPaddedSpan HandleRegularReturn(scoped in CallResult { ZeroPaddedSpan previousCallOutput; ReturnDataBuffer = callResult.Output; + _previousCallAddressResult = null; _previousCallResult = callResult.PrecompileSuccess.HasValue ? (callResult.PrecompileSuccess.Value ? StatusCode.SuccessBytes : StatusCode.FailureBytes) : StatusCode.SuccessBytes; @@ -470,6 +475,7 @@ private void TryChargeAndDepositCode( } _previousCallResult = BytesZero; + _previousCallAddressResult = null; previousStateSucceeded = false; if (_txTracer.IsTracingActions) @@ -512,6 +518,7 @@ protected void HandleRevert(VmState previousState, in CallResult cal ReturnDataBuffer = outputBytes; _previousCallResult = StatusCode.FailureBytes; + _previousCallAddressResult = null; // Slice the output bytes, zero-padding if necessary, to match the expected output length. // This ensures that the returned data conforms to the caller's output length expectations. @@ -590,6 +597,7 @@ protected TransactionSubstate HandleFailure(Exception failure, str } _previousCallResult = StatusCode.FailureBytes; + _previousCallAddressResult = null; bool failedCreate = _currentState.ExecutionType.IsAnyCreate(); // Reset output destination and return data. @@ -722,6 +730,7 @@ protected void PrepareNextCallFrame(in CallResult callResult, ref ZeroPaddedSpan // Clear the previous call result as the execution context is moving to a new frame. _previousCallResult = null; + _previousCallAddressResult = null; // Reset the return data buffer to ensure no residual data persists across call frames. ReturnDataBuffer = Array.Empty(); @@ -772,6 +781,7 @@ protected TransactionSubstate HandleException(scoped in CallResult callResult, s } _previousCallResult = StatusCode.FailureBytes; + _previousCallAddressResult = null; bool failedCreate = _currentState.ExecutionType.IsAnyCreate(); // Reset output destination and clear return data. @@ -1091,6 +1101,9 @@ protected static string GetErrorString(IPrecompile precompile, string? error) /// An optional read-only memory buffer containing the output of a previous call; if provided, its bytes /// will be pushed onto the stack for further processing. /// + /// + /// An optional contract creation result address to push onto the stack. + /// /// /// A zero-padded span containing output from the previous call used for updating the memory state. /// @@ -1108,6 +1121,7 @@ protected static string GetErrorString(IPrecompile precompile, string? error) [SkipLocalsInit] protected CallResult ExecuteCall( ReadOnlyMemory? previousCallResult, + Address? previousCallAddressResult, ZeroPaddedSpan previousCallOutput, scoped in UInt256 previousCallOutputDestination) where TTracingInst : struct, IFlag @@ -1157,8 +1171,19 @@ protected CallResult ExecuteCall( // gas/state-gas accounting without needing interpreter-wide exception handling. ref TGasPolicy gas = ref vmState.Gas; - // If a previous call result exists, push its bytes onto the stack. - if (previousCallResult is not null) + // If a previous call or create result exists, push its bytes onto the stack. + if (previousCallAddressResult is not null) + { + EvmExceptionType pushResult = stack.PushBytes(previousCallAddressResult.Bytes); + if (pushResult != EvmExceptionType.None) return new(pushResult); + + // Report the remaining gas if tracing instructions are enabled. + if (TTracingInst.IsActive) + { + _txTracer.ReportOperationRemainingGas(TGasPolicy.GetRemainingGas(vmState.Gas)); + } + } + else if (previousCallResult is not null) { EvmExceptionType pushResult = stack.PushBytes(previousCallResult.Value.Span); if (pushResult != EvmExceptionType.None) return new(pushResult);