Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b35dc3c
add fork override feature
DarkLord017 May 5, 2026
dcb2e2f
add fork override tests
DarkLord017 May 5, 2026
e3abf6c
more detailed rpc test
DarkLord017 May 5, 2026
47db1e0
fix build
DarkLord017 May 5, 2026
43f5aac
correct design and code changes
DarkLord017 May 5, 2026
22d2d6a
correct design and code changes
DarkLord017 May 5, 2026
f15f0c6
Merge branch 'master' into fork-override-trace-block
DarkLord017 May 5, 2026
f142af8
fix tests
DarkLord017 May 5, 2026
5205c18
Merge branch 'fork-override-trace-block' of https://github.com/Nether…
DarkLord017 May 5, 2026
e5623f9
add header override as per fork
DarkLord017 May 5, 2026
363bdf2
add tests for header overrides
DarkLord017 May 5, 2026
8bf86cb
remove blob unnnecesary var
DarkLord017 May 5, 2026
f751a8b
Merge branch 'master' into fork-override-trace-block
DarkLord017 May 5, 2026
c34a26a
Merge branch 'master' into fork-override-trace-block
DarkLord017 May 7, 2026
ff0fd90
Update src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs
DarkLord017 May 11, 2026
8f6c9db
Update src/Nethermind/Nethermind.JsonRpc/Modules/Trace/TraceRpcModule.cs
DarkLord017 May 11, 2026
6017284
use fork name for tests , merge tests
DarkLord017 May 11, 2026
d174da1
Merge branch 'fork-override-trace-block' of https://github.com/Nether…
DarkLord017 May 11, 2026
23c4508
fix linting
DarkLord017 May 11, 2026
8855c9b
fix header function sig
DarkLord017 May 11, 2026
582fb99
remove comments use nameOf
DarkLord017 May 11, 2026
1295241
separate chiado refine tests
DarkLord017 May 11, 2026
1b6e602
fix
DarkLord017 May 11, 2026
e75dfba
refactoring
DarkLord017 May 12, 2026
528ea21
refactoring
DarkLord017 May 12, 2026
7c275b3
make base class to inherit spec
DarkLord017 May 12, 2026
a3f9e37
Merge branch 'master' into fork-override-trace-block
DarkLord017 May 12, 2026
cfa9328
make mainnet public
DarkLord017 May 12, 2026
178e3c7
Merge branch 'master' into fork-override-trace-block
DarkLord017 May 12, 2026
d397e47
Merge branch 'master' into fork-override-trace-block
DarkLord017 May 12, 2026
452c9f9
blocks first time later
DarkLord017 May 12, 2026
4266069
Merge branch 'fork-override-trace-block' of https://github.com/Nether…
DarkLord017 May 12, 2026
7480b17
blocks first time later
DarkLord017 May 12, 2026
126a0ca
refactor(specs): unify TransitionActivations derivation in ForkSchedule
LukaszRozmej May 13, 2026
01367d1
style: fix whitespace and drop unused System.Linq import
LukaszRozmej May 13, 2026
d2f12d7
refactor(specs): add explicit ForkActivationKind to ForkSchedule indexer
LukaszRozmej May 13, 2026
ed54f92
refactor(specs): rename GenesisBlock/DaoBlockNumberConst to satisfy i…
LukaszRozmej May 13, 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
16 changes: 16 additions & 0 deletions src/Nethermind/Nethermind.Core/Specs/IForkAwareSpecProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Collections.Generic;

namespace Nethermind.Core.Specs;

/// <summary>
/// Extends <see cref="ISpecProvider"/> with the ability to look up a named fork's release spec.
/// Implementations may vary per network (Mainnet, AuRa, Gnosis, etc.).
/// </summary>
public interface IForkAwareSpecProvider : ISpecProvider
{
Comment thread
DarkLord017 marked this conversation as resolved.
IEnumerable<string> AvailableForks { get; }
bool TryGetForkSpec(string forkName, out IReleaseSpec? spec);
}
166 changes: 166 additions & 0 deletions src/Nethermind/Nethermind.JsonRpc.Test/Modules/TraceRpcModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,4 +1182,170 @@ private static TraceRpcModule BuildModuleWithNonCanonicalReceipt(Hash256 txHash,
Substitute.For<ISpecProvider>(),
Substitute.For<IBlocksConfig>());
}

