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 @@ -208,7 +208,7 @@ public override void MarkAsFailed(Address recipient, in GasConsumed gasSpent, by
if (_error is not null)
{
EvmExceptionType errorType = _error.Value;
firstCallFrame.Error = errorType.GetEvmExceptionDescription();
MarkFrameFailed(firstCallFrame, errorType);
if (errorType == EvmExceptionType.Revert && error is not TransactionSubstate.Revert)
{
firstCallFrame.RevertReason = ValidateRevertReason(error);
Expand Down Expand Up @@ -247,15 +247,25 @@ private void OnExit(ulong gas, ReadOnlyMemory<byte>? output, EvmExceptionType? e
}
}

/// <summary>Records an EVM halt on a call frame, for the root frame and the nested ones alike.</summary>
/// <remarks>
/// A CREATE or CREATE2 frame that halted deployed no contract, so its <c>to</c> is dropped —
/// the execution-apis <c>CallFrame</c> schema requires it to be omitted there.
/// </remarks>
private static void MarkFrameFailed(NativeCallTracerCallFrame callFrame, EvmExceptionType error)
{
callFrame.Error = error.GetEvmExceptionDescription();
if (callFrame.Type is Instruction.CREATE or Instruction.CREATE2)
{
callFrame.To = null;
}
}

private static void ProcessOutput(NativeCallTracerCallFrame callFrame, ReadOnlyMemory<byte>? output, EvmExceptionType? error)
{
if (error is not null)
{
callFrame.Error = error.Value.GetEvmExceptionDescription();
if (callFrame.Type is Instruction.CREATE or Instruction.CREATE2)
{
callFrame.To = null;
}
MarkFrameFailed(callFrame, error.Value);

if (error == EvmExceptionType.Revert && output?.Length != 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,13 +572,16 @@ public void Test_CallTrace_SelfDestruct_OnlyTopCall()
Assert.That(callTrace, Is.EqualTo(expectedCallTrace));
}

[TestCase(false, false, TestName = "TopLevelCreate_Success")]
[TestCase(true, false, TestName = "TopLevelCreate_Revert")]
[TestCase(false, true, TestName = "TopLevelCreate_AddressCollision")]
public void Test_CallTrace_TopLevelCreate(bool revert, bool addressCollision)
public enum CreateOutcome { Success, Revert, InvalidOpcode, AddressCollision }

[TestCase(CreateOutcome.Success, TestName = "TopLevelCreate_Success")]
[TestCase(CreateOutcome.Revert, TestName = "TopLevelCreate_Revert")]
[TestCase(CreateOutcome.InvalidOpcode, TestName = "TopLevelCreate_InvalidOpcode")]
[TestCase(CreateOutcome.AddressCollision, TestName = "TopLevelCreate_AddressCollision")]
public void Test_CallTrace_TopLevelCreate(CreateOutcome outcome)
{
byte[] initCode;
if (addressCollision)
if (outcome == CreateOutcome.AddressCollision)
{
initCode = Prepare.EvmCode.PushData(0).PushData(0).Op(Instruction.RETURN).Done;
Address deploymentAddress = ContractAddress.From(Sender, TestState.GetNonce(Sender));
Expand All @@ -588,17 +591,20 @@ public void Test_CallTrace_TopLevelCreate(bool revert, bool addressCollision)
}
else
{
initCode = revert
? Prepare.EvmCode.PushData(0).PushData(0).Op(Instruction.REVERT).Done
: Prepare.EvmCode.ForInitOf(new byte[3]).Done;
initCode = outcome switch
{
CreateOutcome.Revert => Prepare.EvmCode.PushData(0).PushData(0).Op(Instruction.REVERT).Done,
CreateOutcome.InvalidOpcode => Prepare.EvmCode.Op(Instruction.INVALID).Done,
_ => Prepare.EvmCode.ForInitOf(new byte[3]).Done
};
}

(Block block, Transaction tx) = PrepareInitTx(MainnetSpecProvider.CancunActivation, 100000, initCode);
using NativeCallTracer tracer = new(tx, CancunSpec, GetGethTraceOptions(null));
_processor.Execute(tx, new BlockExecutionContext(block.Header, SpecProvider.GetSpec((block.Header.Number, block.Header.Timestamp))), tracer);
using GethLikeTxTrace trace = tracer.BuildResult();

if (addressCollision)
if (outcome == CreateOutcome.AddressCollision)
{
Assert.That(trace.CustomTracerResult, Is.Null,
"address-collision path never enters the EVM; no call frame should be produced");
Expand All @@ -608,13 +614,16 @@ public void Test_CallTrace_TopLevelCreate(bool revert, bool addressCollision)
NativeCallTracerCallFrame? frame = trace.CustomTracerResult?.Value as NativeCallTracerCallFrame;
Assert.That(frame, Is.Not.Null, "expected a top-level CREATE call frame");
Assert.That(frame!.Type, Is.EqualTo(Instruction.CREATE));
if (revert)
Assert.That(frame.Error, Is.Not.Null, "expected error description on reverted CREATE");
else
if (outcome == CreateOutcome.Success)
{
Assert.That(frame.Error, Is.Null, "expected no error on successful CREATE");
Assert.That(frame.To, Is.Not.Null, "expected deployed contract address");
}
else
{
Assert.That(frame.Error, Is.Not.Null, "expected error description on a halted CREATE");
Assert.That(frame.To, Is.Null, "a failed CREATE deploys no contract, so `to` must be omitted");
}
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,4 +371,68 @@ public async Task Debug_traceCall_CREATE_replayed_back_to_back_does_not_throw_co
"the persisted-code hint must not survive overlay reset");
}
}

private const string RevertingContractAddress = "0xc300000000000000000000000000000000000000";

// Error(string) revert payload for "user error", unpadded, as the execution-apis calltree contract emits it.
private const string RevertPayload =
"08c379a0" +
"0000000000000000000000000000000000000000000000000000000000000020" +
"000000000000000000000000000000000000000000000000000000000000000a" +
"75736572206572726f72";

// PUSH1 0x4e PUSH1 0x0c PUSH1 0 CODECOPY PUSH1 0x4e PUSH1 0 REVERT, then the payload as trailing data.
private const string RevertingContractCode = "0x604e600c600039604e6000fd" + RevertPayload;

[Test]
public async Task Debug_traceCall_with_callTracer_reports_revert_in_the_frame()
{
using Context ctx = await Context.Create();

string response = await RpcTest.TestSerializedRequest(ctx.DebugRpcModule, "debug_traceCall",
new { to = RevertingContractAddress, gas = "0x100000" },
null,
new
{
tracer = "callTracer",
stateOverrides = new Dictionary<string, object>
{
[RevertingContractAddress] = new { code = RevertingContractCode }
}
});

JToken parsed = JToken.Parse(response);
Assert.That(parsed["error"], Is.Null, "a revert is a traced result, not a JSON-RPC error");

JToken frame = parsed["result"]!;
Assert.Multiple(() =>
{
Assert.That((string?)frame["type"], Is.EqualTo("CALL"));
Assert.That((string?)frame["error"], Is.EqualTo("execution reverted"));
Assert.That((string?)frame["revertReason"], Is.EqualTo("user error"));
Assert.That((string?)frame["output"], Is.EqualTo("0x" + RevertPayload));
});
}

[Test]
public async Task Debug_traceCall_with_callTracer_omits_to_on_failed_top_level_create()
{
using Context ctx = await Context.Create();

string response = await RpcTest.TestSerializedRequest(ctx.DebugRpcModule, "debug_traceCall",
new { from = TestItem.AddressA.ToString(), data = "0x60006000fd", gas = "0x100000" },
null,
new { tracer = "callTracer" });

JToken parsed = JToken.Parse(response);
Assert.That(parsed["error"], Is.Null, "a failed deployment is a traced result, not a JSON-RPC error");

JToken frame = parsed["result"]!;
Assert.Multiple(() =>
{
Assert.That((string?)frame["type"], Is.EqualTo("CREATE"));
Assert.That((string?)frame["error"], Is.EqualTo("execution reverted"));
Assert.That(frame["to"], Is.Null, "a failed CREATE deploys no contract, so `to` must be omitted");
});
}
}
Loading