Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 51 additions & 0 deletions src/Nethermind/Nethermind.Abi.Test/AbiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using MathNet.Numerics;
using Nethermind.Core;
Expand Down Expand Up @@ -681,4 +682,54 @@ public void Tutorial_test()
Encoding.ASCII.GetBytes("Hello, world!"));
encoded.ToHexString().Should().BeEquivalentTo(expectedValue.ToHexString());
}

[Test]
public void AbiTypeConverter_Parses_TupleArray_Correctly()
{
// Test that "tuple[]" produces AbiArray(AbiTuple) not plain AbiTuple
string json = "\"tuple[]\"";
AbiType result = JsonSerializer.Deserialize<AbiType>(json)!;

result.Should().BeOfType<AbiArray>();
var arrayType = (AbiArray)result;
arrayType.ElementType.Should().BeOfType<AbiTuple>();
// Empty tuple since we don't have components at this level
arrayType.Name.Should().Be("()[]");
}

[Test]
public void AbiTypeConverter_Parses_FixedTupleArray_Correctly()
{
// Test that "tuple[3]" produces AbiFixedLengthArray(AbiTuple, 3)
string json = "\"tuple[3]\"";
AbiType result = JsonSerializer.Deserialize<AbiType>(json)!;

result.Should().BeOfType<AbiFixedLengthArray>();
var arrayType = (AbiFixedLengthArray)result;
arrayType.ElementType.Should().BeOfType<AbiTuple>();
arrayType.Length.Should().Be(3);
arrayType.Name.Should().Be("()[3]");
}

[Test]
public void AbiTypeConverter_Parses_PlainTuple_Correctly()
{
// Test that "tuple" still produces AbiTuple (not array)
string json = "\"tuple\"";
AbiType result = JsonSerializer.Deserialize<AbiType>(json)!;

result.Should().BeOfType<AbiTuple>();
result.Name.Should().Be("()");
}

