Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f90ae86
perf(evm): optimize extcodesize handling
kamilchodola Feb 5, 2026
4eff00a
perf(evm): reuse extcode cache for extcodehash/copy
kamilchodola Feb 5, 2026
dc7833f
perf(evm): rename extcode cache and make size configurable
kamilchodola Feb 5, 2026
3325ebf
perf(evm): scope extcode cache per block
kamilchodola Feb 5, 2026
bd8098c
Merge branch 'kch/gas-benchmarks-bdn' into perf/extcodesize-cache-bdn
kamilchodola Feb 17, 2026
44c4e48
Revert "Merge branch 'kch/gas-benchmarks-bdn' into perf/extcodesize-c…
kamilchodola Feb 17, 2026
a348e35
port non-benchmark runtime changes from bdn merge
kamilchodola Feb 17, 2026
a0a0b19
perf(evm): integrate hash-aware code info path for extcode cache
kamilchodola Feb 17, 2026
70b3b01
merge master (#10563)
kamilchodola Feb 17, 2026
eb0f4cf
Merge branch 'perf/extcodesize-cache' of https://github.com/Nethermin…
kamilchodola Feb 18, 2026
a9439f5
Merge remote-tracking branch 'origin/master' into perf/extcodesize-cache
kamilchodola Feb 18, 2026
4563cdd
fix: refresh full extcode cache entries for existing keys
kamilchodola Feb 18, 2026
68ad590
refactor: unify extcode cache resolution and override lookups
kamilchodola Feb 18, 2026
afcd509
Update src/Nethermind/Nethermind.Evm/VirtualMachine.ExtCodeCache.cs
kamilchodola Feb 18, 2026
94d449f
Merge branch 'master' into perf/extcodesize-cache
kamilchodola Feb 19, 2026
3ea784d
Merge branch 'master' into perf/extcodesize-cache
kamilchodola Feb 26, 2026
8837549
Merge branch 'master' into perf/extcodesize-cache
kamilchodola Feb 26, 2026
277a6f7
refactor: address PR review comments
kamilchodola Feb 27, 2026
9e85a89
Merge branch 'master' into perf/extcodesize-cache
smartprogrammer93 Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<byte> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,19 @@ 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;
}
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);
Expand All @@ -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<AddressAsKey, CodeInfo> originalPrecompile,
ConcurrentDictionary<PreBlockCaches.PrecompileCacheKey, Result<byte[]>> cache)
Expand Down
2 changes: 2 additions & 0 deletions src/Nethermind/Nethermind.Config/BlocksConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/Nethermind/Nethermind.Config/IBlocksConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
22 changes: 19 additions & 3 deletions src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -197,4 +214,3 @@ internal void Clear()
}
}
}

1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Evm/ICodeInfoRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> code, Address codeOwner, IReleaseSpec spec);
void SetDelegation(Address codeSource, Address authority, IReleaseSpec spec);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,7 @@ public static EvmExceptionType InstructionExtCodeCopy<TGasPolicy, TTracingInst>(
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<byte> externalCode = codeInfo.CodeSpan;
Expand All @@ -180,7 +179,7 @@ public static EvmExceptionType InstructionExtCodeCopy<TGasPolicy, TTracingInst>(
}

// 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;
}
Expand Down Expand Up @@ -293,20 +292,8 @@ public static EvmExceptionType InstructionExtCodeSize<TGasPolicy, TTracingInst>(
}
}

// No optimization applied: load the account's code from storage.
ReadOnlySpan<byte> 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<TTracingInst>(2);
}
else
{
// Otherwise, push the actual code length.
stack.PushUInt32<TTracingInst>((uint)accountCode.Length);
}
uint codeSize = vm.GetExtCodeSizeCached(address, spec);
stack.PushUInt32<TTracingInst>(codeSize);
return EvmExceptionType.None;
// Jump forward to be unpredicted by the branch predictor.
OutOfGas:
Expand Down
Loading
Loading