Skip to content
Merged
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
Expand Up @@ -37,6 +37,7 @@ public static AlwaysCancelTxTracer Instance
public bool IsTracingMemory => true;
public bool IsTracingInstructions => true;
public bool IsTracingRefunds => true;
public bool IsTracingReturnData => true;
public bool IsTracingCode => true;
public bool IsTracingStack => true;
public bool IsTracingState => true;
Expand All @@ -59,6 +60,7 @@ public static AlwaysCancelTxTracer Instance
public void ReportLog(LogEntry log) => throw new OperationCanceledException(ErrorMessage);

public void SetOperationMemorySize(ulong newSize) => throw new OperationCanceledException(ErrorMessage);
public void SetOperationReturnData(ReadOnlyMemory<byte> returnData) => throw new OperationCanceledException(ErrorMessage);

public void ReportMemoryChange(long offset, in ReadOnlySpan<byte> data) => throw new OperationCanceledException(ErrorMessage);
public void ReportStorageChange(in ReadOnlySpan<byte> key, in ReadOnlySpan<byte> value) => throw new OperationCanceledException(ErrorMessage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class BlockReceiptsTracer(bool parallel = false) : IBlockTracer, ITxTrace
public bool IsTracingMemory => _currentTxTracer.IsTracingMemory;
public bool IsTracingInstructions => _currentTxTracer.IsTracingInstructions;
public bool IsTracingRefunds => _currentTxTracer.IsTracingRefunds;
public bool IsTracingReturnData => _currentTxTracer.IsTracingReturnData;
public bool IsTracingCode => _currentTxTracer.IsTracingCode;
public bool IsTracingStack => _currentTxTracer.IsTracingStack;
public bool IsTracingState => _currentTxTracer.IsTracingState;
Expand Down Expand Up @@ -166,6 +167,9 @@ public void ReportLog(LogEntry log) =>
public void SetOperationMemorySize(ulong newSize) =>
_currentTxTracer.SetOperationMemorySize(newSize);

public void SetOperationReturnData(ReadOnlyMemory<byte> returnData) =>
_currentTxTracer.SetOperationReturnData(returnData);

public void ReportMemoryChange(long offset, in ReadOnlySpan<byte> data) =>
_currentTxTracer.ReportMemoryChange(offset, data);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public sealed class GethLikeTxDirectStreamingTracer : GethLikeTxTracer
private byte[]? _memoryBuffer;
private int _memoryByteCount;

private byte[]? _returnDataBuffer;
private int _returnDataByteCount;
private byte[]? _returnDataHexBuffer;

private ArrayPoolList<PooledDictionary<UInt256, UInt256>>? _storageByDepth;
private int _activeStorageDepth;

Expand Down Expand Up @@ -103,6 +107,7 @@ internal void ResetForNextTx(Transaction? transaction)
_refundCheckpoints.Clear();
_stackByteCount = 0;
_memoryByteCount = 0;
_returnDataByteCount = 0;
if (_storageByDepth is not null)
{
for (int i = 0; i < _activeStorageDepth; i++) _storageByDepth[i].Clear();
Expand Down Expand Up @@ -137,6 +142,7 @@ public override void StartOperation(int pc, Instruction opcode, long gas, in Exe
_gasCostAlreadySet = false;
_stackByteCount = 0;
_memoryByteCount = 0;
_returnDataByteCount = 0;
}

public override void ReportOperationRemainingGas(long gas)
Expand Down Expand Up @@ -196,6 +202,18 @@ public override void SetOperationStorage(Address address, UInt256 storageIndex,
top[storageIndex] = new UInt256(newValue, isBigEndian: true);
}

public override void SetOperationReturnData(ReadOnlyMemory<byte> returnData)
{
if (!_hasPendingOpcode) return;
int needed = returnData.Length;
if (needed == 0) { _returnDataByteCount = 0; return; }

// The source buffer is reused across opcodes, so copy the bytes into our own scratch.
EnsureBuffer(ref _returnDataBuffer, needed);
returnData.Span.CopyTo(_returnDataBuffer.AsSpan(0, needed));
_returnDataByteCount = needed;
}

public override GethLikeTxTrace BuildResult()
{
FinalizePendingOpcode();
Expand Down Expand Up @@ -246,6 +264,8 @@ private void ReturnPooledBuffers()
{
if (_stackBuffer is not null) { ArrayPool<byte>.Shared.Return(_stackBuffer); _stackBuffer = null; }
if (_memoryBuffer is not null) { ArrayPool<byte>.Shared.Return(_memoryBuffer); _memoryBuffer = null; }
if (_returnDataBuffer is not null) { ArrayPool<byte>.Shared.Return(_returnDataBuffer); _returnDataBuffer = null; }
if (_returnDataHexBuffer is not null) { ArrayPool<byte>.Shared.Return(_returnDataHexBuffer); _returnDataHexBuffer = null; }
if (_storageByDepth is not null)
{
for (int i = 0; i < _storageByDepth.Count; i++) _storageByDepth[i].Dispose();
Expand Down Expand Up @@ -279,10 +299,30 @@ private void WriteOpcodeJson()
if (IsTracingStack) WriteStackArrayIfPresent();
if (IsTracingFullMemory) WriteMemoryArrayIfPresent();
if (IsTracingOpLevelStorage) WriteStorageObjectIfPresent();
if (IsTracingReturnData && _returnDataByteCount > 0) WriteReturnDataValue();

_writer.WriteEndObject();
}

private void WriteReturnDataValue()
{
// Encode "0x"-prefixed hex straight into a pooled scratch buffer and emit it as a raw JSON
// string, avoiding the intermediate string allocation on every traced opcode after a call.
int hexLength = _returnDataByteCount * 2;
int tokenLength = hexLength + 4; // quotes + "0x"
EnsureBuffer(ref _returnDataHexBuffer, tokenLength);

Span<byte> token = _returnDataHexBuffer.AsSpan(0, tokenLength);
token[0] = (byte)'"';
token[1] = (byte)'0';
token[2] = (byte)'x';
_returnDataBuffer.AsSpan(0, _returnDataByteCount).OutputBytesToByteHex(token.Slice(3, hexLength), extraNibble: false);
token[tokenLength - 1] = (byte)'"';

_writer.WritePropertyName("returnData"u8);
_writer.WriteRawValue(token, skipInputValidation: true);
}

private void WriteStackArrayIfPresent()
{
_writer.WriteStartArray("stack"u8);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ public override void StartOperation(int pc, Instruction opcode, long gas, in Exe
}
}

public override void SetOperationReturnData(ReadOnlyMemory<byte> returnData)
{
if (CurrentTraceEntry is not null && !returnData.IsEmpty)
CurrentTraceEntry.ReturnData = returnData.Span.ToHexString(true);
}

public override void ReportRefund(long refund) => _refund += refund;

public override void ReportAction(long gas, UInt256 value, Address from, Address to, ReadOnlyMemory<byte> input, ExecutionType callType, bool isPrecompileCall = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ protected GethLikeTxTracer(GethTraceOptions options)
IsTracingOpLevelStorage = !options.DisableStorage;
IsTracingStack = !options.DisableStack;
IsTracingFullMemory = options.EnableMemory;
IsTracingReturnData = options.EnableReturnData;
IsTracing = IsTracing || IsTracingFullMemory;

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 – IsTracing override doesn't cover IsTracingReturnData.

IsTracing = IsTracing || IsTracingFullMemory was added so that IsTracingFullMemory (which isn't a standard ITxTracer flag) still keeps IsTracing true. IsTracingReturnData is a standard interface flag, so it should instead be fixed in the ITxTracer.IsTracing default (see comment there). As a belt-and-suspenders measure, consider also adding || IsTracingReturnData here — or, better, fix the source of truth in the interface.

In practice this is benign today because IsTracingInstructions returns true unconditionally from GethLikeTxTracer, ensuring IsTracing is always true regardless.

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public record GethTraceOptions

public bool EnableMemory { get; init; }

public bool EnableReturnData { get; init; }

public bool DisableStack { get; init; }

[JsonConverter(typeof(CustomTimeDurationConverter))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@ public GethTxTraceEntry()

public Dictionary<string, string>? Storage { get; set; }

[JsonPropertyName("returnData")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ReturnData { get; set; }

internal virtual void UpdateMemorySize(ulong size) { }
}
5 changes: 5 additions & 0 deletions src/Nethermind/Nethermind.Evm.Test/EvmPooledMemoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ public class MyTracer : ITxTracer, IDisposable
public bool IsTracingDetailedMemory { get; set; } = true;
public bool IsTracingInstructions => true;
public bool IsTracingRefunds { get; } = false;
public bool IsTracingReturnData { get; } = false;
public bool IsTracingCode => true;
public bool IsTracingStack { get; set; } = true;
public bool IsTracingState => false;
Expand Down Expand Up @@ -466,6 +467,10 @@ public void SetOperationMemorySize(ulong newSize)
{
}

public void SetOperationReturnData(ReadOnlyMemory<byte> returnData)
{
}

public void ReportMemoryChange(long offset, in ReadOnlySpan<byte> data)
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,42 @@ public void Streams_journaled_refund_counter(bool clearingFrameReverts)

private static long? RefundAt(IEnumerable<StructLog> logs, string opcode) => logs.Single(l => l.Op == opcode).Refund;

private List<StructLog> ExecuteAndStream(bool clearingFrameReverts)
[Test]
public void Streams_returndata_when_enabled()
{
const string word = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";

List<StructLog> logs = StreamLogs(ReturnDataCallCode(word), GethTraceOptions.Default with { EnableReturnData = true });

using (Assert.EnterMultipleScope())
{
Assert.That(logs.Last(l => l.Op == "CALL").ReturnData, Is.Null, "no return data before the call executes");
Assert.That(logs.Last(l => l.Op == "STOP" && l.Depth == 1).ReturnData, Is.EqualTo($"0x{word}"));
}
}

[Test]
public void Omits_returndata_when_not_enabled()
{
const string word = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";

List<StructLog> logs = StreamLogs(ReturnDataCallCode(word), GethTraceOptions.Default);

Assert.That(logs.All(l => l.ReturnData is null), Is.True);
}

private List<StructLog> ExecuteAndStream(bool clearingFrameReverts) =>
StreamLogs(clearingFrameReverts ? RevertingCallCode() : ClearingSstoreCode(), GethTraceOptions.Default);

private List<StructLog> StreamLogs(byte[] code, GethTraceOptions options)
{
byte[] code = clearingFrameReverts ? RevertingCallCode() : ClearingSstoreCode();
(Block block, Transaction transaction) = PrepareTx(Activation, 100000, code);

using MemoryStream stream = new();
using (Utf8JsonWriter writer = new(stream))
{
writer.WriteStartArray();
GethLikeTxDirectStreamingTracer tracer = new(transaction, GethTraceOptions.Default, writer, null, CancellationToken.None);
GethLikeTxDirectStreamingTracer tracer = new(transaction, options, writer, null, CancellationToken.None);
_processor.Execute(transaction, new BlockExecutionContext(block.Header, SpecProvider.GetSpec(block.Header)), tracer);
tracer.BuildResult();
writer.WriteEndArray();
Expand All @@ -65,7 +91,25 @@ private List<StructLog> ExecuteAndStream(bool clearingFrameReverts)
return [.. document.RootElement.EnumerateArray().Select(static e => new StructLog(
e.GetProperty("op").GetString()!,
e.GetProperty("depth").GetInt32(),
e.TryGetProperty("refund", out JsonElement refund) ? refund.GetInt64() : null))];
e.TryGetProperty("refund", out JsonElement refund) ? refund.GetInt64() : null,
e.TryGetProperty("returnData", out JsonElement returnData) ? returnData.GetString() : null))];
}

private byte[] ReturnDataCallCode(string word)
{
byte[] calleeCode = Prepare.EvmCode
.StoreDataInMemory(0, word)
.Return(32, 0)
.Done;

TestState.CreateAccount(TestItem.AddressC, 1.Ether);
TestState.InsertCode(TestItem.AddressC, calleeCode, Spec);
TestState.Commit(Spec);

return Prepare.EvmCode
.Call(TestItem.AddressC, 50000)
.Op(Instruction.STOP)
.Done;
}

private byte[] ClearingSstoreCode()
Expand Down Expand Up @@ -100,5 +144,5 @@ private byte[] RevertingCallCode()
.Done;
}

private readonly record struct StructLog(string Op, int Depth, long? Refund);
private readonly record struct StructLog(string Op, int Depth, long? Refund, string? ReturnData);
}
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,62 @@ public void Refund_is_rolled_back_when_frame_reverts()
}
}

[Test]
public void Can_trace_returndata_when_enabled()
{
const string word = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";

byte[] calleeCode = Prepare.EvmCode
.StoreDataInMemory(0, word)
.Return(32, 0)
.Done;

TestState.CreateAccount(TestItem.AddressC, 1.Ether);
TestState.InsertCode(TestItem.AddressC, calleeCode, Spec);
TestState.Commit(Spec);

byte[] code = Prepare.EvmCode
.Call(TestItem.AddressC, 50000)
.Op(Instruction.STOP)
.Done;

GethLikeTxTrace trace = ExecuteAndTrace(GethTraceOptions.Default with { EnableReturnData = true }, code);

GethTxTraceEntry call = trace.Entries.Last(e => e.Opcode == "CALL");
GethTxTraceEntry topLevelStop = trace.Entries.Last(e => e.Opcode == "STOP" && e.Depth == 1);

using (Assert.EnterMultipleScope())
{
Assert.That(call.ReturnData, Is.Null, "no return data before the call executes");
Assert.That(topLevelStop.ReturnData, Is.EqualTo($"0x{word}"), "return data visible after the inner call returns");
}
}

[Test]
public void ReturnData_is_omitted_when_not_enabled()
{
const string word = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";

byte[] calleeCode = Prepare.EvmCode
.StoreDataInMemory(0, word)
.Return(32, 0)
.Done;

TestState.CreateAccount(TestItem.AddressC, 1.Ether);
TestState.InsertCode(TestItem.AddressC, calleeCode, Spec);
TestState.Commit(Spec);

byte[] code = Prepare.EvmCode
.Call(TestItem.AddressC, 50000)
.Op(Instruction.STOP)
.Done;

// Default options leave EnableReturnData off, so the field must never be populated.
GethLikeTxTrace trace = ExecuteAndTrace(code);

Assert.That(trace.Entries.All(e => e.ReturnData is null), Is.True);
}

private static void AssertEntry(GethTxTraceEntry entry, long expectedPc, string expectedOpcode, string expectedStackTop, int expectedStackCount)
{
using (Assert.EnterMultipleScope())
Expand Down
8 changes: 8 additions & 0 deletions src/Nethermind/Nethermind.Evm.Test/VirtualMachineTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ protected GethLikeTxTrace ExecuteAndTrace(params byte[] code)
return tracer.BuildResult();
}

protected GethLikeTxTrace ExecuteAndTrace(GethTraceOptions options, params byte[] code)
{
(Block block, Transaction transaction) = PrepareTx(Activation, 100000, code);
GethLikeTxMemoryTracer tracer = new(transaction, options);
_processor.Execute(transaction, new BlockExecutionContext(block.Header, SpecProvider.GetSpec(block.Header)), tracer);
return tracer.BuildResult();
}

protected GethLikeTxTrace ExecuteAndTrace(long blockNumber, long gasLimit, params byte[] code)
{
(Block block, Transaction transaction) = PrepareTx((blockNumber, Timestamp), gasLimit, code);
Expand Down
16 changes: 16 additions & 0 deletions src/Nethermind/Nethermind.Evm/Tracing/CancellationTxTracer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class CancellationTxTracer(ITxTracer innerTracer, CancellationToken token
private readonly bool _isTracingMemory;
private readonly bool _isTracingInstructions;
private readonly bool _isTracingRefunds;
private readonly bool _isTracingReturnData;
private readonly bool _isTracingCode;
private readonly bool _isTracingStack;
private readonly bool _isTracingState;
Expand Down Expand Up @@ -69,6 +70,12 @@ public bool IsTracingRefunds
init => _isTracingRefunds = value;
}

public bool IsTracingReturnData
{
get => _isTracingReturnData || innerTracer.IsTracingReturnData;
init => _isTracingReturnData = value;
}

public bool IsTracingCode
{
get => _isTracingCode || innerTracer.IsTracingCode;
Expand Down Expand Up @@ -280,6 +287,15 @@ public void SetOperationMemorySize(ulong newSize)
}
}

public void SetOperationReturnData(ReadOnlyMemory<byte> returnData)
{
token.ThrowIfCancellationRequested();
if (innerTracer.IsTracingReturnData)
{
innerTracer.SetOperationReturnData(returnData);
}
}

public void ReportMemoryChange(long offset, in ReadOnlySpan<byte> data)
{
token.ThrowIfCancellationRequested();
Expand Down
Loading
Loading