[Test]
public void AbiTuple_Name_Reflects_Elements()
{
// Test that AbiTuple with elements has correct Name for signature generation
var tuple = new AbiTuple(AbiType.UInt8, AbiType.UInt64);
tuple.Name.Should().Be("(uint8,uint64)");

var tupleArray = new AbiArray(tuple);
tupleArray.Name.Should().Be("(uint8,uint64)[]");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ object[] GetTestDataWithException(string type, Exception exception, object[] com

yield return new TestCaseData(GetTestData("tuple", new AbiTuple<CustomAbiType>(),
new { name = "c", type = "int32" }));

// Tuple array tests
yield return new TestCaseData(GetTestData("tuple[]", new AbiArray(new AbiTuple([]))));
yield return new TestCaseData(GetTestData("tuple[3]", new AbiFixedLengthArray(new AbiTuple([]), 3)));
yield return new TestCaseData(GetTestData("tuple[]",
new AbiArray(new AbiTuple(new AbiType[] { new AbiUInt(8), new AbiUInt(64) })),
new { name = "resource", type = "uint8" },
new { name = "weight", type = "uint64" }));
yield return new TestCaseData(GetTestData("tuple[2]",
new AbiFixedLengthArray(new AbiTuple(new AbiType[] { AbiType.Address, AbiType.UInt256 }), 2),
new { name = "addr", type = "address" },
new { name = "amount", type = "uint256" }));

yield return new TestCaseData(GetTestDataWithException("int1", new ArgumentOutOfRangeException()));
yield return new TestCaseData(GetTestDataWithException("int9", new ArgumentOutOfRangeException()));
yield return new TestCaseData(GetTestDataWithException("int300", new ArgumentOutOfRangeException()));
Expand Down
4 changes: 4 additions & 0 deletions src/Nethermind/Nethermind.Abi/AbiTuple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public override byte[] Encode(object? arg, bool packed)

private static Type GetCSharpType(AbiType[] elements)
{
// Handle empty tuple - return non-generic ValueTuple
if (elements.Length == 0)
return typeof(ValueTuple);

Type genericType = Type.GetType("System.ValueTuple`" + elements.Length)!;
Type[] typeArguments = elements.Select(static v => v.CSharpType).ToArray();
return genericType.MakeGenericType(typeArguments);
Expand Down
6 changes: 3 additions & 3 deletions src/Nethermind/Nethermind.Abi/AbiType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ public class AbiTypeConverter : JsonConverter<AbiType>

static AbiType ParseAbiType(string type)
{
if (type == "tuple" || type.StartsWith("tuple["))
{
// Handle plain "tuple" without array suffix
// Note: "tuple[]" and "tuple[N]" fall through to array handling below
if (type == "tuple")
return new AbiTuple();
}

// Check for array suffix: [N] for fixed-size or [] for dynamic
int lastBracket = type.LastIndexOf('[');
Expand Down
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Blockchain/BlockTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1760,5 +1760,6 @@ public void ForkChoiceUpdated(Hash256? finalizedBlockHash, Hash256? safeBlockHas
public long GetLowestBlock() => _oldestBlock;

public void NewOldestBlock(long oldestBlock) => _oldestBlock = oldestBlock;

}
}
9 changes: 9 additions & 0 deletions src/Nethermind/Nethermind.Evm/CacheCodeInfoRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ public void SetDelegation(Address codeSource, Address authority, IReleaseSpec sp

internal static void Clear() => _codeCache.Clear();

/// <summary>
/// Lightweight service for DI auto-discovery of code cache clearing.
/// Nested to access the private static cache without changing visibility.
/// </summary>
public sealed class CacheClearService : IClearableCache
{
public void ClearCache() => _codeCache.Clear();
}

private sealed class CodeLruCache
{
private const int CacheCount = 16;
Expand Down
24 changes: 24 additions & 0 deletions src/Nethermind/Nethermind.Evm/GasPolicy/AccountAccessKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

namespace Nethermind.Evm.GasPolicy;

/// <summary>
/// Discriminator for account access gas charging.
/// Allows <see cref="IGasPolicy{TSelf}.ConsumeAccountAccessGas"/> to vary
/// its resource-kind split based on the calling opcode's semantics.
/// </summary>
public enum AccountAccessKind : byte
{
/// <summary>
/// Regular account access (BALANCE, EXTCODESIZE, CALL, etc.).
/// </summary>
Default = 0,

/// <summary>
/// SELFDESTRUCT beneficiary access.
/// Cold access charges full cost to StorageAccess (no Computation split);
/// warm access charges nothing.
/// </summary>
SelfDestructBeneficiary = 1
}
39 changes: 15 additions & 24 deletions src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
Expand Down Expand Up @@ -51,45 +52,35 @@ public static bool ConsumeAccountAccessGasWithDelegation(ref EthereumGasPolicy g
ref readonly StackAccessTracker accessTracker,
bool isTracingAccess,
Address address,
Address? delegated,
bool chargeForWarm = true)
Address? delegated)
{
if (!spec.UseHotAndColdStorage)
return true;

bool notOutOfGas = ConsumeAccountAccessGas(ref gas, spec, in accessTracker, isTracingAccess, address, chargeForWarm);
return notOutOfGas && (delegated is null || ConsumeAccountAccessGas(ref gas, spec, in accessTracker, isTracingAccess, delegated, chargeForWarm));
bool notOutOfGas = ConsumeAccountAccessGas(ref gas, spec, in accessTracker, isTracingAccess, address);
return notOutOfGas && (delegated is null || ConsumeAccountAccessGas(ref gas, spec, in accessTracker, isTracingAccess, delegated));
}

public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas,
IReleaseSpec spec,
ref readonly StackAccessTracker accessTracker,
bool isTracingAccess,
Address address,
bool chargeForWarm = true)
AccountAccessKind kind = AccountAccessKind.Default)
{
bool result = true;
if (spec.UseHotAndColdStorage)
if (!spec.UseHotAndColdStorage) return true;
if (isTracingAccess)
{
if (isTracingAccess)
{
// Ensure that tracing simulates access-list behavior.
accessTracker.WarmUp(address);
}

// If the account is cold (and not a precompile), charge the cold access cost.
if (!spec.IsPrecompile(address) && accessTracker.WarmUp(address))
{
result = UpdateGas(ref gas, GasCostOf.ColdAccountAccess);
}
else if (chargeForWarm)
{
// Otherwise, if warm access should be charged, apply the warm read cost.
result = UpdateGas(ref gas, GasCostOf.WarmStateRead);
}
// Ensure that tracing simulates access-list behavior.
accessTracker.WarmUp(address);
}

return result;
return (!spec.IsPrecompile(address) && accessTracker.WarmUp(address)) switch
{
true => UpdateGas(ref gas, GasCostOf.ColdAccountAccess),
false when kind == AccountAccessKind.SelfDestructBeneficiary => true,
false => UpdateGas(ref gas, GasCostOf.WarmStateRead)
};
}

public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas,
Expand Down
10 changes: 5 additions & 5 deletions src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,33 +81,33 @@ public interface IGasPolicy<TSelf> where TSelf : struct, IGasPolicy<TSelf>
/// <param name="isTracingAccess">Whether access tracing is enabled.</param>
/// <param name="address">The target account address.</param>
/// <param name="delegated">The delegated account address, if any.</param>
/// <param name="chargeForWarm">If true, charge even if the account is already warm.</param>
/// <returns>True if gas was successfully charged; otherwise false.</returns>
static abstract bool ConsumeAccountAccessGasWithDelegation(ref TSelf gas,
IReleaseSpec spec,
ref readonly StackAccessTracker accessTracker,
bool isTracingAccess,
Address address,
Address? delegated,
bool chargeForWarm = true);
Address? delegated);

/// <summary>
/// Charges gas for accessing an account based on its storage state (cold vs. warm).
/// Precompiles are treated as exceptions to the cold/warm gas charge.
/// The <paramref name="kind"/> discriminator controls the multi-gas resource split
/// (e.g. SELFDESTRUCT beneficiary charges full cold cost to StorageAccess with no warm charge).
/// </summary>
/// <param name="gas">The gas state to update.</param>
/// <param name="spec">The release specification governing gas costs.</param>
/// <param name="accessTracker">The access tracker for cold/warm state.</param>
/// <param name="isTracingAccess">Whether access tracing is enabled.</param>
/// <param name="address">The target account address.</param>
/// <param name="chargeForWarm">If true, applies the warm read gas cost even if the account is warm.</param>
/// <param name="kind">Discriminator that varies resource-kind accounting per calling opcode.</param>
/// <returns>True if the gas charge was successful; otherwise false.</returns>
static abstract bool ConsumeAccountAccessGas(ref TSelf gas,
IReleaseSpec spec,
ref readonly StackAccessTracker accessTracker,
bool isTracingAccess,
Address address,
bool chargeForWarm = true);
AccountAccessKind kind = AccountAccessKind.Default);

