Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
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 @@ -271,9 +271,17 @@ protected virtual IEnumerable<IConfig> CreateConfigs() => [new BlocksConfig()
MinGasPrice = 0 // Tx pool test seems to need this.
}];

private static ISpecProvider WrapSpecProvider(ISpecProvider specProvider) => specProvider is TestSpecProvider { AllowTestChainOverride: false }
private static ISpecProvider WrapSpecProvider(ISpecProvider specProvider)
{
ISpecProvider unwrapped =
specProvider is Nethermind.State.OverridableEnv.OverridableSpecProvider envSpecProvider
? envSpecProvider.SpecProvider
: specProvider;

return unwrapped is TestSpecProvider { AllowTestChainOverride: false }
? specProvider
: new OverridableSpecProvider(specProvider, static s => new OverridableReleaseSpec(s) { IsEip3607Enabled = false });
Comment thread
DarkLord017 marked this conversation as resolved.
}

protected virtual IBlockProducer CreateTestBlockProducer()
{
Expand Down
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);
}
156 changes: 156 additions & 0 deletions src/Nethermind/Nethermind.JsonRpc.Test/Modules/TraceRpcModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,4 +1194,160 @@ private static TraceRpcModule BuildModuleWithNonCanonicalReceipt(Hash256 txHash,
Substitute.For<ISpecProvider>(),
Substitute.For<IBlocksConfig>());
}

// AllowTestChainOverride=false prevents TestBlockchain from wrapping this instance and stripping IForkAwareSpecProvider.
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);
}

[TestCase(nameof(Berlin))]
[TestCase(nameof(Istanbul))]
[TestCase(nameof(Cancun))]
public async Task trace_block_with_valid_fork_name_returns_success(string forkName)
{
Context context = new();
await context.Build(new ForkAwareTestSpecProvider(Berlin.Instance, MainnetSpecProvider.Instance));

ResultWrapper<IEnumerable<ParityTxTraceFromStore>> result =
context.TraceRpcModule.trace_block(BlockParameter.Latest, forkName);

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

[Test]
public async Task trace_block_fork_parameter_json_applies_berlin_gas_rules()
{
(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, "berlin");

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, "NonExistentFork");

result.Result.ResultType.Should().Be(ResultType.Failure);
result.ErrorCode.Should().Be(ErrorCodes.InvalidParams);
result.Result.Error.Should().Contain(nameof(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, nameof(Berlin));

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

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, forkName)
.Data.First(t => t.Type == "call").Result!.GasUsed;

[TestCase(nameof(Istanbul), 13056L, "pre-EIP-2565 formula: 32^2 * 255 / 20 = 13056")]
[TestCase(nameof(Berlin), 1360L, "EIP-2565 formula: ceil(32/8)^2 * 255 / 3 = 16 * 255 / 3 = 1360")]
public async Task trace_block_modexp_gas_cost_respects_fork_override(string forkName, long expectedGas, string reason)
{
(Context context, BlockParameter block) = await BuildModExpBlockAsync();

long gasUsed = ModExpGasUsed(context, block, forkName);

gasUsed.Should().Be(expectedGas, reason);
}

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

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

secondIstanbulGas.Should().Be(firstIstanbulGas,
"the Berlin override from the intervening call must not leak into subsequent calls");
}

[Test]
public async Task trace_block_pre_1559_fork_override_on_london_block_zeroes_base_fee()
{
OverridableReleaseSpec londonSpec = new(London.Instance) { Eip1559TransitionBlock = 1 };
ForkAwareTestSpecProvider specProvider = new(londonSpec, MainnetSpecProvider.Instance);

Context context = new();
await context.Build(specProvider);

BlockHeader? block1 = context.Blockchain.BlockTree.FindHeader(1, BlockTreeLookupOptions.None);
block1.Should().NotBeNull();
block1!.BaseFeePerGas.Should().BeGreaterThan(UInt256.Zero,
"the London fork-activation block must carry a non-zero base fee so the test is meaningful");

// Re-executing this London block with a Berlin (pre-EIP-1559) fork override must succeed:
// AdjustHeaderForSpec must zero BaseFeePerGas for pre-1559 specs.
ResultWrapper<IEnumerable<ParityTxTraceFromStore>> result =
context.TraceRpcModule.trace_block(new BlockParameter(1L), nameof(Berlin));

result.Result.ResultType.Should().Be(ResultType.Success,
"tracing a London block with a pre-EIP-1559 fork override must succeed (AdjustHeaderForSpec zeroes BaseFeePerGas)");
Comment thread
DarkLord017 marked this conversation as resolved.
Comment thread
DarkLord017 marked this conversation as resolved.
Comment thread
DarkLord017 marked this conversation as resolved.
Comment thread
DarkLord017 marked this conversation as resolved.
Comment thread
DarkLord017 marked this conversation as resolved.
Comment thread
DarkLord017 marked this conversation as resolved.
}
}
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, string? 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
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, string? 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 @@ -257,7 +257,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, string? fork = null)
{
SearchResult<Block> blockSearch = blockFinder.SearchForBlock(blockParameter);
if (blockSearch.IsError)
Expand All @@ -282,7 +282,10 @@ 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));
if (!TryResolveForkSpec(fork, out IReleaseSpec? forkSpec, out ResultWrapper<IEnumerable<ParityTxTraceFromStore>>? forkError))
return forkError!;

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 @@ -362,17 +365,70 @@ 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);
Block blockToExecute = block;
if (specOverride is not null)
{
BlockHeader adjustedHeader = AdjustHeaderForSpec(block.Header, baseBlock, specOverride);
blockToExecute = block.WithReplacedHeader(adjustedHeader);
}

