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
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Specs;
using Nethermind.Evm.State;
using Nethermind.Evm.Tracing;
using Nethermind.Evm.TransactionProcessing;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — Unused using

Nethermind.Evm.TransactionProcessing is not referenced anywhere in this file. Remove it.

Suggested change
using Nethermind.Evm.TransactionProcessing;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left this one in — GasConsumed lives in Nethermind.Evm.TransactionProcessing and it's the type in the MarkAsSuccess/MarkAsFailed signatures. Tried removing it and the build went red.


namespace Nethermind.Blockchain.Tracing;

/// <summary>
/// Captures the world state root after each transaction in a block.
/// Mirrors geth's <c>debug_intermediateRoots</c> behaviour.
/// </summary>
/// <remarks>
/// Roots are produced per transaction in execution order; a failed transaction still
/// produces its post-execution root (matching geth's partial-result semantics).
/// System-level calls (EIP-4788, EIP-2935) and withdrawals do not produce entries
/// because <see cref="BlockTracerBase{TTrace,TTracer}"/> only dispatches on user transactions.
/// </remarks>
public class IntermediateRootsBlockTracer(IWorldState worldState, IReleaseSpec spec)
: BlockTracerBase<Hash256, IntermediateRootsBlockTracer.IntermediateRootsTxTracer>
{
protected override IntermediateRootsTxTracer OnStart(Transaction? tx) => new(worldState, spec);

protected override Hash256 OnEnd(IntermediateRootsTxTracer txTracer) => txTracer.StateRoot;

public class IntermediateRootsTxTracer(IWorldState worldState, IReleaseSpec spec) : TxTracer
{
public override bool IsTracingReceipt => true;

public Hash256 StateRoot { get; private set; }

public override void MarkAsSuccess(Address recipient, in GasConsumed gasSpent, byte[] output, LogEntry[] logs, Hash256? stateRoot = null) =>
Capture(stateRoot);

public override void MarkAsFailed(Address recipient, in GasConsumed gasSpent, byte[] output, string? error, Hash256? stateRoot = null) =>
Capture(stateRoot);
Comment on lines +36 to +40
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Partial-result semantics unspecified and untested

If a transaction fails mid-block (e.g. an OOG reverted tx), MarkAsFailed is still called and a root is still appended — so the count of returned roots always equals the number of transactions regardless of success/failure. This is the correct and intended semantics, but:

  1. It is not documented: the JsonRpcMethod description and XML docs don't mention whether failed txs contribute a root.
  2. It is not tested: there's no test with a mixed success/failure block that verifies the final count and the state after a failed tx.

Geth returns roots[0..i] with nil error when a tx fails mid-block (the exact diagnostic use case). Add a test asserting that a block with one successful tx and one OOG tx returns exactly two roots, and the second root equals the state root after the failed tx's gas refund.

Fix this →

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented the behaviour in the tracer's XML <remarks> (5d39af79) and the JsonRpcMethod description (598cf9b1). Didn't add a dedicated failed-tx test in this round — wants an OOG fixture wired up. Can layer it in here if you'd rather not defer, otherwise will track separately.


private void Capture(Hash256? reportedStateRoot)
{
if (reportedStateRoot is not null)
{
StateRoot = reportedStateRoot;
return;
}

// Post-Byzantium (EIP-658): TransactionProcessor commits with commitRoots=false,
// which buffers changes into _blockChanges without flushing them to the trie.
// Force a flush so the recomputed root reflects this transaction's effect — the
// BlockProcessor's later end-of-block commit is idempotent for already-flushed entries.
worldState.Commit(spec, commitRoots: true);
worldState.RecalculateStateRoot();
StateRoot = worldState.StateRoot;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;

namespace Nethermind.Consensus.Tracing;

/// <summary>Thrown when a tracer is asked to replay the genesis block, which has no parent state and no transactions.</summary>
/// <remarks>Callers at the RPC boundary should map this to a clean client-facing error (e.g. invalid input) rather than letting it surface as a generic internal error.</remarks>
public sealed class GenesisNotTraceableException : InvalidOperationException
{
public GenesisNotTraceableException() : base("genesis is not traceable") { }
}
29 changes: 29 additions & 0 deletions src/Nethermind/Nethermind.Consensus/Tracing/GethStyleTracer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Nethermind.Evm.State;
using Nethermind.State.OverridableEnv;
using Nethermind.Evm.Tracing;
using Nethermind.Blockchain.Tracing;
using Nethermind.Blockchain.Tracing.GethStyle;
using Nethermind.Blockchain.Tracing.GethStyle.Custom.JavaScript;
using Nethermind.Blockchain.Tracing.GethStyle.Custom.Native;
Expand Down Expand Up @@ -123,6 +124,34 @@ public IReadOnlyCollection<GethLikeTxTrace> TraceBlock(BlockParameter blockParam

public IReadOnlyCollection<GethLikeTxTrace> TraceBlock(Block block, GethTraceOptions options, CancellationToken cancellationToken) => TraceBlockImpl(block, options, cancellationToken);

public IReadOnlyCollection<Hash256> TraceBlockIntermediateRoots(Hash256 blockHash, GethTraceOptions options, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(blockHash);
ArgumentNullException.ThrowIfNull(options);

// Mirror geth: canonical blocks first, fall back to the bad-block store so the diagnostic
// use case (replaying a rejected block to find divergence) works.
Block block = blockTree.FindBlock(blockHash)
?? badBlockStore.GetAll().FirstOrDefault(b => b.Hash == blockHash)
?? throw new InvalidOperationException($"Cannot find block {blockHash}");
if (block.IsGenesis) throw new GenesisNotTraceableException();

BlockHeader? parent = FindParent(block);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High — Genesis NRE / wrong behaviour

FindParent returns null when block.IsGenesis (see line 240: the body is skipped and null is returned). That null is then passed to BuildAndOverride on line 135. Whether this produces an NRE or silent wrong output depends on BuildAndOverride's implementation, but either way the behaviour is incorrect — geth explicitly rejects genesis with "genesis is not traceable".

Add a guard before the FindParent call:

Suggested change
BlockHeader? parent = FindParent(block);
Block block = blockTree.FindBlock(blockHash) ?? throw new InvalidOperationException($"Cannot find block {blockHash}");
if (block.IsGenesis) throw new InvalidOperationException("genesis is not traceable");
BlockHeader? parent = FindParent(block);

Fix this →

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. Added an explicit block.IsGenesis guard with the same wording geth uses, before FindParent runs. a160e9f1.


using Scope<BlockProcessingComponents> scope = blockProcessingEnv.BuildAndOverride(parent, options.StateOverrides);
IntermediateRootsBlockTracer tracer = new(scope.Component.WorldState, specProvider.GetSpec(block.Header));
try
{
scope.Component.BlockchainProcessor.Process(block, ProcessingOptions.Trace, tracer.WithCancellation(cancellationToken), cancellationToken);
return tracer.BuildResult();
}
catch
{
tracer.TryDispose();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just using on the tracer?

throw;
}
Comment on lines +142 to +152
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IntermediateRootsBlockTracer doesn't implement IDisposable, so the TryDispose() in the catch resolves to the object-overload of DisposableExtensions.TryDispose(this object?), which type-checks, finds no IDisposable, and returns immediately — it's a no-op. The whole try/catch adds noise without benefit.

Since the tracer has no resources to release, just remove the guard:

Suggested change
IntermediateRootsBlockTracer tracer = new(scope.Component.WorldState, specProvider.GetSpec(block.Header));
try
{
scope.Component.BlockchainProcessor.Process(block, ProcessingOptions.Trace, tracer.WithCancellation(cancellationToken), cancellationToken);
return tracer.BuildResult();
}
catch
{
tracer.TryDispose();
throw;
}
IntermediateRootsBlockTracer tracer = new(scope.Component.WorldState, specProvider.GetSpec(block.Header));
scope.Component.BlockchainProcessor.Process(block, ProcessingOptions.Trace, tracer.WithCancellation(cancellationToken), cancellationToken);
return tracer.BuildResult();

(If the tracer ever becomes IDisposable, a using declaration would replace this naturally at that point.)

}

public IEnumerable<string> TraceBlockToFile(Hash256 blockHash, GethTraceOptions options, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(blockHash);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ public interface IGethStyleTracer
IReadOnlyCollection<GethLikeTxTrace> TraceBlock(BlockParameter blockParameter, GethTraceOptions options, CancellationToken cancellationToken);
IReadOnlyCollection<GethLikeTxTrace> TraceBlock(Rlp blockRlp, GethTraceOptions options, CancellationToken cancellationToken);
IReadOnlyCollection<GethLikeTxTrace> TraceBlock(Block block, GethTraceOptions options, CancellationToken cancellationToken);
/// <summary>
/// Replays <paramref name="blockHash"/> and returns the world-state root after each transaction
/// in execution order. EIP-4788/EIP-2935 system calls and withdrawals do not produce an entry.
/// </summary>
/// <param name="blockHash">Hash of a canonical or bad-stored block to replay.</param>
/// <param name="options">Trace options (state overrides are applied to the parent base state).</param>
/// <param name="cancellationToken">Cooperative cancellation.</param>
/// <returns>Post-tx state roots in execution order; failed transactions still produce a root.</returns>
IReadOnlyCollection<Hash256> TraceBlockIntermediateRoots(Hash256 blockHash, GethTraceOptions options, CancellationToken cancellationToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — missing XML doc

Per the project's coding style, all public API members should have <summary> documentation. Every other method in this interface currently lacks docs too, but since this is new surface area it's a good opportunity to set the standard.

/// <summary>Replays <paramref name="blockHash"/> and returns the world-state root after each transaction, in execution order.</summary>
IReadOnlyCollection<Hash256> TraceBlockIntermediateRoots(Hash256 blockHash, GethTraceOptions options, CancellationToken cancellationToken);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added on the interface in 598cf9b1, plus a more descriptive JsonRpcMethod attribute alongside it.

IEnumerable<string> TraceBlockToFile(Hash256 blockHash, GethTraceOptions options, CancellationToken cancellationToken);
IEnumerable<string> TraceBadBlockToFile(Hash256 blockHash, GethTraceOptions options, CancellationToken cancellationToken);
}
49 changes: 49 additions & 0 deletions src/Nethermind/Nethermind.JsonRpc.Test/Modules/DebugModuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,4 +419,53 @@ public void StandardTraceBlockToFile_returns_error_when_state_unavailable(bool i
actual.ErrorCode.Should().Be(ErrorCodes.ResourceUnavailable);
actual.Result.Error.Should().Contain("No state available");
}

[Test]
public void Debug_intermediateRoots_returns_post_tx_roots_from_bridge()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can those be collapsed to testcases?

{
Hash256 blockHash = TestItem.KeccakA;
BlockHeader header = Build.A.BlockHeader.WithNumber(1).TestObject;
_blockFinder.FindHeader(blockHash).Returns(header);
_blockchainBridge.HasStateForBlock(Arg.Is(header)).Returns(true);

Hash256[] expected = [TestItem.KeccakB, TestItem.KeccakC];
_debugBridge
.GetBlockIntermediateRoots(Arg.Is(blockHash), Arg.Any<CancellationToken>(), Arg.Any<GethTraceOptions?>())
.Returns(expected);

DebugRpcModule rpcModule = CreateDebugRpcModule(_debugBridge);
ResultWrapper<IReadOnlyCollection<Hash256>> actual = rpcModule.debug_intermediateRoots(blockHash);

actual.Result.ResultType.Should().Be(ResultType.Success);
actual.Data.Should().BeEquivalentTo(expected);
}

[Test]
public void Debug_intermediateRoots_fails_when_state_unavailable()
{
Hash256 blockHash = TestItem.KeccakA;
BlockHeader header = Build.A.BlockHeader.WithNumber(1).TestObject;
_blockFinder.FindHeader(blockHash).Returns(header);
_blockchainBridge.HasStateForBlock(Arg.Is(header)).Returns(false);

DebugRpcModule rpcModule = CreateDebugRpcModule(_debugBridge);
ResultWrapper<IReadOnlyCollection<Hash256>> actual = rpcModule.debug_intermediateRoots(blockHash);

actual.Result.ResultType.Should().Be(ResultType.Failure);
actual.ErrorCode.Should().Be(ErrorCodes.ResourceUnavailable);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — missing integration test + Low — missing "block not found" RPC test

Two gaps in coverage:

  1. Integration path: Both existing tests mock IDebugBridge entirely. The IntermediateRootsBlockTracer.Capture logic — particularly the worldState.RecalculateStateRoot() fallback for post-Byzantium blocks — is never exercised. Adding a test that drives TraceBlockIntermediateRoots through TestBlockchain (as other tracer tests do) would give confidence that the root accumulation across transactions is correct.

  2. Header-not-found path: StandardTraceBlockToFile_returns_error_when_missing_block exists for debug_standardTraceBlockToFile but the equivalent test is absent for debug_intermediateRoots. Consider adding:

[Test]
public void Debug_intermediateRoots_fails_when_block_not_found()
{
    Hash256 blockHash = TestItem.KeccakA;
    _blockFinder.FindHeader(blockHash).ReturnsNull();

    DebugRpcModule rpcModule = CreateDebugRpcModule(_debugBridge);
    ResultWrapper<IReadOnlyCollection<Hash256>> actual = rpcModule.debug_intermediateRoots(blockHash);

    actual.Result.ResultType.Should().Be(ResultType.Failure);
    actual.ErrorCode.Should().Be(ErrorCodes.ResourceNotFound);
}

Fix this →

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block-not-found unit test landed in 8258b4fa. For the integration path, wired up a TestRpcBlockchain-driven test in 74b08eb2 that replays a real two-tx block and checks the roots are non-zero and distinct — and writing that test actually surfaced a real bug in the post-Byzantium fallback (details in the summary comment, fix in b3290a83). Glad it was flagged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Critical test scenarios missing

Both current tests mock IDebugBridge at the boundary — the "matches geth" claim is unverified. The following scenarios are completely uncovered:

Scenario Why it matters
N txs in → exactly N roots out Basic contract verification
Genesis hash → clean error (not NRE) Once genesis guard is added
Block-not-found → ResourceNotFound Exercises FindHeader null path
Failed tx still produces a root Partial-result semantics
System calls (EIP-4788/2935) don't add entry Spec correctness
Withdrawals don't add entry Spec correctness
Cancellation → correct error Parity with other trace methods
Root equals hand-computed post-tx stateRoot Correctness against known reference

At minimum, add a TestBlockchain-based integration test (similar to those in GethStyleTracerTests) that replays a two-transaction block and asserts the two returned roots are distinct and non-zero. The block-not-found and genesis error tests can stay at the DebugRpcModule unit level.

Fix this →

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integration test in 74b08eb2 covers count, distinctness, non-zero, plus the genesis rejection. Unit-level block-not-found in 8258b4fa. The remaining four (system-call/withdrawal absence, failed-tx still producing a root, hand-computed reference, cancellation) need richer fixtures — would rather not bolt them on hastily. Happy to either pull them into this PR or track in a follow-up; let me know which you prefer.


[Test]
public void Debug_intermediateRoots_fails_when_block_not_found()
{
Hash256 blockHash = TestItem.KeccakA;
_blockFinder.FindHeader(blockHash).ReturnsNull();

DebugRpcModule rpcModule = CreateDebugRpcModule(_debugBridge);
ResultWrapper<IReadOnlyCollection<Hash256>> actual = rpcModule.debug_intermediateRoots(blockHash);

actual.Result.ResultType.Should().Be(ResultType.Failure);
actual.ErrorCode.Should().Be(ErrorCodes.ResourceNotFound);
actual.Result.Error.Should().Contain("Cannot find header");
}
Comment on lines +443 to +470
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two failure tests have different setups (one stubs FindHeader to return a header but state unavailable; the other stubs it to return null), which makes a direct [TestCase(false/true)] awkward — the test body would need a conditional branch for each case.

A [TestCaseSource] with setup lambdas would work (similar to the GetRawBlockAccessListErrorCases pattern that used to live in this file), but the additional indirection tends to hurt readability on tests this short.

Simplest collapse that stays readable — fold into a single parameterised test with two TestCaseData entries carrying the expected error code:

private static IEnumerable<TestCaseData> IntermediateRootsErrorCases()
{
    yield return new TestCaseData(
        (Action<IBlockFinder, IBlockchainBridge>)((finder, _) => finder.FindHeader(Arg.Any<Hash256>()).ReturnsNull()),
        ErrorCodes.ResourceNotFound)
    { TestName = "block_not_found" };

    yield return new TestCaseData(
        (Action<IBlockFinder, IBlockchainBridge>)((finder, chain) =>
        {
            BlockHeader h = Build.A.BlockHeader.WithNumber(1).TestObject;
            finder.FindHeader(Arg.Any<Hash256>()).Returns(h);
            chain.HasStateForBlock(h).Returns(false);
        }),
        ErrorCodes.ResourceUnavailable)
    { TestName = "state_unavailable" };
}

[TestCaseSource(nameof(IntermediateRootsErrorCases))]
public void Debug_intermediateRoots_fails(Action<IBlockFinder, IBlockchainBridge> setup, int expectedCode)
{
    setup(_blockFinder, _blockchainBridge);
    DebugRpcModule rpcModule = CreateDebugRpcModule(_debugBridge);
    ResultWrapper<IReadOnlyCollection<Hash256>> actual = rpcModule.debug_intermediateRoots(TestItem.KeccakA);
    actual.Result.ResultType.Should().Be(ResultType.Failure);
    actual.ErrorCode.Should().Be(expectedCode);
}

That said, the current two named tests are equally clear; this is a style preference rather than a correctness concern.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using NUnit.Framework;

namespace Nethermind.JsonRpc.Test.Modules;

public partial class DebugRpcModuleTests
{
[Test]
public async Task Debug_intermediateRoots_returns_one_root_per_transaction()
{
using Context context = await Context.Create();
await context.Blockchain.AddBlock(CreateTraceBlockTransactions(context.Blockchain));

Hash256 blockHash = context.Blockchain.BlockTree.Head!.Hash!;
ResultWrapper<IReadOnlyCollection<Hash256>> result = context.DebugRpcModule.debug_intermediateRoots(blockHash);

result.Result.ResultType.Should().Be(ResultType.Success);
result.Data.Should().HaveCount(2, "the block has exactly two user transactions");
result.Data.Should().OnlyHaveUniqueItems("each tx mutates state, producing a distinct post-tx root");
result.Data.Should().NotContain(Keccak.Zero);
}

[Test]
public async Task Debug_intermediateRoots_rejects_genesis()
{
using Context context = await Context.Create();
Hash256 genesisHash = context.Blockchain.BlockTree.Genesis!.Hash!;

ResultWrapper<IReadOnlyCollection<Hash256>> result = context.DebugRpcModule.debug_intermediateRoots(genesisHash);

result.Result.ResultType.Should().Be(ResultType.Failure);
result.ErrorCode.Should().Be(ErrorCodes.InvalidInput);
result.Result.Error.Should().Contain("genesis");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ public IReadOnlyCollection<GethLikeTxTrace> GetBlockTrace(Rlp blockRlp, Cancella
public IReadOnlyCollection<GethLikeTxTrace> GetBlockTrace(Block block, CancellationToken cancellationToken, GethTraceOptions? gethTraceOptions = null) =>
_tracer.TraceBlock(block, gethTraceOptions ?? GethTraceOptions.Default, cancellationToken);

public IReadOnlyCollection<Hash256> GetBlockIntermediateRoots(Hash256 blockHash, CancellationToken cancellationToken, GethTraceOptions? gethTraceOptions = null) =>
_tracer.TraceBlockIntermediateRoots(blockHash, gethTraceOptions ?? GethTraceOptions.Default, cancellationToken);

public byte[]? GetBlockRlp(BlockParameter parameter)
{
if (parameter.BlockNumber is long number)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Nethermind.Core.Extensions;
using Nethermind.Evm;
using Nethermind.Blockchain.Tracing.GethStyle;
using Nethermind.Consensus.Tracing;
using Nethermind.JsonRpc.Data;
using Nethermind.Logging;
using Nethermind.Serialization.Rlp;
Expand Down Expand Up @@ -298,6 +299,32 @@ public ResultWrapper<IReadOnlyCollection<GethLikeTxTrace>> debug_traceBlockByHas
}
}

public ResultWrapper<IReadOnlyCollection<Hash256>> debug_intermediateRoots(Hash256 blockHash, GethTraceOptions? options = null)
{
TryGetHeaderAndCheckState<IReadOnlyCollection<Hash256>>(blockHash, out ResultWrapper<IReadOnlyCollection<Hash256>>? headerError);
if (headerError is not null)
{
return headerError;
}

using CancellationTokenSource? timeout = BuildTimeoutCancellationTokenSource();
CancellationToken cancellationToken = timeout.Token;

IReadOnlyCollection<Hash256> roots;
try
{
roots = debugBridge.GetBlockIntermediateRoots(blockHash, cancellationToken, options);
}
catch (GenesisNotTraceableException e)
{
return ResultWrapper<IReadOnlyCollection<Hash256>>.Fail(e.Message, ErrorCodes.InvalidInput);
}

if (_logger.IsTrace) _logger.Trace($"{nameof(debug_intermediateRoots)} request {blockHash}, roots: {roots.Count}");

return ResultWrapper<IReadOnlyCollection<Hash256>>.Success(roots);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Genesis throws unhandled exception

TryGetHeaderAndCheckState passes for genesis (the header exists and state is available), so the InvalidOperationException("genesis is not traceable") thrown by GethStyleTracer propagates unhandled to the RPC framework — clients get a −32603 internal error instead of a meaningful response. All other invalid-input conditions on this method return clean ResourceNotFound / ResourceUnavailable wrappers.

Suggested change
return ResultWrapper<IReadOnlyCollection<Hash256>>.Success(roots);
IReadOnlyCollection<Hash256> roots;
try
{
roots = debugBridge.GetBlockIntermediateRoots(blockHash, cancellationToken, options);
}
catch (InvalidOperationException e)
{
return ResultWrapper<IReadOnlyCollection<Hash256>>.Fail(e.Message, ErrorCodes.InvalidInput);
}

(Or introduce a typed GenesisNotTraceableException and catch that specifically to avoid catching unrelated InvalidOperationExceptions from deeper in the stack.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Went with the typed exception route — added GenesisNotTraceableException in Nethermind.Consensus.Tracing and the RPC now catches it specifically, mapping to ErrorCodes.InvalidInput. Keeps it consistent with the block-not-found / state-unavailable paths and avoids the string-match fragility. Test updated to assert the wrapped failure result.

  • 8dac374f3b — introduce the typed exception
  • 399f3680e1 — catch + map at the RPC layer, update test

}

public ResultWrapper<GethLikeTxTrace[]> debug_traceBlockFromFile(string fileName, GethTraceOptions options = null) => throw new NotImplementedException();

public ResultWrapper<object> debug_dumpBlock(BlockParameter blockParameter) => throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public interface IDebugBridge
IReadOnlyCollection<GethLikeTxTrace> GetBlockTrace(BlockParameter blockParameter, CancellationToken cancellationToken, GethTraceOptions gethTraceOptions = null);
IReadOnlyCollection<GethLikeTxTrace> GetBlockTrace(Rlp blockRlp, CancellationToken cancellationToken, GethTraceOptions? gethTraceOptions = null);
IReadOnlyCollection<GethLikeTxTrace> GetBlockTrace(Block block, CancellationToken cancellationToken, GethTraceOptions? gethTraceOptions = null);
IReadOnlyCollection<Hash256> GetBlockIntermediateRoots(Hash256 blockHash, CancellationToken cancellationToken, GethTraceOptions? gethTraceOptions = null);
Block? GetBlock(BlockParameter param);
byte[] GetBlockRlp(BlockParameter param);
byte[] GetDbValue(string dbName, byte[] key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public interface IDebugRpcModule : IRpcModule
[JsonRpcMethod(Description = "Similar to debug_traceBlock, this method accepts a block hash and replays the block that is already present in the database.", IsImplemented = true, IsSharable = false)]
ResultWrapper<IReadOnlyCollection<GethLikeTxTrace>> debug_traceBlockByHash(Hash256 blockHash, GethTraceOptions options = null);

[JsonRpcMethod(Description = "Replays a block and returns the world-state root after each transaction in execution order. EIP-4788/EIP-2935 system calls and withdrawals do not produce an entry. Failed transactions still produce a root (geth partial-result semantics). The block must be canonical or in the bad-block store.", IsImplemented = true, IsSharable = false)]
ResultWrapper<IReadOnlyCollection<Hash256>> debug_intermediateRoots(Hash256 blockHash, GethTraceOptions? options = null);

[JsonRpcMethod(Description = "", IsImplemented = false, IsSharable = false)]
ResultWrapper<GethLikeTxTrace[]> debug_traceBlockFromFile(string fileName, GethTraceOptions options = null);

Expand Down
Loading