/// <summary>
/// A <see cref="TestSpecProvider"/> subclass that also implements <see cref="IForkAwareSpecProvider"/>,
/// delegating fork name lookups to an inner <see cref="IForkAwareSpecProvider"/>.
/// Setting <see cref="TestSpecProvider.AllowTestChainOverride"/> to false prevents
/// <see cref="Nethermind.Core.Test.Blockchain.TestBlockchain"/> from wrapping this instance,
/// which ensures the <see cref="IForkAwareSpecProvider"/> interface is preserved for tracing tests.
/// </summary>
Comment thread
DarkLord017 marked this conversation as resolved.
Outdated
private sealed class ForkAwareTestSpecProvider : TestSpecProvider, IForkAwareSpecProvider
{
private readonly IForkAwareSpecProvider _forkAware;

public ForkAwareTestSpecProvider(IReleaseSpec blockchainSpec, IForkAwareSpecProvider forkAware) : base(blockchainSpec)
{
_forkAware = forkAware;
AllowTestChainOverride = false;
}

public IEnumerable<string> AvailableForks => _forkAware.AvailableForks;
public bool TryGetForkSpec(string forkName, out IReleaseSpec? spec) => _forkAware.TryGetForkSpec(forkName, out spec);
}

[Test]
public async Task trace_block_with_valid_fork_name_returns_traces()
Comment thread
DarkLord017 marked this conversation as resolved.
Outdated
{
Context context = new();
await context.Build(new ForkAwareTestSpecProvider(Berlin.Instance, MainnetSpecProvider.Instance));

ResultWrapper<IEnumerable<ParityTxTraceFromStore>> result =
context.TraceRpcModule.trace_block(BlockParameter.Latest, new ForkActivationParameter { ForkName = "Berlin" });

result.Result.ResultType.Should().Be(ResultType.Success);
}

[Test]
public async Task trace_block_fork_parameter_json_applies_berlin_gas_rules()
{
// Sends {"forkName":"berlin"} (lowercase) over the full JSON-RPC wire path and checks
// that the ModExp gasUsed in the response equals 0x550 (= 1360), the EIP-2565 Berlin value.
// This verifies both JSON deserialization (forkName camelCase) and case-insensitive lookup.
(Context context, BlockParameter block) = await BuildModExpBlockAsync();

string blockHex = "0x" + context.Blockchain.BlockTree.Head!.Number.ToString("x");
string serialized = await RpcTest.TestSerializedRequest(
context.TraceRpcModule, "trace_block", blockHex, "{\"forkName\":\"berlin\"}");

// Berlin EIP-2565: ceil(32/8)^2 * 255 / 3 = 1360 = 0x550
serialized.Should().Contain("\"gasUsed\":\"0x550\"",
"Berlin fork (EIP-2565) ModExp gas for 32-byte inputs must be 1360 (0x550)");
}

[Test]
public async Task trace_block_unknown_fork_returns_invalid_params_failure_listing_known_forks()
{
Context context = new();
await context.Build(new ForkAwareTestSpecProvider(Berlin.Instance, MainnetSpecProvider.Instance));

ResultWrapper<IEnumerable<ParityTxTraceFromStore>> result =
context.TraceRpcModule.trace_block(BlockParameter.Latest, new ForkActivationParameter { ForkName = "NonExistentFork" });

result.Result.ResultType.Should().Be(ResultType.Failure);
result.ErrorCode.Should().Be(ErrorCodes.InvalidParams);
result.Result.Error.Should().Contain("Berlin",
"the error message must list known fork names so callers can correct the request");
}

[Test]
public async Task trace_block_non_fork_aware_provider_returns_invalid_params_failure_mentioning_not_supported()
{
Context context = new();
await context.Build(new TestSpecProvider(Berlin.Instance));

ResultWrapper<IEnumerable<ParityTxTraceFromStore>> result =
context.TraceRpcModule.trace_block(BlockParameter.Latest, new ForkActivationParameter { ForkName = "Berlin" });

result.Result.ResultType.Should().Be(ResultType.Failure);
result.ErrorCode.Should().Be(ErrorCodes.InvalidParams);
result.Result.Error.Should().Contain("does not support fork overrides");
}

