Skip to content

Commit 88dd5a1

Browse files
authored
refactor: Port stack-traces/debug.ts (#596)
* Port over debugging facilities from debug.ts * patch(hardhat): Port debug.ts * fix: Make the depth optional in printMessageTrace * fixup: Formatting
1 parent 54e7234 commit 88dd5a1

10 files changed

+822
-151
lines changed

crates/edr_napi/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,8 @@ export interface SourceMap {
387387
location: SourceMapLocation
388388
jumpType: JumpType
389389
}
390+
export function printMessageTrace(trace: PrecompileMessageTrace | CallMessageTrace | CreateMessageTrace, depth?: number | undefined | null): void
391+
export function printStackTrace(trace: SolidityStackTrace): void
390392
export interface SubmessageData {
391393
messageTrace: PrecompileMessageTrace | CallMessageTrace | CreateMessageTrace
392394
stacktrace: SolidityStackTrace

crates/edr_napi/index.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ if (!nativeBinding) {
310310
throw new Error(`Failed to load native binding`)
311311
}
312312

313-
const { SpecId, EdrContext, MineOrdering, Provider, Response, SuccessReason, ExceptionalHalt, createModelsAndDecodeBytecodes, linkHexStringBytecode, SourceFile, SourceLocation, ContractFunctionType, ContractFunctionVisibility, ContractFunction, CustomError, Instruction, JumpType, jumpTypeToString, Bytecode, ContractType, Contract, ContractsIdentifier, Exit, ExitCode, Opcode, opcodeToString, isPush, isJump, isCall, isCreate, ReturnData, StackTraceEntryType, stackTraceEntryTypeToString, FALLBACK_FUNCTION_NAME, RECEIVE_FUNCTION_NAME, CONSTRUCTOR_FUNCTION_NAME, UNRECOGNIZED_FUNCTION_NAME, UNKNOWN_FUNCTION_NAME, PRECOMPILE_FUNCTION_NAME, UNRECOGNIZED_CONTRACT_NAME, SolidityTracer, VmTraceDecoder, initializeVmTraceDecoder, VmTracer, RawTrace } = nativeBinding
313+
const { SpecId, EdrContext, MineOrdering, Provider, Response, SuccessReason, ExceptionalHalt, createModelsAndDecodeBytecodes, linkHexStringBytecode, SourceFile, SourceLocation, ContractFunctionType, ContractFunctionVisibility, ContractFunction, CustomError, Instruction, JumpType, jumpTypeToString, Bytecode, ContractType, Contract, ContractsIdentifier, printMessageTrace, printStackTrace, Exit, ExitCode, Opcode, opcodeToString, isPush, isJump, isCall, isCreate, ReturnData, StackTraceEntryType, stackTraceEntryTypeToString, FALLBACK_FUNCTION_NAME, RECEIVE_FUNCTION_NAME, CONSTRUCTOR_FUNCTION_NAME, UNRECOGNIZED_FUNCTION_NAME, UNKNOWN_FUNCTION_NAME, PRECOMPILE_FUNCTION_NAME, UNRECOGNIZED_CONTRACT_NAME, SolidityTracer, VmTraceDecoder, initializeVmTraceDecoder, VmTracer, RawTrace } = nativeBinding
314314

315315
module.exports.SpecId = SpecId
316316
module.exports.EdrContext = EdrContext
@@ -334,6 +334,8 @@ module.exports.Bytecode = Bytecode
334334
module.exports.ContractType = ContractType
335335
module.exports.Contract = Contract
336336
module.exports.ContractsIdentifier = ContractsIdentifier
337+
module.exports.printMessageTrace = printMessageTrace
338+
module.exports.printStackTrace = printStackTrace
337339
module.exports.Exit = Exit
338340
module.exports.ExitCode = ExitCode
339341
module.exports.Opcode = Opcode

crates/edr_napi/src/trace.rs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mod model;
2222
mod source_map;
2323

2424
mod contracts_identifier;
25+
mod debug;
2526
mod error_inferrer;
2627
mod exit;
2728
mod mapped_inlined_internal_functions_heuristics;

crates/edr_napi/src/trace/debug.rs

+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
//! Port of `hardhat-network/stack-traces/debug.ts` from Hardhat.
2+
3+
use edr_eth::U256;
4+
use edr_evm::hex;
5+
use napi::{
6+
bindgen_prelude::{Either24, Either3, Either4},
7+
Either, Env,
8+
};
9+
use napi_derive::napi;
10+
11+
use super::{
12+
message_trace::{CallMessageTrace, CreateMessageTrace, PrecompileMessageTrace},
13+
solidity_stack_trace::{RevertErrorStackTraceEntry, SolidityStackTrace},
14+
};
15+
use crate::trace::{model::JumpType, return_data::ReturnData};
16+
17+
const MARGIN_SPACE: usize = 6;
18+
19+
#[napi]
20+
fn print_message_trace(
21+
trace: Either3<PrecompileMessageTrace, CallMessageTrace, CreateMessageTrace>,
22+
depth: Option<u32>,
23+
env: Env,
24+
) -> napi::Result<()> {
25+
let trace = match &trace {
26+
Either3::A(precompile) => Either3::A(precompile),
27+
Either3::B(call) => Either3::B(call),
28+
Either3::C(create) => Either3::C(create),
29+
};
30+
31+
let depth = depth.unwrap_or(0);
32+
33+
print_message_trace_inner(trace, depth, env)
34+
}
35+
36+
fn print_message_trace_inner(
37+
trace: Either3<&PrecompileMessageTrace, &CallMessageTrace, &CreateMessageTrace>,
38+
depth: u32,
39+
env: Env,
40+
) -> napi::Result<()> {
41+
println!();
42+
43+
match trace {
44+
Either3::A(precompile) => print_precompile_trace(precompile, depth),
45+
Either3::B(call) => print_call_trace(call, depth, env)?,
46+
Either3::C(create) => print_create_trace(create, depth, env)?,
47+
}
48+
49+
println!();
50+
51+
Ok(())
52+
}
53+
54+
fn print_precompile_trace(trace: &PrecompileMessageTrace, depth: u32) {
55+
let margin = " ".repeat(depth as usize * MARGIN_SPACE);
56+
57+
let value = U256::from_limbs_slice(&trace.value.words);
58+
59+
println!("{margin}Precompile trace");
60+
61+
println!("{margin} precompile number: {}", trace.precompile);
62+
println!("{margin} value: {value}");
63+
println!(
64+
"{margin} calldata: {}",
65+
hex::encode_prefixed(&*trace.calldata)
66+
);
67+
68+
if trace.exit.is_error() {
69+
println!("{margin} error: {}", trace.exit.get_reason());
70+
}
71+
72+
println!(
73+
"{margin} returnData: {}",
74+
hex::encode_prefixed(&*trace.return_data)
75+
);
76+
}
77+
78+
fn print_call_trace(trace: &CallMessageTrace, depth: u32, env: Env) -> napi::Result<()> {
79+
let margin = " ".repeat(depth as usize * MARGIN_SPACE);
80+
81+
println!("{margin}Call trace");
82+
83+
if let Some(bytecode) = &trace.bytecode {
84+
let contract = bytecode.contract.borrow(env)?;
85+
let location = contract.location.borrow(env)?;
86+
let file = location.file.borrow(env)?;
87+
88+
println!(
89+
"{margin} calling contract: {}:{}",
90+
file.source_name, contract.name
91+
);
92+
} else {
93+
println!(
94+
"{margin} unrecognized contract code: {:?}",
95+
hex::encode_prefixed(&*trace.code)
96+
);
97+
println!(
98+
"{margin} contract: {}",
99+
hex::encode_prefixed(&*trace.address)
100+
);
101+
}
102+
103+
println!(
104+
"{margin} value: {}",
105+
U256::from_limbs_slice(&trace.value.words)
106+
);
107+
println!(
108+
"{margin} calldata: {}",
109+
hex::encode_prefixed(&*trace.calldata)
110+
);
111+
112+
if trace.exit.is_error() {
113+
println!("{margin} error: {}", trace.exit.get_reason());
114+
}
115+
116+
println!(
117+
"{margin} returnData: {}",
118+
hex::encode_prefixed(&*trace.return_data)
119+
);
120+
121+
trace_steps(Either::A(trace), depth, env)
122+
}
123+
124+
fn print_create_trace(trace: &CreateMessageTrace, depth: u32, env: Env) -> napi::Result<()> {
125+
let margin = " ".repeat(depth as usize * MARGIN_SPACE);
126+
127+
println!("{margin}Create trace");
128+
129+
if let Some(bytecode) = &trace.bytecode {
130+
let contract = bytecode.contract.borrow(env)?;
131+
132+
println!("{margin} deploying contract: {}", contract.name);
133+
println!("{margin} code: {}", hex::encode_prefixed(&*trace.code));
134+
} else {
135+
println!(
136+
"{margin} unrecognized deployment code: {}",
137+
hex::encode_prefixed(&*trace.code)
138+
);
139+
}
140+
141+
println!(
142+
"{margin} value: {}",
143+
U256::from_limbs_slice(&trace.value.words)
144+
);
145+
146+
if let Some(Either::A(deployed_contract)) = &trace.deployed_contract {
147+
println!(
148+
"{margin} contract address: {}",
149+
hex::encode_prefixed(deployed_contract)
150+
);
151+
}
152+
153+
if trace.exit.is_error() {
154+
println!("{margin} error: {}", trace.exit.get_reason());
155+
// The return data is the deployed-bytecode if there was no error, so we don't
156+
// show it
157+
println!(
158+
"{margin} returnData: {}",
159+
hex::encode_prefixed(&*trace.return_data)
160+
);
161+
}
162+
163+
trace_steps(Either::B(trace), depth, env)?;
164+
165+
Ok(())
166+
}
167+
168+
fn trace_steps(
169+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
170+
depth: u32,
171+
env: Env,
172+
) -> napi::Result<()> {
173+
let margin = " ".repeat(depth as usize * MARGIN_SPACE);
174+
175+
println!("{margin} steps:");
176+
println!();
177+
178+
let (bytecode, steps) = match &trace {
179+
Either::A(call) => (&call.bytecode, &call.steps),
180+
Either::B(create) => (&create.bytecode, &create.steps),
181+
};
182+
183+
for step in steps {
184+
let step = match step {
185+
Either4::A(step) => step,
186+
trace @ (Either4::B(..) | Either4::C(..) | Either4::D(..)) => {
187+
let trace = match trace {
188+
Either4::A(..) => unreachable!(),
189+
Either4::B(precompile) => Either3::A(precompile),
190+
Either4::C(call) => Either3::B(call),
191+
Either4::D(create) => Either3::C(create),
192+
};
193+
194+
print_message_trace_inner(trace, depth + 1, env)?;
195+
continue;
196+
}
197+
};
198+
199+
let pc = format!("{:>5}", format!("{:03}", step.pc));
200+
201+
if let Some(bytecode) = bytecode {
202+
let inst = bytecode.get_instruction_inner(step.pc)?;
203+
let inst = inst.borrow(env)?;
204+
205+
let location = inst
206+
.location
207+
.as_ref()
208+
.map(|inst_location| {
209+
let inst_location = inst_location.borrow(env)?;
210+
let file = inst_location.file.borrow(env)?;
211+
212+
let mut location_str = file.source_name.clone();
213+
214+
if let Some(func) = inst_location.get_containing_function_inner(env)? {
215+
let func = func.borrow(env)?;
216+
let location = func.location.borrow(env)?;
217+
let file = location.file.borrow(env)?;
218+
219+
let contract_name = func
220+
.contract
221+
.as_ref()
222+
.map(|contract| -> napi::Result<_> {
223+
Ok(contract.borrow(env)?.name.clone())
224+
})
225+
.transpose()?;
226+
227+
let source_name = contract_name.unwrap_or_else(|| file.source_name.clone());
228+
229+
location_str += &format!(":{source_name}:{}", func.name);
230+
}
231+
location_str +=
232+
&format!(" - {}:{}", inst_location.offset, inst_location.length);
233+
234+
napi::Result::Ok(location_str)
235+
})
236+
.transpose()?
237+
.unwrap_or_default();
238+
239+
if inst.opcode.is_jump() {
240+
let jump = if inst.jump_type == JumpType::NOT_JUMP {
241+
"".to_string()
242+
} else {
243+
format!("({})", inst.jump_type.into_static_str())
244+
};
245+
246+
let entry = format!(
247+
"{margin} {pc} {opcode} {jump}",
248+
opcode = inst.opcode.into_static_str()
249+
);
250+
251+
println!("{entry:<50}{location}");
252+
} else if inst.opcode.is_push() {
253+
let entry = format!(
254+
"{margin} {pc} {opcode} {push_data}",
255+
opcode = inst.opcode.into_static_str(),
256+
push_data = inst
257+
.push_data
258+
.as_deref()
259+
.map(hex::encode_prefixed)
260+
.unwrap_or_default()
261+
);
262+
263+
println!("{entry:<50}{location}");
264+
} else {
265+
let entry = format!(
266+
"{margin} {pc} {opcode}",
267+
opcode = inst.opcode.into_static_str()
268+
);
269+
270+
println!("{entry:<50}{location}");
271+
}
272+
} else {
273+
println!("{margin} {pc}");
274+
}
275+
}
276+
277+
Ok(())
278+
}
279+
280+
#[napi]
281+
fn print_stack_trace(trace: SolidityStackTrace) -> napi::Result<()> {
282+
let entry_values = trace
283+
.into_iter()
284+
.map(|entry| match entry {
285+
Either24::A(entry) => serde_json::to_value(entry),
286+
Either24::B(entry) => serde_json::to_value(entry),
287+
Either24::C(entry) => serde_json::to_value(entry),
288+
Either24::D(entry) => serde_json::to_value(entry),
289+
Either24::F(entry) => serde_json::to_value(entry),
290+
Either24::G(entry) => serde_json::to_value(entry),
291+
Either24::H(entry) => serde_json::to_value(entry),
292+
Either24::I(entry) => serde_json::to_value(entry),
293+
Either24::J(entry) => serde_json::to_value(entry),
294+
Either24::K(entry) => serde_json::to_value(entry),
295+
Either24::L(entry) => serde_json::to_value(entry),
296+
Either24::M(entry) => serde_json::to_value(entry),
297+
Either24::N(entry) => serde_json::to_value(entry),
298+
Either24::O(entry) => serde_json::to_value(entry),
299+
Either24::P(entry) => serde_json::to_value(entry),
300+
Either24::Q(entry) => serde_json::to_value(entry),
301+
Either24::R(entry) => serde_json::to_value(entry),
302+
Either24::S(entry) => serde_json::to_value(entry),
303+
Either24::T(entry) => serde_json::to_value(entry),
304+
Either24::U(entry) => serde_json::to_value(entry),
305+
Either24::V(entry) => serde_json::to_value(entry),
306+
Either24::W(entry) => serde_json::to_value(entry),
307+
Either24::X(entry) => serde_json::to_value(entry),
308+
// Decode the error message from the return data
309+
Either24::E(entry @ RevertErrorStackTraceEntry { .. }) => {
310+
use serde::de::Error;
311+
312+
let decoded_error_msg = ReturnData::new(entry.return_data.clone())
313+
.decode_error()
314+
.map_err(|e| {
315+
serde_json::Error::custom(format_args!("Error decoding return data: {e}"))
316+
})?;
317+
318+
let mut value = serde_json::to_value(entry)?;
319+
value["message"] = decoded_error_msg.into();
320+
Ok(value)
321+
}
322+
})
323+
.collect::<Result<Vec<_>, _>>()
324+
.map_err(|e| napi::Error::from_reason(format!("Error converting to JSON: {e}")))?;
325+
326+
println!("{}", serde_json::to_string_pretty(&entry_values)?);
327+
328+
Ok(())
329+
}

crates/edr_napi/src/trace/model.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use napi::{
1010
Either, Env, JsObject,
1111
};
1212
use napi_derive::napi;
13+
use serde::Serialize;
1314
use serde_json::Value;
1415

1516
use super::opcodes::Opcode;
@@ -153,7 +154,7 @@ impl SourceLocation {
153154
}
154155
}
155156

156-
#[derive(PartialEq, Eq)]
157+
#[derive(PartialEq, Eq, Serialize)]
157158
#[allow(non_camel_case_types)] // intentionally mimicks the original case in TS
158159
#[allow(clippy::upper_case_acronyms)]
159160
#[napi]
@@ -296,6 +297,12 @@ pub enum JumpType {
296297
INTERNAL_JUMP,
297298
}
298299

300+
impl JumpType {
301+
pub fn into_static_str(self) -> &'static str {
302+
self.into()
303+
}
304+
}
305+
299306
#[napi]
300307
pub fn jump_type_to_string(jump_type: JumpType) -> &'static str {
301308
jump_type.into()

0 commit comments

Comments
 (0)