using Scope<ITracer> env = tracerEnv.BuildAndOverride(baseBlock, specOverride: specOverride);
ITracer tracer2 = env.Component;

using CancellationTokenSource timeout = BuildTimeoutCancellationTokenSource();
CancellationToken cancellationToken = timeout.Token;
tracer2.Execute(block, tracer.WithCancellation(cancellationToken));
tracer2.Execute(blockToExecute, tracer.WithCancellation(cancellationToken));
return tracer.BuildResult();
}

private static BlockHeader AdjustHeaderForSpec(BlockHeader header, BlockHeader parentHeader, IReleaseSpec spec)
{
BlockHeader adjusted = header.Clone();

adjusted.BaseFeePerGas = spec.IsEip1559Enabled
? adjusted.BaseFeePerGas.IsZero
? BaseFeeCalculator.Calculate(parentHeader, spec)
: adjusted.BaseFeePerGas
: UInt256.Zero;

adjusted.ExcessBlobGas = spec.IsEip4844Enabled
? BlobGasCalculator.CalculateExcessBlobGas(parentHeader, spec)
: null;

return adjusted;
}

private bool TryResolveForkSpec<TResult>(string? fork, out IReleaseSpec? forkSpec, out ResultWrapper<TResult>? error)
{
forkSpec = null;
error = null;

if (fork is null)
return true;

if (specProvider is not IForkAwareSpecProvider forkAwareProvider)
{
error = ResultWrapper<TResult>.Fail("Spec provider does not support fork overrides", ErrorCodes.InvalidParams);
return false;
}

if (string.IsNullOrEmpty(fork))
{
error = ResultWrapper<TResult>.Fail("Fork name must not be null or empty", ErrorCodes.InvalidParams);
return false;
}

if (!forkAwareProvider.TryGetForkSpec(fork, out forkSpec))
{
error = ResultWrapper<TResult>.Fail($"Unknown fork: '{fork}'. Available: {string.Join(", ", forkAwareProvider.AvailableForks)}", ErrorCodes.InvalidParams);
return false;
}

return true;
}

private static ResultWrapper<TResult> GetStateFailureResult<TResult>(BlockHeader header) =>
ResultWrapper<TResult>.Fail($"No state available for block {header.ToString(BlockHeader.Format.FullHashAndNumber)}", ErrorCodes.ResourceUnavailable);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Nethermind.Serialization.Json;
using Nethermind.Specs.ChainSpecStyle;
using Nethermind.Specs.ChainSpecStyle.Json;
using Nethermind.Specs.Forks;
using Nethermind.Specs.GnosisForks;
using NSubstitute;
using NUnit.Framework;
using System;
Expand Down Expand Up @@ -710,6 +712,33 @@ public void Dao_block_number_is_set_correctly()
Assert.That(provider.DaoBlockNumber, Is.EqualTo(23));
}

[TestCase(nameof(Berlin), BlockchainIds.Mainnet, typeof(Berlin))]
[TestCase(nameof(Prague), BlockchainIds.Mainnet, typeof(Prague))]
[TestCase(nameof(Osaka), BlockchainIds.Gnosis, typeof(OsakaGnosis))]
[TestCase(nameof(Osaka), BlockchainIds.Chiado, typeof(OsakaGnosis))]
[TestCase(nameof(Cancun), BlockchainIds.Sepolia, typeof(Cancun))]
[TestCase(nameof(Prague), BlockchainIds.Sepolia, typeof(Prague))]
[TestCase(nameof(Osaka), BlockchainIds.Hoodi, typeof(Osaka))]
public void Named_forks_are_available_for_chain_spec_based_provider(string forkName, ulong chainId, Type expectedSpecType)
{
ChainSpec chainSpec = new()
{
ChainId = chainId,
Parameters = new ChainParameters(),
EngineChainSpecParametersProvider = TestChainSpecParametersProvider.NethDev
};

ChainSpecBasedSpecProvider provider = new(chainSpec);

using (Assert.EnterMultipleScope())
{
Assert.That(provider, Is.InstanceOf<IForkAwareSpecProvider>());
Assert.That(provider.AvailableForks, Does.Contain(forkName));
Assert.That(provider.TryGetForkSpec(forkName.ToLowerInvariant(), out IReleaseSpec? spec), Is.True);
Assert.That(spec, Is.InstanceOf(expectedSpecType));
}
}

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