/// <summary>
/// Charges the appropriate gas cost for accessing a storage cell, taking into account whether the access is cold or warm.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ private static EvmExceptionType InstructionSelfDestruct<TGasPolicy>(VirtualMachi
if (inheritor is null)
goto StackUnderflow;

// Charge gas for account access; if insufficient, signal out-of-gas.
if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vmState.AccessTracker, vm.TxTracer.IsTracingAccess, inheritor, false))
// Charge gas for SELFDESTRUCT beneficiary access; if insufficient, signal out-of-gas.
if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vmState.AccessTracker, vm.TxTracer.IsTracingAccess, inheritor, AccountAccessKind.SelfDestructBeneficiary))
goto OutOfGas;

Address executingAccount = vmState.Env.ExecutingAccount;
Expand All @@ -256,17 +256,19 @@ private static EvmExceptionType InstructionSelfDestruct<TGasPolicy>(VirtualMachi
vm.TxTracer.ReportSelfDestruct(executingAccount, result, inheritor);

// For certain specs, charge gas if transferring to a dead account.
// Use ConsumeNewAccountCreation to track StorageGrowth for multi-gas accounting
if (clearEmpty && !result.IsZero && state.IsDeadAccount(inheritor))
{
if (!TGasPolicy.UpdateGas(ref gas, GasCostOf.NewAccount))
if (!TGasPolicy.ConsumeNewAccountCreation(ref gas))
goto OutOfGas;
}

// If account creation rules apply, ensure gas is charged for new accounts.
// Use ConsumeNewAccountCreation to track StorageGrowth for multi-gas accounting
bool inheritorAccountExists = state.AccountExists(inheritor);
if (!clearEmpty && !inheritorAccountExists && spec.UseShanghaiDDosProtection)
{
if (!TGasPolicy.UpdateGas(ref gas, GasCostOf.NewAccount))
if (!TGasPolicy.ConsumeNewAccountCreation(ref gas))
goto OutOfGas;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Nethermind/Nethermind.Trie/Pruning/CommitSetQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,9 @@ public void Remove(BlockCommitSet blockCommitSet)
{
lock (_queue) _queue.Remove(blockCommitSet);
}

public void Clear()
{
lock (_queue) _queue.Clear();
}
}
52 changes: 51 additions & 1 deletion src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Threading.Tasks;
using Nethermind.Core;
using Nethermind.Core.Collections;
using Nethermind.Core.Caching;
using Nethermind.Core.Cpu;
using Nethermind.Core.Crypto;
using Nethermind.Core.Threading;
Expand All @@ -25,7 +26,7 @@ namespace Nethermind.Trie.Pruning;
/// Trie store helps to manage trie commits block by block.
/// If persistence and pruning are needed they have a chance to execute their behavior on commits.
/// </summary>
public sealed class TrieStore : ITrieStore, IPruningTrieStore
public sealed class TrieStore : ITrieStore, IPruningTrieStore, IClearableCache
{
private const double PruningEfficiencyWarningThreshold = 0.9;
private readonly int _shardedDirtyNodeCount = 256;
Expand Down Expand Up @@ -1029,6 +1030,55 @@ public void WaitForPruning()
_pruningTask.Wait();
}

/// <summary>
/// Resets all internal state for test isolation.
/// Used by comparison testing to allow worker reuse without restarting the process.
/// </summary>
void IClearableCache.ClearCache()
{
// Wait for any in-progress pruning to finish
_pruningTask.Wait();

using var scopeL = _scopeLock.EnterScope();
using var pruneL = _pruningLock.EnterScope();

// Clear dirty nodes cache (all shards)
for (int i = 0; i < _dirtyNodes.Length; i++)
_dirtyNodes[i].Clear();

// Clear persisted hash tracking (all shards)
for (int i = 0; i < _persistedHashes.Length; i++)
_persistedHashes[i].Clear();

// Clear commit queue and tracking
_commitSetQueue.Clear();
_lastCommitSet = null;
_commitBuffer = null;
_commitBufferUnused = null;
_currentBlockCommitter = null;

// Reset counters
MemoryUsedByDirtyCache = 0;
DirtyMemoryUsedByDirtyCache = 0;
Interlocked.Exchange(ref _totalCachedNodesCount, 0);
Interlocked.Exchange(ref _dirtyNodesCount, 0);
_committedNodesCount = 0;
_persistedNodesCount = 0;
_latestPersistedBlockNumber = 0;
LatestCommittedBlockNumber = 0;
_lastPersistedReachedReorgBoundary = false;
_toBePersistedBlockNumber = -1;
_isFirst = 0;
_lastPrunedShardIdx = 0;

// Reset metrics
Metrics.CachedNodesCount = 0;
Metrics.DirtyNodesCount = 0;
Metrics.LastPersistedBlockNumber = 0;

if (_logger.IsInfo) _logger.Info("TrieStore cleared for testing");
}

private readonly INodeStorage _nodeStorage;

private readonly TrieKeyValueStore _publicStore;
Expand Down
Loading