diff --git a/src/Nethermind/Nethermind.Blockchain.Test/TransactionProcessorEip7702Tests.cs b/src/Nethermind/Nethermind.Blockchain.Test/TransactionProcessorEip7702Tests.cs index 73267a3f94a2..68699dd65507 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/TransactionProcessorEip7702Tests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/TransactionProcessorEip7702Tests.cs @@ -21,6 +21,8 @@ using Nethermind.Blockchain.Tracing; using Nethermind.Core.Test; using Nethermind.Int256; +using Nethermind.Evm.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; namespace Nethermind.Evm.Test; @@ -491,7 +493,7 @@ public void Execute_FirstTxHasAuthorizedCodeThatIncrementsAndSecondDoesNot_Stora .WithTransactions(tx1, tx2) .WithGasLimit(10000000).TestObject; - var blkCtx = new BlockExecutionContext(block.Header, _specProvider.GetSpec(block.Header)); + BlockExecutionContext blkCtx = new(block.Header, _specProvider.GetSpec(block.Header)); _transactionProcessor.Execute(tx1, blkCtx, NullTxTracer.Instance); _transactionProcessor.Execute(tx2, blkCtx, NullTxTracer.Instance); @@ -944,7 +946,7 @@ public void Execute_SetNormalDelegationAndThenSetDelegationWithZeroAddress_Accou .WithTimestamp(MainnetSpecProvider.PragueBlockTimestamp) .WithTransactions(tx) .WithGasLimit(10000000).TestObject; - var blkCtx = new BlockExecutionContext(block.Header, _specProvider.GetSpec(block.Header)); + BlockExecutionContext blkCtx = new(block.Header, _specProvider.GetSpec(block.Header)); _transactionProcessor.Execute(tx, blkCtx, NullTxTracer.Instance); _stateProvider.CommitTree(block.Number); @@ -1018,6 +1020,191 @@ public void Execute_EXTCODESIZEOnDelegatedThatTriggersOptimization_ReturnsZeroIf Assert.That(tracer.ReturnValue, Is.EquivalentTo(new byte[] { Convert.ToByte(!isDelegated) })); } + [TestCase(Instruction.EXTCODESIZE)] + [TestCase(Instruction.EXTCODECOPY)] + public void Execute_ExtCode_WhenCodeChangesWithinBlock_ReturnsUpdatedValue(Instruction instruction) + { + PrivateKey sender = TestItem.PrivateKeyA; + Address inspectedAddress = TestItem.AddressB; + Address codeSource = TestItem.AddressC; + _stateProvider.CreateAccount(sender.Address, 1.Ether()); + + byte[] initialInspectedCode = Prepare.EvmCode.Op(Instruction.ADD).Done; + byte[] updatedInspectedCode = Prepare.EvmCode.Op(Instruction.MUL).Done; + DeployCode(inspectedAddress, initialInspectedCode); + + byte[] readerCode = instruction == Instruction.EXTCODESIZE + ? Prepare.EvmCode + .PushData(inspectedAddress) + .Op(Instruction.EXTCODESIZE) + .Op(Instruction.PUSH0) + .Op(Instruction.MSTORE8) + .PushData(1) + .Op(Instruction.PUSH0) + .Op(Instruction.RETURN) + .Done + : Prepare.EvmCode + .PushData(1) + .Op(Instruction.PUSH0) + .Op(Instruction.PUSH0) + .PushData(inspectedAddress) + .Op(Instruction.EXTCODECOPY) + .PushData(1) + .Op(Instruction.PUSH0) + .Op(Instruction.RETURN) + .Done; + DeployCode(codeSource, readerCode); + _stateProvider.Commit(Prague.Instance, true); + + Block block = Build.A.Block.WithNumber(long.MaxValue) + .WithTimestamp(MainnetSpecProvider.PragueBlockTimestamp) + .WithGasLimit(10_000_000) + .TestObject; + BlockExecutionContext blkCtx = new(block.Header, _specProvider.GetSpec(block.Header)); + + // EXTCODESIZE returns the code length; EXTCODECOPY returns the first byte of code + byte expectedInitial = instruction == Instruction.EXTCODESIZE + ? (byte)initialInspectedCode.Length + : (byte)Instruction.ADD; + byte expectedUpdated = instruction == Instruction.EXTCODESIZE + ? (byte)updatedInspectedCode.Length + : (byte)Instruction.MUL; + + CallOutputTracer firstTracer = ExecuteCallWithOutput(sender, codeSource, blkCtx, 0); + Assert.That(firstTracer.ReturnValue?.ToArray(), Is.EquivalentTo(new byte[] { expectedInitial })); + + DeployCode(inspectedAddress, updatedInspectedCode); + _stateProvider.Commit(Prague.Instance, true); + + CallOutputTracer secondTracer = ExecuteCallWithOutput(sender, codeSource, blkCtx, 1); + Assert.That(secondTracer.ReturnValue?.ToArray(), Is.EquivalentTo(new byte[] { expectedUpdated })); + } + + [Test] + [NonParallelizable] + public void Execute_EXTCODESIZE_WhenCacheIsFull_ExistingKeyIsRefreshed() + { + int previousMaxEntries = VirtualMachine.MaxExtCodeCacheEntries; + VirtualMachine.SetMaxExtCodeCacheEntries(1); + try + { + PrivateKey sender = TestItem.PrivateKeyA; + Address inspectedAddress = TestItem.AddressB; + Address codeSource = TestItem.AddressC; + _stateProvider.CreateAccount(sender.Address, 1.Ether()); + + byte[] initialInspectedCode = Prepare.EvmCode + .Op(Instruction.STOP) + .Done; + byte[] updatedInspectedCode = Prepare.EvmCode + .Op(Instruction.ADD) + .Op(Instruction.STOP) + .Done; + DeployCode(inspectedAddress, initialInspectedCode); + + byte[] extcodesizeReaderCode = Prepare.EvmCode + .PushData(inspectedAddress) + .Op(Instruction.EXTCODESIZE) + .Op(Instruction.PUSH0) + .Op(Instruction.MSTORE8) + .PushData(1) + .Op(Instruction.PUSH0) + .Op(Instruction.RETURN) + .Done; + DeployCode(codeSource, extcodesizeReaderCode); + _stateProvider.Commit(Prague.Instance, true); + + TrackingCodeInfoRepository trackingRepository = new(new EthereumCodeInfoRepository(_stateProvider), inspectedAddress); + EthereumVirtualMachine virtualMachine = new(new TestBlockhashProvider(_specProvider), _specProvider, LimboLogs.Instance); + ITransactionProcessor transactionProcessor = new EthereumTransactionProcessor( + BlobBaseFeeCalculator.Instance, + _specProvider, + _stateProvider, + virtualMachine, + trackingRepository, + LimboLogs.Instance); + + Block block = Build.A.Block.WithNumber(long.MaxValue) + .WithTimestamp(MainnetSpecProvider.PragueBlockTimestamp) + .WithGasLimit(10_000_000) + .TestObject; + BlockExecutionContext blkCtx = new(block.Header, _specProvider.GetSpec(block.Header)); + + _ = ExecuteCallWithOutput(transactionProcessor, sender, codeSource, blkCtx, 0); + + DeployCode(inspectedAddress, updatedInspectedCode); + _stateProvider.Commit(Prague.Instance, true); + + int before = trackingRepository.TrackedHashLookupCount; + _ = ExecuteCallWithOutput(transactionProcessor, sender, codeSource, blkCtx, 1); + int afterFirst = trackingRepository.TrackedHashLookupCount; + _ = ExecuteCallWithOutput(transactionProcessor, sender, codeSource, blkCtx, 2); + int afterSecond = trackingRepository.TrackedHashLookupCount; + + Assert.That(afterFirst - before, Is.EqualTo(1)); + Assert.That(afterSecond - afterFirst, Is.EqualTo(0)); + } + finally + { + VirtualMachine.SetMaxExtCodeCacheEntries(previousMaxEntries); + } + } + + private CallOutputTracer ExecuteCallWithOutput(PrivateKey sender, Address to, BlockExecutionContext blockExecutionContext, ulong nonce) + { + return ExecuteCallWithOutput(_transactionProcessor, sender, to, blockExecutionContext, nonce); + } + + private CallOutputTracer ExecuteCallWithOutput( + ITransactionProcessor transactionProcessor, + PrivateKey sender, + Address to, + BlockExecutionContext blockExecutionContext, + ulong nonce) + { + Transaction tx = Build.A.Transaction + .WithType(TxType.EIP1559) + .WithNonce(nonce) + .WithTo(to) + .WithGasLimit(100_000) + .SignedAndResolved(_ethereumEcdsa, sender, true) + .TestObject; + + CallOutputTracer tracer = new(); + _ = transactionProcessor.Execute(tx, blockExecutionContext, tracer); + return tracer; + } + + private sealed class TrackingCodeInfoRepository(ICodeInfoRepository inner, Address trackedAddress) : ICodeInfoRepository + { + public int TrackedHashLookupCount { get; private set; } + + public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IReleaseSpec vmSpec, out Address? delegationAddress) + => inner.GetCachedCodeInfo(codeSource, followDelegation, vmSpec, out delegationAddress); + + public CodeInfo GetCachedCodeInfo(Address codeSource, in ValueHash256 codeHash, IReleaseSpec vmSpec) + { + if (codeSource == trackedAddress) + { + TrackedHashLookupCount++; + } + + return inner.GetCachedCodeInfo(codeSource, in codeHash, vmSpec); + } + + public ValueHash256 GetExecutableCodeHash(Address address, IReleaseSpec spec) + => inner.GetExecutableCodeHash(address, spec); + + public void InsertCode(ReadOnlyMemory code, Address codeOwner, IReleaseSpec spec) + => inner.InsertCode(code, codeOwner, spec); + + public void SetDelegation(Address codeSource, Address authority, IReleaseSpec spec) + => inner.SetDelegation(codeSource, authority, spec); + + public bool TryGetDelegation(Address address, IReleaseSpec spec, [NotNullWhen(true)] out Address? delegatedAddress) + => inner.TryGetDelegation(address, spec, out delegatedAddress); + } + private void DeployCode(Address codeSource, byte[] code) { _stateProvider.CreateAccountIfNotExists(codeSource, 0); diff --git a/src/Nethermind/Nethermind.Blockchain/CachedCodeInfoRepository.cs b/src/Nethermind/Nethermind.Blockchain/CachedCodeInfoRepository.cs index cdbd1857d4fb..860ea5ae1d09 100644 --- a/src/Nethermind/Nethermind.Blockchain/CachedCodeInfoRepository.cs +++ b/src/Nethermind/Nethermind.Blockchain/CachedCodeInfoRepository.cs @@ -28,7 +28,7 @@ public class CachedCodeInfoRepository( public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IReleaseSpec vmSpec, out Address? delegationAddress) { - if (vmSpec.IsPrecompile(codeSource) && _cachedPrecompile.TryGetValue(codeSource, out var cachedCodeInfo)) + if (TryGetCachedPrecompile(codeSource, vmSpec, out CodeInfo cachedCodeInfo)) { delegationAddress = null; return cachedCodeInfo; @@ -36,6 +36,11 @@ public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IRe return baseCodeInfoRepository.GetCachedCodeInfo(codeSource, followDelegation, vmSpec, out delegationAddress); } + public CodeInfo GetCachedCodeInfo(Address codeSource, in ValueHash256 codeHash, IReleaseSpec vmSpec) => + TryGetCachedPrecompile(codeSource, vmSpec, out CodeInfo cachedCodeInfo) + ? cachedCodeInfo + : baseCodeInfoRepository.GetCachedCodeInfo(codeSource, in codeHash, vmSpec); + public ValueHash256 GetExecutableCodeHash(Address address, IReleaseSpec spec) { return baseCodeInfoRepository.GetExecutableCodeHash(address, spec); @@ -57,6 +62,18 @@ public bool TryGetDelegation(Address address, IReleaseSpec spec, return baseCodeInfoRepository.TryGetDelegation(address, spec, out delegatedAddress); } + private bool TryGetCachedPrecompile(Address codeSource, IReleaseSpec vmSpec, [NotNullWhen(true)] out CodeInfo? cachedCodeInfo) + { + if (vmSpec.IsPrecompile(codeSource) && _cachedPrecompile.TryGetValue(codeSource, out CodeInfo precompileCodeInfo)) + { + cachedCodeInfo = precompileCodeInfo; + return true; + } + + cachedCodeInfo = null; + return false; + } + private static CodeInfo CreateCachedPrecompile( in KeyValuePair originalPrecompile, ConcurrentDictionary> cache) diff --git a/src/Nethermind/Nethermind.Config/BlocksConfig.cs b/src/Nethermind/Nethermind.Config/BlocksConfig.cs index 1eeba56cc523..193c44d5ce6a 100644 --- a/src/Nethermind/Nethermind.Config/BlocksConfig.cs +++ b/src/Nethermind/Nethermind.Config/BlocksConfig.cs @@ -61,6 +61,8 @@ private static string GetDefaultVersionExtraData() public bool CachePrecompilesOnBlockProcessing { get; set; } = true; + public int ExtCodeCacheEntries { get; set; } = 1024; + public int PreWarmStateConcurrency { get; set; } = 0; public int BlockProductionTimeoutMs { get; set; } = 4_000; diff --git a/src/Nethermind/Nethermind.Config/IBlocksConfig.cs b/src/Nethermind/Nethermind.Config/IBlocksConfig.cs index 5706b1b458fe..44d26cbee88d 100644 --- a/src/Nethermind/Nethermind.Config/IBlocksConfig.cs +++ b/src/Nethermind/Nethermind.Config/IBlocksConfig.cs @@ -43,6 +43,9 @@ public interface IBlocksConfig : IConfig [ConfigItem(Description = "Whether to cache precompile results when processing blocks.", DefaultValue = "True", HiddenFromDocs = true)] bool CachePrecompilesOnBlockProcessing { get; set; } + [ConfigItem(Description = "The max entries in the EXTCODE* cache. Set to 0 to disable.", DefaultValue = "1024", HiddenFromDocs = true)] + int ExtCodeCacheEntries { get; set; } + [ConfigItem(Description = "Specify pre-warm state concurrency. Default is logical processor - 1.", DefaultValue = "0", HiddenFromDocs = true)] int PreWarmStateConcurrency { get; set; } diff --git a/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs b/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs index 2b7d9bce0e6d..d927acd2f75c 100644 --- a/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs +++ b/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs @@ -31,9 +31,9 @@ public CodeInfoRepository(IWorldState worldState, IPrecompileProvider precompile public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IReleaseSpec vmSpec, out Address? delegationAddress) { delegationAddress = null; - if (vmSpec.IsPrecompile(codeSource)) // _localPrecompiles have to have all precompiles + if (TryGetPrecompileCodeInfo(codeSource, vmSpec, out CodeInfo precompileCodeInfo)) { - return _localPrecompiles[codeSource]; + return precompileCodeInfo; } CodeInfo cachedCodeInfo = InternalGetCachedCode(_worldState, codeSource, vmSpec); @@ -47,6 +47,23 @@ public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IRe return cachedCodeInfo; } + public CodeInfo GetCachedCodeInfo(Address codeSource, in ValueHash256 codeHash, IReleaseSpec vmSpec) => + TryGetPrecompileCodeInfo(codeSource, vmSpec, out CodeInfo precompileCodeInfo) + ? precompileCodeInfo + : InternalGetCachedCode(_worldState, in codeHash, vmSpec); + + private bool TryGetPrecompileCodeInfo(Address codeSource, IReleaseSpec vmSpec, [NotNullWhen(true)] out CodeInfo? precompileCodeInfo) + { + if (vmSpec.IsPrecompile(codeSource)) // _localPrecompiles have to have all precompiles + { + precompileCodeInfo = _localPrecompiles[codeSource]; + return true; + } + + precompileCodeInfo = null; + return false; + } + internal static void Clear() => _codeCache.Clear(); private CodeInfo InternalGetCachedCode(Address codeSource, IReleaseSpec vmSpec) @@ -197,4 +214,3 @@ internal void Clear() } } } - diff --git a/src/Nethermind/Nethermind.Evm/ICodeInfoRepository.cs b/src/Nethermind/Nethermind.Evm/ICodeInfoRepository.cs index 20cedaa8233d..ce0d7c543630 100644 --- a/src/Nethermind/Nethermind.Evm/ICodeInfoRepository.cs +++ b/src/Nethermind/Nethermind.Evm/ICodeInfoRepository.cs @@ -12,6 +12,7 @@ namespace Nethermind.Evm; public interface ICodeInfoRepository { CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IReleaseSpec vmSpec, out Address? delegationAddress); + CodeInfo GetCachedCodeInfo(Address codeSource, in ValueHash256 codeHash, IReleaseSpec vmSpec); ValueHash256 GetExecutableCodeHash(Address address, IReleaseSpec spec); void InsertCode(ReadOnlyMemory code, Address codeOwner, IReleaseSpec spec); void SetDelegation(Address codeSource, Address authority, IReleaseSpec spec); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs index 99c1b931bde0..340383bbe131 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs @@ -166,8 +166,7 @@ public static EvmExceptionType InstructionExtCodeCopy( if (!TGasPolicy.UpdateMemoryCost(ref gas, in a, result, vm.VmState)) goto OutOfGas; - CodeInfo codeInfo = vm.CodeInfoRepository - .GetCachedCodeInfo(address, followDelegation: false, spec, out _); + CodeInfo codeInfo = vm.GetExtCodeInfoCached(address, spec); // Get the external code from the repository. ReadOnlySpan externalCode = codeInfo.CodeSpan; @@ -180,7 +179,7 @@ public static EvmExceptionType InstructionExtCodeCopy( } // If EOF is enabled and the code is an EOF contract, use a predefined magic value. - if (spec.IsEofEnabled && EofValidator.IsEof(externalCode, out _)) + if (spec.IsEofEnabled && codeInfo is EofCodeInfo) { externalCode = EofValidator.MAGIC; } @@ -293,20 +292,8 @@ public static EvmExceptionType InstructionExtCodeSize( } } - // No optimization applied: load the account's code from storage. - ReadOnlySpan accountCode = vm.CodeInfoRepository - .GetCachedCodeInfo(address, followDelegation: false, spec, out _) - .CodeSpan; - // If EOF is enabled and the code is an EOF contract, push a fixed size (2). - if (spec.IsEofEnabled && EofValidator.IsEof(accountCode, out _)) - { - stack.PushUInt32(2); - } - else - { - // Otherwise, push the actual code length. - stack.PushUInt32((uint)accountCode.Length); - } + uint codeSize = vm.GetExtCodeSizeCached(address, spec); + stack.PushUInt32(codeSize); return EvmExceptionType.None; // Jump forward to be unpredicted by the branch predictor. OutOfGas: diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Environment.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Environment.cs index 7e770d5ec07b..f1d8c4e09599 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Environment.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Environment.cs @@ -6,6 +6,7 @@ using Nethermind.Core; using Nethermind.Core.Specs; using Nethermind.Core.Crypto; +using Nethermind.Evm.CodeAnalysis; using Nethermind.Evm.EvmObjectFormat; using Nethermind.Evm.GasPolicy; using Nethermind.Evm.State; @@ -676,16 +677,23 @@ public static EvmExceptionType InstructionExtCodeHashEof code = state.GetCode(address); - // If the code passes EOF validation, push the EOF-specific hash. - if (EofValidator.IsEof(code, out _)) + ref readonly ValueHash256 codeHash = ref state.GetCodeHash(address); + if (codeHash == ValueKeccak.OfAnEmptyString) + { + stack.Push32Bytes(in codeHash); + return EvmExceptionType.None; + } + + CodeInfo codeInfo = vm.CodeInfoRepository.GetCachedCodeInfo(address, in codeHash, spec); + // If the code is EOF, push the EOF-specific hash. + if (codeInfo is EofCodeInfo) { stack.PushBytes(EofHash256); } else { // Otherwise, push the standard code hash. - stack.PushBytes(state.GetCodeHash(address).Bytes); + stack.Push32Bytes(in codeHash); } } diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.ExtCodeCache.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.ExtCodeCache.cs new file mode 100644 index 000000000000..d38735f922ed --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.ExtCodeCache.cs @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.Evm.CodeAnalysis; +using Nethermind.Evm.EvmObjectFormat; + +namespace Nethermind.Evm; + +public unsafe partial class VirtualMachine +{ + private const int DefaultMaxExtCodeCacheEntries = 1024; + private static int _maxExtCodeCacheEntries = DefaultMaxExtCodeCacheEntries; + private Dictionary? _extCodeCache; + private long _extCodeCacheBlockNumber = long.MinValue; + + /// + /// Gets the maximum number of entries kept in the EXTCODE* cache. + /// + public static int MaxExtCodeCacheEntries => _maxExtCodeCacheEntries; + + /// + /// Sets the maximum number of entries kept in the EXTCODE* cache. + /// Use 0 to disable the cache. + /// + /// The maximum number of cache entries. Must be non-negative. + /// Thrown when is negative. + public static void SetMaxExtCodeCacheEntries(int maxEntries) + { + ArgumentOutOfRangeException.ThrowIfNegative(maxEntries); + _maxExtCodeCacheEntries = maxEntries; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal uint GetExtCodeSizeCached(Address address, IReleaseSpec spec) + => ResolveExtCodeCacheEntry(address, spec).CodeSize; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ResetExtCodeCache() + { + // Cache is keyed by address, with code hash validation to remain correct when code changes mid-transaction. + _extCodeCache?.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ResetExtCodeCacheForBlock() + { + long blockNumber = BlockExecutionContext.Header.Number; + if (_extCodeCacheBlockNumber != blockNumber) + { + _extCodeCacheBlockNumber = blockNumber; + ResetExtCodeCache(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal CodeInfo GetExtCodeInfoCached(Address address, IReleaseSpec spec) + => ResolveExtCodeCacheEntry(address, spec).CodeInfo; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ExtCodeCacheEntry ResolveExtCodeCacheEntry(Address address, IReleaseSpec spec) + { + if (_maxExtCodeCacheEntries == 0) + { + CodeInfo uncachedCodeInfo = _codeInfoRepository.GetCachedCodeInfo(address, followDelegation: false, spec, out _); + return CreateExtCodeCacheEntry(default, uncachedCodeInfo); + } + + _extCodeCache ??= new Dictionary(8); + AddressAsKey key = address; + + ValueHash256 codeHash = _worldState.GetCodeHash(address); + if (_extCodeCache.TryGetValue(key, out ExtCodeCacheEntry entry) && entry.CodeHash == codeHash) + { + return entry; + } + + CodeInfo codeInfo = _codeInfoRepository.GetCachedCodeInfo(address, in codeHash, spec); + ExtCodeCacheEntry refreshedEntry = CreateExtCodeCacheEntry(in codeHash, codeInfo); + StoreCacheEntry(key, in refreshedEntry); + return refreshedEntry; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ExtCodeCacheEntry CreateExtCodeCacheEntry(in ValueHash256 codeHash, CodeInfo codeInfo) + { + uint codeSize = codeInfo is EofCodeInfo ? (uint)EofValidator.MAGIC.Length : (uint)codeInfo.CodeSpan.Length; + return new ExtCodeCacheEntry(codeHash, codeSize, codeInfo); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void StoreCacheEntry(AddressAsKey key, in ExtCodeCacheEntry entry) + { + if (_maxExtCodeCacheEntries == 0) + { + return; + } + + Dictionary extCodeCache = _extCodeCache!; + + // Under high-cardinality workloads, clearing the whole dictionary causes avoidable churn. + // Once capacity is reached, keep current hot set and skip admitting new keys. + // Still allow updating existing keys so hot entries can be refreshed. + if (extCodeCache.Count >= _maxExtCodeCacheEntries && !extCodeCache.ContainsKey(key)) + { + return; + } + + extCodeCache[key] = entry; + } + + private readonly record struct ExtCodeCacheEntry(ValueHash256 CodeHash, uint CodeSize, CodeInfo CodeInfo); +} diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index af066f0cb9e5..d7a94f10a59d 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -164,6 +164,7 @@ public TransactionSubstate ExecuteTransaction( OpCodeCount = 0; // Initialize the code repository and set up the initial execution state. _codeInfoRepository = TxExecutionContext.CodeInfoRepository; + ResetExtCodeCacheForBlock(); _currentState = vmState; _previousCallResult = null; _previousCallOutputDestination = UInt256.Zero; diff --git a/src/Nethermind/Nethermind.Init/Modules/BlockProcessingModule.cs b/src/Nethermind/Nethermind.Init/Modules/BlockProcessingModule.cs index 22c2b4a8214e..21765cfc0635 100644 --- a/src/Nethermind/Nethermind.Init/Modules/BlockProcessingModule.cs +++ b/src/Nethermind/Nethermind.Init/Modules/BlockProcessingModule.cs @@ -38,6 +38,8 @@ public class BlockProcessingModule(IInitConfig initConfig, IBlocksConfig blocksC { protected override void Load(ContainerBuilder builder) { + VirtualMachine.SetMaxExtCodeCacheEntries(blocksConfig.ExtCodeCacheEntries); + builder // Validators .AddSingleton((spec) => new TxValidator(spec.ChainId)) diff --git a/src/Nethermind/Nethermind.State/OverridableEnv/OverridableCodeInfoRepository.cs b/src/Nethermind/Nethermind.State/OverridableEnv/OverridableCodeInfoRepository.cs index 7b1a1da5dd5a..2ab3c32091c7 100644 --- a/src/Nethermind/Nethermind.State/OverridableEnv/OverridableCodeInfoRepository.cs +++ b/src/Nethermind/Nethermind.State/OverridableEnv/OverridableCodeInfoRepository.cs @@ -21,20 +21,38 @@ public class OverridableCodeInfoRepository(ICodeInfoRepository codeInfoRepositor public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IReleaseSpec vmSpec, out Address? delegationAddress) { delegationAddress = null; - if (_precompileOverrides.TryGetValue(codeSource, out var precompile)) return precompile.codeInfo; + if (TryGetPrecompileOverride(codeSource, out CodeInfo precompileCodeInfo)) + { + return precompileCodeInfo; + } - if (_codeOverrides.TryGetValue(codeSource, out var result)) + if (TryGetCodeOverride(codeSource, out var overrideInfo)) { - return !result.codeInfo.IsEmpty && - ICodeInfoRepository.TryGetDelegatedAddress(result.codeInfo.CodeSpan, out delegationAddress) && + return !overrideInfo.codeInfo.IsEmpty && + ICodeInfoRepository.TryGetDelegatedAddress(overrideInfo.codeInfo.CodeSpan, out delegationAddress) && followDelegation ? GetCachedCodeInfo(delegationAddress, false, vmSpec, out Address? _) - : result.codeInfo; + : overrideInfo.codeInfo; } return codeInfoRepository.GetCachedCodeInfo(codeSource, followDelegation, vmSpec, out delegationAddress); } + public CodeInfo GetCachedCodeInfo(Address codeSource, in ValueHash256 codeHash, IReleaseSpec vmSpec) + { + if (TryGetPrecompileOverride(codeSource, out CodeInfo precompileCodeInfo)) + { + return precompileCodeInfo; + } + + if (TryGetCodeOverride(codeSource, out var overrideInfo)) + { + return overrideInfo.codeInfo; + } + + return codeInfoRepository.GetCachedCodeInfo(codeSource, in codeHash, vmSpec); + } + public void InsertCode(ReadOnlyMemory code, Address codeOwner, IReleaseSpec spec) => codeInfoRepository.InsertCode(code, codeOwner, spec); @@ -69,6 +87,21 @@ public ValueHash256 GetExecutableCodeHash(Address address, IReleaseSpec spec) => ? result.codeHash : codeInfoRepository.GetExecutableCodeHash(address, spec); + private bool TryGetPrecompileOverride(Address codeSource, [NotNullWhen(true)] out CodeInfo? precompileCodeInfo) + { + if (_precompileOverrides.TryGetValue(codeSource, out (CodeInfo codeInfo, Address _) precompileOverride)) + { + precompileCodeInfo = precompileOverride.codeInfo; + return true; + } + + precompileCodeInfo = null; + return false; + } + + private bool TryGetCodeOverride(Address codeSource, out (CodeInfo codeInfo, ValueHash256 codeHash) codeOverride) + => _codeOverrides.TryGetValue(codeSource, out codeOverride); + public void ResetOverrides() { _precompileOverrides.Clear();