// ── ModExp gas under fork override ────────────────────────────────────────
//
// Input: base=2, exp=2^255, mod=3, all fields 32 bytes.
//
// Istanbul (pre-EIP-2565, EIP-198 formula):
// complexity = max(base_len, mod_len)^2 = 32^2 = 1024
// adjusted = index-of-highest-bit(exp) = 255
// gas = complexity * adjusted / 20 = 13056
//
// Berlin (EIP-2565):
// complexity = ceil(max(base_len, mod_len) / 8)^2 = 4^2 = 16
// iterations = bit_length(exp) - 1 = 255
// gas = complexity * iterations / 3 = 1360

private static byte[] BuildModExpInput()
{
byte[] input = new byte[96 + 32 + 32 + 32];
input[31] = 32; // base_length = 32
input[63] = 32; // exp_length = 32
input[95] = 32; // mod_length = 32
input[96 + 31] = 2; // base = 2
input[96 + 32] = 0x80; // exp = 2^255 (MSB set)
input[96 + 64 + 31] = 3; // mod = 3
return input;
}

private static async Task<(Context context, BlockParameter block)> BuildModExpBlockAsync()
{
Context context = new();
await context.Build(new ForkAwareTestSpecProvider(Byzantium.Instance, MainnetSpecProvider.Instance));

UInt256 nonce = context.Blockchain.StateReader.GetNonce(
context.Blockchain.BlockTree.Head!.Header, TestItem.AddressB);

Transaction tx = Build.A.Transaction
.WithTo(Address.FromNumber(5))
.WithData(BuildModExpInput())
.WithGasLimit(50_000)
.WithNonce(nonce)
.SignedAndResolved(context.Blockchain.EthereumEcdsa, TestItem.PrivateKeyB)
.TestObject;

await context.Blockchain.AddBlock(tx);
return (context, new BlockParameter(context.Blockchain.BlockTree.Head!.Number));
}

private static long ModExpGasUsed(
Context context, BlockParameter block, string forkName) =>
context.TraceRpcModule
.trace_block(block, new ForkActivationParameter { ForkName = forkName })
.Data.First(t => t.Type == "call").Result!.GasUsed;

[Test]
public async Task trace_block_istanbul_modexp_gas_cost_is_13056()
{
(Context context, BlockParameter block) = await BuildModExpBlockAsync();

long gasUsed = ModExpGasUsed(context, block, "Istanbul");

gasUsed.Should().Be(13056,
"pre-EIP-2565 formula: 32^2 * 255 / 20 = 13056");
}

[Test]
public async Task trace_block_berlin_modexp_gas_cost_is_1360()
{
(Context context, BlockParameter block) = await BuildModExpBlockAsync();

long gasUsed = ModExpGasUsed(context, block, "Berlin");

gasUsed.Should().Be(1360,
"EIP-2565 formula: ceil(32/8)^2 * 255 / 3 = 16 * 255 / 3 = 1360");
}

[Test]
public async Task trace_block_fork_override_does_not_persist_between_calls()
{
(Context context, BlockParameter block) = await BuildModExpBlockAsync();

long firstIstanbulGas = ModExpGasUsed(context, block, "Istanbul");
ModExpGasUsed(context, block, "Berlin");
long secondIstanbulGas = ModExpGasUsed(context, block, "Istanbul");

secondIstanbulGas.Should().Be(firstIstanbulGas,
"the Berlin override from the intervening call must not leak into subsequent calls");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,12 @@ public ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_filter(TraceFilt
}
}

public ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_block(BlockParameter blockParameter)
public ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_block(BlockParameter blockParameter, ForkActivationParameter? fork = null)
{
// Fork overrides require live re-execution; bypass the store and delegate directly.
if (fork is not null)
return _traceModule.trace_block(blockParameter, fork);

SearchResult<BlockHeader> blockSearch = _blockFinder.SearchForHeader(blockParameter);
if (blockSearch.IsError)
{
Expand Down
14 changes: 14 additions & 0 deletions src/Nethermind/Nethermind.JsonRpc/Data/ForkActivationParameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

namespace Nethermind.JsonRpc.Data;

/// <summary>
/// Optional second parameter for <c>trace_block</c> that requests re-execution
/// under a specific hard-fork spec instead of the block's canonical spec.
/// </summary>
/// <param name="ForkName">
/// Case-insensitive name of the target fork (e.g. <c>"Berlin"</c>, <c>"Prague"</c>).
/// Must be a fork recognised by the node's spec provider.
/// </param>
public record ForkActivationParameter(string ForkName = "");
Comment thread
DarkLord017 marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ ResultWrapper<IEnumerable<ParityTxTraceFromReplay>> trace_callMany(
IsImplemented = true,
IsSharable = false,
ExampleResponse = "{\"action\":{\"callType\":\"call\",\"from\":\"0x31b98d14007bdee637298086988a0bbd31184523\",\"gas\":\"0x0\",\"input\":\"0x\",\"to\":\"0x0e8cda5d7ebda67606a9b296a9dd4351bca1d263\",\"value\":\"0x1043561a882930000\"},\"blockHash\":\"0x6537c92f1fae55d9ea9b0fb25744262114b09e50ac320d7d839830f8c4d723a0\",\"blockNumber\":8969312,\"result\":{\"gasUsed\":\"0x0\",\"output\":\"0x\"},\"subtraces\":0,\"traceAddress\":[],\"transactionHash\":\"0xf4860fc1dc22404b85db7d666dfae65dec7cdcb196837a67ffa992d709f78b9e\",\"transactionPosition\":11,\"type\":\"call\"},{\"action\":{\"callType\":\"call\",\"from\":\"0x71c95151c960aa3976b462ff41adb328790f110d\",\"gas\":\"0x7205\",\"input\":\"0x095ea7b3000000000000000000000000c5992c0e0a3267c7f75493d0f717201e26be35f7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\",\"to\":\"0x5592ec0cfb4dbc12d3ab100b257153436a1f0fea\",\"value\":\"0x0\"},\"blockHash\":\"0x6537c92f1fae55d9ea9b0fb25744262114b09e50ac320d7d839830f8c4d723a0\",\"blockNumber\":8969312,\"result\":{\"gasUsed\":\"0x5fdd\",\"output\":\"0x0000000000000000000000000000000000000000000000000000000000000001\"},\"subtraces\":0,\"traceAddress\":[],\"transactionHash\":\"0xec216ca7e754ea289dd59fc7f9f2c9a5b90668afb5a52d49ee15c3c5fd559b3b\",\"transactionPosition\":12,\"type\":\"call\"}")]
ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_block([JsonRpcParameter(ExampleValue = "\"latest\"")] BlockParameter numberOrTag);
ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_block([JsonRpcParameter(ExampleValue = "\"latest\"")] BlockParameter numberOrTag, ForkActivationParameter? fork = null);

[JsonRpcMethod(Description = "Returns trace at given position.",
IsImplemented = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_filter(TraceFilt
return ResultWrapper<IEnumerable<ParityTxTraceFromStore>>.Success(txTracerFilter.FilterTxTraces(txTracesResult));
}

public ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_block(BlockParameter blockParameter)
public ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_block(BlockParameter blockParameter, ForkActivationParameter? fork = null)
{
SearchResult<Block> blockSearch = blockFinder.SearchForBlock(blockParameter);
if (blockSearch.IsError)
Expand All @@ -275,7 +275,20 @@ public ResultWrapper<IEnumerable<ParityTxTraceFromStore>> trace_block(BlockParam
return GetStateFailureResult<IEnumerable<ParityTxTraceFromStore>>(parentSearch.Object);
}

IReadOnlyCollection<ParityLikeTxTrace> txTraces = ExecuteBlock(parentSearch.Object, block, new(ParityTraceTypes.Trace | ParityTraceTypes.Rewards));
IReleaseSpec? forkSpec = null;
if (fork is not null)
{
if (specProvider is not IForkAwareSpecProvider forkAwareProvider)
return ResultWrapper<IEnumerable<ParityTxTraceFromStore>>.Fail("Spec provider does not support fork overrides", ErrorCodes.InvalidParams);

if (string.IsNullOrEmpty(fork.ForkName))
return ResultWrapper<IEnumerable<ParityTxTraceFromStore>>.Fail("Fork name must not be null or empty", ErrorCodes.InvalidParams);

if (!forkAwareProvider.TryGetForkSpec(fork.ForkName, out forkSpec))
Comment thread
DarkLord017 marked this conversation as resolved.
Outdated
return ResultWrapper<IEnumerable<ParityTxTraceFromStore>>.Fail($"Unknown fork: '{fork.ForkName}'. Available: {string.Join(", ", forkAwareProvider.AvailableForks)}", ErrorCodes.InvalidParams);
}
Comment thread
DarkLord017 marked this conversation as resolved.
Outdated

IReadOnlyCollection<ParityLikeTxTrace> txTraces = ExecuteBlock(parentSearch.Object, block, new(ParityTraceTypes.Trace | ParityTraceTypes.Rewards), forkSpec);
return ResultWrapper<IEnumerable<ParityTxTraceFromStore>>.Success(txTraces.SelectMany(ParityTxTraceFromStore.FromTxTrace));
}

Expand Down Expand Up @@ -355,9 +368,9 @@ private IReadOnlyCollection<ParityLikeTxTrace> TraceBlockDirect(ITracer tracer,
return parityTracer.BuildResult();
}

private IReadOnlyCollection<ParityLikeTxTrace> ExecuteBlock(BlockHeader baseBlock, Block block, ParityLikeBlockTracer tracer)
private IReadOnlyCollection<ParityLikeTxTrace> ExecuteBlock(BlockHeader baseBlock, Block block, ParityLikeBlockTracer tracer, IReleaseSpec? specOverride = null)
{
using Scope<ITracer> env = tracerEnv.BuildAndOverride(baseBlock);
using Scope<ITracer> env = tracerEnv.BuildAndOverride(baseBlock, specOverride: specOverride);
ITracer tracer2 = env.Component;

using CancellationTokenSource timeout = BuildTimeoutCancellationTokenSource();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,53 @@ public void Dao_block_number_is_set_correctly()
Assert.That(provider.DaoBlockNumber, Is.EqualTo(23));
}

[Test]
public void Named_fork_specs_are_available_for_chain_spec_based_provider()
Comment thread
DarkLord017 marked this conversation as resolved.
Outdated
{
ChainSpec chainSpec = new()
{
Parameters = new ChainParameters
{
Eip2565Transition = 5
},
EngineChainSpecParametersProvider = TestChainSpecParametersProvider.NethDev,
BerlinBlockNumber = 5
};

ChainSpecBasedSpecProvider provider = new(chainSpec);

using (Assert.EnterMultipleScope())
{
Assert.That(provider, Is.InstanceOf<IForkAwareSpecProvider>());
Assert.That(provider.AvailableForks, Does.Contain("Berlin"));
Assert.That(provider.TryGetForkSpec("berlin", out IReleaseSpec? berlinSpec), Is.True);
Assert.That(berlinSpec!.IsEip2565Enabled, Is.True);
}
}

[Test]
public void Named_timestamp_fork_specs_are_available_for_chain_spec_based_provider()
{
ChainSpec chainSpec = new()
{
Parameters = new ChainParameters
{
Eip4844TransitionTimestamp = 10
},
EngineChainSpecParametersProvider = TestChainSpecParametersProvider.NethDev,
CancunTimestamp = 10
};

ChainSpecBasedSpecProvider provider = new(chainSpec);

using (Assert.EnterMultipleScope())
{
Assert.That(provider.AvailableForks, Does.Contain("Cancun"));
Assert.That(provider.TryGetForkSpec("cancun", out IReleaseSpec? cancunSpec), Is.True);
Assert.That(cancunSpec!.IsEip4844Enabled, Is.True);
}
}

[Test]
public void Max_code_transition_loaded_correctly()
{
Expand Down
Loading
Loading