Skip to content

Conversation

@dmitriy-b
Copy link
Contributor

Changes

  • Add OpcodeTracing plugin that counts EVM opcode usage across blocks
  • The plugin supports two tracing modes:
    • Retrospective: reads historical blocks from database and outputs a single summary file
    • RealTime: traces blocks as they're processed, writes per-block files and a cumulative file
  • Plugin gracefully disables itself on invalid config without crashing the client. The plugin is disabled by default.
  • Configurable via CLI flags: --OpcodeTracing.Enabled, --OpcodeTracing.Mode, --OpcodeTracing.Blocks, --OpcodeTracing.StartBlock, --OpcodeTracing.EndBlock, --OpcodeTracing.OutputDirectory, --OpcodeTracing. MaxDegreeOfParallelism (for retrospective mode only).

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

Tested manually via Docker with Hoodi testnet:

  • Plugin disabled by default when not specified
  • Retrospective mode generates correct JSON output
  • RealTime mode generates per-block and cumulative JSON files
  • Invalid config logs error and disables plugin without crashing

See attached logs json files.

opcode-trace-block-1845471.json
opcode-trace-block-1845470.json
opcode-trace-all-20251224143649.json
opcode-trace-1844925-1844934.json
05-validation-error.log
04-realtime-mode.log
03-retrospective-mode.log
02-plugin-not-specified.log
01-plugin-disabled.log

Documentation

Requires documentation update

  • Yes
  • No

Usage examples needed in docs for CLI flags and output format.
I also added detailed documentation how it works at src/Nethermind/Nethermind.OpcodeTracing.Plugin/README.md.

Requires explanation in Release Notes

  • Yes
  • No

We can add something like: New optional plugin for tracing EVM opcode usage. Enable with --OpcodeTracing.Enabled=true. Useful for analyzing opcode frequency across block ranges.

Copy link
Member

@LukaszRozmej LukaszRozmej left a comment

Choose a reason for hiding this comment

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

Didn't check everything, but things that potentially stand out:

  • Not sure if converting to string is done too soon. IMO whole trace could be by domain types (byte, Hash256), and stringify only when outputing JSON - could be faster
  • Paths manipulation - should be standarised with other in node
  • File writing - avoid serializing to string first, directly to file.

Comment on lines 1035 to 1041
"Nethermind.Api": "[1.36.0-unstable, )",
"Nethermind.Blockchain": "[1.36.0-unstable, )",
"Nethermind.Config": "[1.36.0-unstable, )",
"Nethermind.Core": "[1.36.0-unstable, )",
"Nethermind.Evm": "[1.36.0-unstable, )",
"Nethermind.Logging": "[1.36.0-unstable, )",
"Nethermind.Synchronization": "[1.36.0-unstable, )"
Copy link
Member

Choose a reason for hiding this comment

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

Should probably be 1.37.0-unstable

/// <summary>
/// Gets or sets a value indicating whether the OpcodeTracing plugin is enabled.
/// </summary>
[ConfigItem(Description = "Enable the OpcodeTracing plugin.", DefaultValue = "false")]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
[ConfigItem(Description = "Enable the OpcodeTracing plugin.", DefaultValue = "false")]
[ConfigItem(Description = "Whether to enable opcode tracing.", DefaultValue = "false")]

/// <summary>
/// Gets or sets the directory where opcode trace JSON files are written.
/// </summary>
[ConfigItem(Description = "Directory where opcode trace JSON files are written.", DefaultValue = "traces/opcodes")]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
[ConfigItem(Description = "Directory where opcode trace JSON files are written.", DefaultValue = "traces/opcodes")]
[ConfigItem(Description = "Directory of opcode trace JSON files to be written.", DefaultValue = "traces/opcodes")]

/// Gets or sets the opcode counts dictionary mapping opcode names to occurrence counts.
/// </summary>
[JsonPropertyName("opcodeCounts")]
public required Dictionary<string, long> OpcodeCounts { get; init; }
Copy link
Member

Choose a reason for hiding this comment

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

It would probably be faster to accumulate if it was

Suggested change
public required Dictionary<string, long> OpcodeCounts { get; init; }
public required Dictionary<byte, long> OpcodeCounts { get; init; }

And we can add JsonConverter to serialize it later as string (or just convert to <string, long> dictionary

/// <returns>The full path to the created file, or null if writing failed.</returns>
public async Task<string?> WriteAsync(string outputDirectory, TraceOutput traceOutput)
{
if (string.IsNullOrWhiteSpace(outputDirectory))
Copy link
Member

Choose a reason for hiding this comment

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

You might want to use PathUtils.GetApplicationResourcePath to use relative/absolute paths better and potentially avoid issues with environments with restricted access

/// Gets or sets the opcode counts dictionary mapping opcode names to occurrence counts for this block.
/// </summary>
[JsonPropertyName("opcodeCounts")]
public required Dictionary<string, long> OpcodeCounts { get; init; }
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public required Dictionary<string, long> OpcodeCounts { get; init; }
public required Dictionary<byte, long> OpcodeCounts { get; init; }

Comment on lines 28 to 30
labels[i] = Enum.IsDefined(typeof(Instruction), opcode)
? ((Instruction)opcode).ToString()
: $"0x{opcode:x2}";
Copy link
Member

Choose a reason for hiding this comment

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

use F

Suggested change
labels[i] = Enum.IsDefined(typeof(Instruction), opcode)
? ((Instruction)opcode).ToString()
: $"0x{opcode:x2}";
labels[i] = FastEnum.IsDefined(typeof(Instruction), opcode)
? FastEnum.ToString((Instruction)opcode)
: $"0x{opcode:x2}";

Comment on lines 54 to 73
if (traceOutput is null)
{
if (_logger.IsError)
{
_logger.Error("Cumulative trace output is null");
}
return null;
}

try
{
// Ensure directory exists
if (!Directory.Exists(_outputDirectory))
{
Directory.CreateDirectory(_outputDirectory);
}

// Serialize and write (overwrite existing file)
string json = JsonSerializer.Serialize(traceOutput, _serializerOptions);
await File.WriteAllTextAsync(_filePath!, json).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

Same here

/// Gets or sets the aggregated opcode counts dictionary mapping opcode names to total occurrence counts.
/// </summary>
[JsonPropertyName("opcodeCounts")]
public required Dictionary<string, long> OpcodeCounts { get; init; }
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public required Dictionary<string, long> OpcodeCounts { get; init; }
public required Dictionary<byte, long> OpcodeCounts { get; init; }

Comment on lines 119 to 120
BlockHash = blockHash.ToString(),
ParentHash = parentHash.ToString(),
Copy link
Member

Choose a reason for hiding this comment

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

keep it as Hash256

@dmitriy-b dmitriy-b requested a review from LukaszRozmej January 2, 2026 12:01
@dmitriy-b
Copy link
Contributor Author

@LukaszRozmej thank you for the review. I think I fixed everything, please take a look

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants