@nomicfoundation/edr 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +7 -0
- package/index.d.ts +277 -1
- package/index.js +23 -1
- package/package.json +44 -35
- package/src/logger.rs +1 -0
- package/src/provider.rs +0 -6
- package/src/trace/compiler.rs +27 -0
- package/src/trace/debug.rs +318 -0
- package/src/trace/error_inferrer.rs +2219 -0
- package/src/trace/exit.rs +86 -0
- package/src/trace/library_utils.rs +6 -0
- package/src/trace/mapped_inlined_internal_functions_heuristics.rs +180 -0
- package/src/trace/message_trace.rs +179 -0
- package/src/trace/model.rs +59 -0
- package/src/trace/return_data.rs +84 -0
- package/src/trace/solidity_stack_trace.rs +694 -0
- package/src/trace/solidity_tracer.rs +315 -0
- package/src/trace/vm_trace_decoder.rs +234 -0
- package/src/trace/vm_tracer.rs +71 -0
- package/src/trace.rs +23 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
use edr_evm::interpreter::OpCode;
|
|
2
|
+
use edr_solidity::build_model::{Instruction, JumpType};
|
|
3
|
+
use napi::{
|
|
4
|
+
bindgen_prelude::{Either3, Either4},
|
|
5
|
+
Either,
|
|
6
|
+
};
|
|
7
|
+
use napi_derive::napi;
|
|
8
|
+
|
|
9
|
+
use super::{
|
|
10
|
+
error_inferrer::{
|
|
11
|
+
instruction_to_callstack_stack_trace_entry, ErrorInferrer, SubmessageDataRef,
|
|
12
|
+
},
|
|
13
|
+
mapped_inlined_internal_functions_heuristics::{
|
|
14
|
+
adjust_stack_trace, stack_trace_may_require_adjustments,
|
|
15
|
+
},
|
|
16
|
+
message_trace::{CallMessageTrace, CreateMessageTrace, EvmStep, PrecompileMessageTrace},
|
|
17
|
+
solidity_stack_trace::{PrecompileErrorStackTraceEntry, SolidityStackTrace},
|
|
18
|
+
};
|
|
19
|
+
use crate::trace::{
|
|
20
|
+
exit::ExitCode,
|
|
21
|
+
solidity_stack_trace::{
|
|
22
|
+
ContractTooLargeErrorStackTraceEntry, SolidityStackTraceEntry, StackTraceEntryTypeConst,
|
|
23
|
+
UnrecognizedContractCallstackEntryStackTraceEntry,
|
|
24
|
+
UnrecognizedContractErrorStackTraceEntry, UnrecognizedCreateCallstackEntryStackTraceEntry,
|
|
25
|
+
UnrecognizedCreateErrorStackTraceEntry,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
#[napi(constructor)]
|
|
30
|
+
pub struct SolidityTracer;
|
|
31
|
+
|
|
32
|
+
#[allow(clippy::unused_self)] // we allow this for convenience for now
|
|
33
|
+
#[napi]
|
|
34
|
+
impl SolidityTracer {
|
|
35
|
+
#[napi(catch_unwind)]
|
|
36
|
+
pub fn get_stack_trace(
|
|
37
|
+
&self,
|
|
38
|
+
trace: Either3<PrecompileMessageTrace, CallMessageTrace, CreateMessageTrace>,
|
|
39
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
40
|
+
let trace = match &trace {
|
|
41
|
+
Either3::A(precompile) => Either3::A(precompile),
|
|
42
|
+
Either3::B(call) => Either3::B(call),
|
|
43
|
+
Either3::C(create) => Either3::C(create),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
self.get_stack_trace_inner(trace)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn get_stack_trace_inner(
|
|
50
|
+
&self,
|
|
51
|
+
trace: Either3<&PrecompileMessageTrace, &CallMessageTrace, &CreateMessageTrace>,
|
|
52
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
53
|
+
let exit = match &trace {
|
|
54
|
+
Either3::A(precompile) => &precompile.exit,
|
|
55
|
+
Either3::B(call) => &call.exit,
|
|
56
|
+
Either3::C(create) => &create.exit,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if !exit.is_error() {
|
|
60
|
+
return Ok(vec![]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
match trace {
|
|
64
|
+
Either3::A(precompile) => Ok(self.get_precompile_message_stack_trace(precompile)?),
|
|
65
|
+
Either3::B(call) if call.bytecode.is_some() => {
|
|
66
|
+
Ok(self.get_call_message_stack_trace(call)?)
|
|
67
|
+
}
|
|
68
|
+
Either3::C(create) if create.bytecode.is_some() => {
|
|
69
|
+
Ok(self.get_create_message_stack_trace(create)?)
|
|
70
|
+
}
|
|
71
|
+
// No bytecode is present
|
|
72
|
+
Either3::B(call) => Ok(self.get_unrecognized_message_stack_trace(Either::A(call))?),
|
|
73
|
+
Either3::C(create) => Ok(self.get_unrecognized_message_stack_trace(Either::B(create))?),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn get_last_subtrace<'a>(
|
|
78
|
+
&self,
|
|
79
|
+
trace: Either<&'a CallMessageTrace, &'a CreateMessageTrace>,
|
|
80
|
+
) -> Option<Either3<&'a PrecompileMessageTrace, &'a CallMessageTrace, &'a CreateMessageTrace>>
|
|
81
|
+
{
|
|
82
|
+
let (number_of_subtraces, steps) = match trace {
|
|
83
|
+
Either::A(create) => (create.number_of_subtraces, &create.steps),
|
|
84
|
+
Either::B(call) => (call.number_of_subtraces, &call.steps),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if number_of_subtraces == 0 {
|
|
88
|
+
return None;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
steps.iter().rev().find_map(|step| match step {
|
|
92
|
+
Either4::A(EvmStep { .. }) => None,
|
|
93
|
+
Either4::B(precompile) => Some(Either3::A(precompile)),
|
|
94
|
+
Either4::C(call) => Some(Either3::B(call)),
|
|
95
|
+
Either4::D(create) => Some(Either3::C(create)),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn get_precompile_message_stack_trace(
|
|
100
|
+
&self,
|
|
101
|
+
trace: &PrecompileMessageTrace,
|
|
102
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
103
|
+
Ok(vec![PrecompileErrorStackTraceEntry {
|
|
104
|
+
type_: StackTraceEntryTypeConst,
|
|
105
|
+
precompile: trace.precompile,
|
|
106
|
+
source_reference: None,
|
|
107
|
+
}
|
|
108
|
+
.into()])
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn get_create_message_stack_trace(
|
|
112
|
+
&self,
|
|
113
|
+
trace: &CreateMessageTrace,
|
|
114
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
115
|
+
let inferred_error = ErrorInferrer::infer_before_tracing_create_message(trace)?;
|
|
116
|
+
|
|
117
|
+
if let Some(inferred_error) = inferred_error {
|
|
118
|
+
return Ok(inferred_error);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
self.trace_evm_execution(Either::B(trace))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn get_call_message_stack_trace(
|
|
125
|
+
&self,
|
|
126
|
+
trace: &CallMessageTrace,
|
|
127
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
128
|
+
let inferred_error = ErrorInferrer::infer_before_tracing_call_message(trace)?;
|
|
129
|
+
|
|
130
|
+
if let Some(inferred_error) = inferred_error {
|
|
131
|
+
return Ok(inferred_error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
self.trace_evm_execution(Either::A(trace))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn get_unrecognized_message_stack_trace(
|
|
138
|
+
&self,
|
|
139
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
140
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
141
|
+
let (trace_exit_kind, trace_return_data) = match &trace {
|
|
142
|
+
Either::A(call) => (call.exit.kind(), &call.return_data),
|
|
143
|
+
Either::B(create) => (create.exit.kind(), &create.return_data),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
let subtrace = self.get_last_subtrace(trace);
|
|
147
|
+
|
|
148
|
+
if let Some(subtrace) = subtrace {
|
|
149
|
+
let (is_error, return_data) = match subtrace {
|
|
150
|
+
Either3::A(precompile) => {
|
|
151
|
+
(precompile.exit.is_error(), precompile.return_data.clone())
|
|
152
|
+
}
|
|
153
|
+
Either3::B(call) => (call.exit.is_error(), call.return_data.clone()),
|
|
154
|
+
Either3::C(create) => (create.exit.is_error(), create.return_data.clone()),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// This is not a very exact heuristic, but most of the time it will be right, as
|
|
158
|
+
// solidity reverts if a call fails, and most contracts are in
|
|
159
|
+
// solidity
|
|
160
|
+
if is_error && trace_return_data.as_ref() == return_data.as_ref() {
|
|
161
|
+
let unrecognized_entry: SolidityStackTraceEntry = match trace {
|
|
162
|
+
Either::A(CallMessageTrace { address, .. }) => {
|
|
163
|
+
UnrecognizedContractCallstackEntryStackTraceEntry {
|
|
164
|
+
type_: StackTraceEntryTypeConst,
|
|
165
|
+
address: address.clone(),
|
|
166
|
+
source_reference: None,
|
|
167
|
+
}
|
|
168
|
+
.into()
|
|
169
|
+
}
|
|
170
|
+
Either::B(CreateMessageTrace { .. }) => {
|
|
171
|
+
UnrecognizedCreateCallstackEntryStackTraceEntry {
|
|
172
|
+
type_: StackTraceEntryTypeConst,
|
|
173
|
+
source_reference: None,
|
|
174
|
+
}
|
|
175
|
+
.into()
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let mut stacktrace = vec![unrecognized_entry];
|
|
180
|
+
stacktrace.extend(self.get_stack_trace_inner(subtrace)?);
|
|
181
|
+
|
|
182
|
+
return Ok(stacktrace);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if trace_exit_kind == ExitCode::CODESIZE_EXCEEDS_MAXIMUM {
|
|
187
|
+
return Ok(vec![ContractTooLargeErrorStackTraceEntry {
|
|
188
|
+
type_: StackTraceEntryTypeConst,
|
|
189
|
+
source_reference: None,
|
|
190
|
+
}
|
|
191
|
+
.into()]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let is_invalid_opcode_error = trace_exit_kind == ExitCode::INVALID_OPCODE;
|
|
195
|
+
|
|
196
|
+
match trace {
|
|
197
|
+
Either::A(trace @ CallMessageTrace { .. }) => {
|
|
198
|
+
Ok(vec![UnrecognizedContractErrorStackTraceEntry {
|
|
199
|
+
type_: StackTraceEntryTypeConst,
|
|
200
|
+
address: trace.address.clone(),
|
|
201
|
+
return_data: trace.return_data.clone(),
|
|
202
|
+
is_invalid_opcode_error,
|
|
203
|
+
source_reference: None,
|
|
204
|
+
}
|
|
205
|
+
.into()])
|
|
206
|
+
}
|
|
207
|
+
Either::B(trace @ CreateMessageTrace { .. }) => {
|
|
208
|
+
Ok(vec![UnrecognizedCreateErrorStackTraceEntry {
|
|
209
|
+
type_: StackTraceEntryTypeConst,
|
|
210
|
+
return_data: trace.return_data.clone(),
|
|
211
|
+
is_invalid_opcode_error,
|
|
212
|
+
source_reference: None,
|
|
213
|
+
}
|
|
214
|
+
.into()])
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn trace_evm_execution(
|
|
220
|
+
&self,
|
|
221
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
222
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
223
|
+
let stack_trace = self.raw_trace_evm_execution(trace)?;
|
|
224
|
+
|
|
225
|
+
if stack_trace_may_require_adjustments(&stack_trace, trace) {
|
|
226
|
+
return adjust_stack_trace(stack_trace, trace);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
Ok(stack_trace)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fn raw_trace_evm_execution(
|
|
233
|
+
&self,
|
|
234
|
+
trace: Either<&CallMessageTrace, &CreateMessageTrace>,
|
|
235
|
+
) -> napi::Result<SolidityStackTrace> {
|
|
236
|
+
let (bytecode, steps, number_of_subtraces) = match &trace {
|
|
237
|
+
Either::A(call) => (&call.bytecode, &call.steps, call.number_of_subtraces),
|
|
238
|
+
Either::B(create) => (&create.bytecode, &create.steps, create.number_of_subtraces),
|
|
239
|
+
};
|
|
240
|
+
let bytecode = bytecode.as_ref().expect("JS code asserts");
|
|
241
|
+
|
|
242
|
+
let mut stacktrace: SolidityStackTrace = vec![];
|
|
243
|
+
|
|
244
|
+
let mut subtraces_seen = 0;
|
|
245
|
+
|
|
246
|
+
// There was a jump into a function according to the sourcemaps
|
|
247
|
+
let mut jumped_into_function = false;
|
|
248
|
+
|
|
249
|
+
let mut function_jumpdests: Vec<&Instruction> = vec![];
|
|
250
|
+
|
|
251
|
+
let mut last_submessage_data: Option<SubmessageDataRef<'_>> = None;
|
|
252
|
+
|
|
253
|
+
let mut iter = steps.iter().enumerate().peekable();
|
|
254
|
+
while let Some((step_index, step)) = iter.next() {
|
|
255
|
+
if let Either4::A(EvmStep { pc }) = step {
|
|
256
|
+
let inst = bytecode.get_instruction(*pc)?;
|
|
257
|
+
|
|
258
|
+
if inst.jump_type == JumpType::IntoFunction && iter.peek().is_some() {
|
|
259
|
+
let (_, next_step) = iter.peek().unwrap();
|
|
260
|
+
let Either4::A(next_evm_step) = next_step else {
|
|
261
|
+
unreachable!("JS code asserted that");
|
|
262
|
+
};
|
|
263
|
+
let next_inst = bytecode.get_instruction(next_evm_step.pc)?;
|
|
264
|
+
|
|
265
|
+
if next_inst.opcode == OpCode::JUMPDEST {
|
|
266
|
+
let frame = instruction_to_callstack_stack_trace_entry(bytecode, inst)?;
|
|
267
|
+
stacktrace.push(match frame {
|
|
268
|
+
Either::A(frame) => frame.into(),
|
|
269
|
+
Either::B(frame) => frame.into(),
|
|
270
|
+
});
|
|
271
|
+
if next_inst.location.is_some() {
|
|
272
|
+
jumped_into_function = true;
|
|
273
|
+
}
|
|
274
|
+
function_jumpdests.push(next_inst);
|
|
275
|
+
}
|
|
276
|
+
} else if inst.jump_type == JumpType::OutofFunction {
|
|
277
|
+
stacktrace.pop();
|
|
278
|
+
function_jumpdests.pop();
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
let message_trace = match step {
|
|
282
|
+
Either4::A(_) => unreachable!("branch is taken above"),
|
|
283
|
+
Either4::B(precompile) => Either3::A(precompile),
|
|
284
|
+
Either4::C(call) => Either3::B(call),
|
|
285
|
+
Either4::D(create) => Either3::C(create),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
subtraces_seen += 1;
|
|
289
|
+
|
|
290
|
+
// If there are more subtraces, this one didn't terminate the execution
|
|
291
|
+
if subtraces_seen < number_of_subtraces {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let submessage_trace = self.get_stack_trace_inner(message_trace)?;
|
|
296
|
+
|
|
297
|
+
last_submessage_data = Some(SubmessageDataRef {
|
|
298
|
+
message_trace,
|
|
299
|
+
step_index: step_index as u32,
|
|
300
|
+
stacktrace: submessage_trace,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let stacktrace_with_inferred_error = ErrorInferrer::infer_after_tracing(
|
|
306
|
+
trace,
|
|
307
|
+
stacktrace,
|
|
308
|
+
&function_jumpdests,
|
|
309
|
+
jumped_into_function,
|
|
310
|
+
last_submessage_data,
|
|
311
|
+
)?;
|
|
312
|
+
|
|
313
|
+
ErrorInferrer::filter_redundant_frames(stacktrace_with_inferred_error)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
use std::rc::Rc;
|
|
2
|
+
|
|
3
|
+
use edr_solidity::{
|
|
4
|
+
artifacts::BuildInfo,
|
|
5
|
+
build_model::{Bytecode, ContractFunctionType},
|
|
6
|
+
compiler::create_models_and_decode_bytecodes,
|
|
7
|
+
contracts_identifier::ContractsIdentifier,
|
|
8
|
+
};
|
|
9
|
+
use napi::{
|
|
10
|
+
bindgen_prelude::{ClassInstance, Either3, Either4, Uint8Array, Undefined},
|
|
11
|
+
Either, Env,
|
|
12
|
+
};
|
|
13
|
+
use napi_derive::napi;
|
|
14
|
+
use serde::{Deserialize, Serialize};
|
|
15
|
+
|
|
16
|
+
use super::{
|
|
17
|
+
message_trace::{CallMessageTrace, CreateMessageTrace, PrecompileMessageTrace},
|
|
18
|
+
solidity_stack_trace::{
|
|
19
|
+
FALLBACK_FUNCTION_NAME, RECEIVE_FUNCTION_NAME, UNRECOGNIZED_CONTRACT_NAME,
|
|
20
|
+
UNRECOGNIZED_FUNCTION_NAME,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
use crate::trace::model::BytecodeWrapper;
|
|
24
|
+
|
|
25
|
+
#[derive(Deserialize, Serialize)]
|
|
26
|
+
#[serde(rename_all = "camelCase")]
|
|
27
|
+
pub struct TracingConfig {
|
|
28
|
+
pub build_infos: Option<Vec<BuildInfo>>,
|
|
29
|
+
pub ignore_contracts: Option<bool>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Default)]
|
|
33
|
+
#[napi]
|
|
34
|
+
pub struct VmTraceDecoder {
|
|
35
|
+
contracts_identifier: ContractsIdentifier,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[napi]
|
|
39
|
+
impl VmTraceDecoder {
|
|
40
|
+
#[napi(constructor)]
|
|
41
|
+
pub fn new() -> Self {
|
|
42
|
+
Self::default()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[napi(catch_unwind)]
|
|
46
|
+
pub fn add_bytecode(&mut self, bytecode: ClassInstance<BytecodeWrapper>) {
|
|
47
|
+
self.add_bytecode_inner(bytecode.inner().clone());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn add_bytecode_inner(&mut self, bytecode: Rc<Bytecode>) {
|
|
51
|
+
self.contracts_identifier.add_bytecode(bytecode);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[napi(catch_unwind)]
|
|
55
|
+
pub fn try_to_decode_message_trace(
|
|
56
|
+
&mut self,
|
|
57
|
+
message_trace: Either3<PrecompileMessageTrace, CallMessageTrace, CreateMessageTrace>,
|
|
58
|
+
env: Env,
|
|
59
|
+
) -> napi::Result<Either3<PrecompileMessageTrace, CallMessageTrace, CreateMessageTrace>> {
|
|
60
|
+
match message_trace {
|
|
61
|
+
precompile @ Either3::A(..) => Ok(precompile),
|
|
62
|
+
// NOTE: The branches below are the same with the difference of `is_create`
|
|
63
|
+
Either3::B(mut call) => {
|
|
64
|
+
let is_create = false;
|
|
65
|
+
|
|
66
|
+
let bytecode = self
|
|
67
|
+
.contracts_identifier
|
|
68
|
+
.get_bytecode_for_call(call.code.as_ref(), is_create);
|
|
69
|
+
|
|
70
|
+
let steps: Vec<_> = call
|
|
71
|
+
.steps
|
|
72
|
+
.into_iter()
|
|
73
|
+
.map(|step| {
|
|
74
|
+
let trace = match step {
|
|
75
|
+
Either4::A(step) => return Ok(Either4::A(step)),
|
|
76
|
+
Either4::B(precompile) => Either3::A(precompile),
|
|
77
|
+
Either4::C(create) => Either3::B(create),
|
|
78
|
+
Either4::D(call) => Either3::C(call),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
Ok(match self.try_to_decode_message_trace(trace, env)? {
|
|
82
|
+
Either3::A(precompile) => Either4::B(precompile),
|
|
83
|
+
Either3::B(create) => Either4::C(create),
|
|
84
|
+
Either3::C(call) => Either4::D(call),
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
.collect::<napi::Result<_>>()?;
|
|
88
|
+
|
|
89
|
+
let bytecode = bytecode
|
|
90
|
+
.map(|b| BytecodeWrapper::new(b).into_instance(env))
|
|
91
|
+
.transpose()?;
|
|
92
|
+
|
|
93
|
+
call.bytecode = bytecode;
|
|
94
|
+
call.steps = steps;
|
|
95
|
+
|
|
96
|
+
Ok(Either3::B(call))
|
|
97
|
+
}
|
|
98
|
+
Either3::C(mut create @ CreateMessageTrace { .. }) => {
|
|
99
|
+
let is_create = true;
|
|
100
|
+
|
|
101
|
+
let bytecode = self
|
|
102
|
+
.contracts_identifier
|
|
103
|
+
.get_bytecode_for_call(create.code.as_ref(), is_create);
|
|
104
|
+
|
|
105
|
+
let steps: Vec<_> = create
|
|
106
|
+
.steps
|
|
107
|
+
.into_iter()
|
|
108
|
+
.map(|step| {
|
|
109
|
+
let trace = match step {
|
|
110
|
+
Either4::A(step) => return Ok(Either4::A(step)),
|
|
111
|
+
Either4::B(precompile) => Either3::A(precompile),
|
|
112
|
+
Either4::C(create) => Either3::B(create),
|
|
113
|
+
Either4::D(call) => Either3::C(call),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
Ok(match self.try_to_decode_message_trace(trace, env)? {
|
|
117
|
+
Either3::A(precompile) => Either4::B(precompile),
|
|
118
|
+
Either3::B(create) => Either4::C(create),
|
|
119
|
+
Either3::C(call) => Either4::D(call),
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
.collect::<napi::Result<_>>()?;
|
|
123
|
+
|
|
124
|
+
let bytecode = bytecode
|
|
125
|
+
.map(|b| BytecodeWrapper::new(b).into_instance(env))
|
|
126
|
+
.transpose()?;
|
|
127
|
+
create.bytecode = bytecode;
|
|
128
|
+
create.steps = steps;
|
|
129
|
+
|
|
130
|
+
Ok(Either3::C(create))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[napi]
|
|
136
|
+
pub fn get_contract_and_function_names_for_call(
|
|
137
|
+
&mut self,
|
|
138
|
+
code: Uint8Array,
|
|
139
|
+
calldata: Either<Uint8Array, Undefined>,
|
|
140
|
+
) -> napi::Result<ContractAndFunctionName> {
|
|
141
|
+
let is_create = matches!(calldata, Either::B(()));
|
|
142
|
+
let bytecode = self
|
|
143
|
+
.contracts_identifier
|
|
144
|
+
.get_bytecode_for_call(code.as_ref(), is_create);
|
|
145
|
+
|
|
146
|
+
let contract = bytecode.map(|bytecode| bytecode.contract.clone());
|
|
147
|
+
let contract = contract.as_ref().map(|c| c.borrow());
|
|
148
|
+
|
|
149
|
+
let contract_name = contract.as_ref().map_or_else(
|
|
150
|
+
|| UNRECOGNIZED_CONTRACT_NAME.to_string(),
|
|
151
|
+
|c| c.name.clone(),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if is_create {
|
|
155
|
+
Ok(ContractAndFunctionName {
|
|
156
|
+
contract_name,
|
|
157
|
+
function_name: Either::B(()),
|
|
158
|
+
})
|
|
159
|
+
} else {
|
|
160
|
+
match contract {
|
|
161
|
+
None => Ok(ContractAndFunctionName {
|
|
162
|
+
contract_name,
|
|
163
|
+
function_name: Either::A("".to_string()),
|
|
164
|
+
}),
|
|
165
|
+
Some(contract) => {
|
|
166
|
+
let calldata = match calldata {
|
|
167
|
+
Either::A(calldata) => calldata,
|
|
168
|
+
Either::B(_) => {
|
|
169
|
+
unreachable!("calldata should be Some if is_create is false")
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let selector = &calldata.get(..4).unwrap_or(&calldata[..]);
|
|
174
|
+
|
|
175
|
+
let func = contract.get_function_from_selector(selector);
|
|
176
|
+
|
|
177
|
+
let function_name = match func {
|
|
178
|
+
Some(func) => match func.r#type {
|
|
179
|
+
ContractFunctionType::Fallback => FALLBACK_FUNCTION_NAME.to_string(),
|
|
180
|
+
ContractFunctionType::Receive => RECEIVE_FUNCTION_NAME.to_string(),
|
|
181
|
+
_ => func.name.clone(),
|
|
182
|
+
},
|
|
183
|
+
None => UNRECOGNIZED_FUNCTION_NAME.to_string(),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
Ok(ContractAndFunctionName {
|
|
187
|
+
contract_name,
|
|
188
|
+
function_name: Either::A(function_name),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[napi(object)]
|
|
197
|
+
pub struct ContractAndFunctionName {
|
|
198
|
+
pub contract_name: String,
|
|
199
|
+
pub function_name: Either<String, Undefined>,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[napi(catch_unwind)]
|
|
203
|
+
pub fn initialize_vm_trace_decoder(
|
|
204
|
+
mut vm_trace_decoder: ClassInstance<VmTraceDecoder>,
|
|
205
|
+
tracing_config: serde_json::Value,
|
|
206
|
+
) -> napi::Result<()> {
|
|
207
|
+
let config = serde_json::from_value::<TracingConfig>(tracing_config).map_err(|e| {
|
|
208
|
+
napi::Error::from_reason(format!("Failed to deserialize tracing config: {e:?}"))
|
|
209
|
+
})?;
|
|
210
|
+
|
|
211
|
+
let Some(build_infos) = config.build_infos else {
|
|
212
|
+
return Ok(());
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
for build_info in &build_infos {
|
|
216
|
+
let bytecodes = create_models_and_decode_bytecodes(
|
|
217
|
+
build_info.solc_version.clone(),
|
|
218
|
+
&build_info.input,
|
|
219
|
+
&build_info.output,
|
|
220
|
+
)?;
|
|
221
|
+
|
|
222
|
+
for bytecode in bytecodes {
|
|
223
|
+
if config.ignore_contracts == Some(true)
|
|
224
|
+
&& bytecode.contract.borrow().name.starts_with("Ignored")
|
|
225
|
+
{
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
vm_trace_decoder.add_bytecode_inner(Rc::new(bytecode));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
Ok(())
|
|
234
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//! N-API bindings for the Rust port of `VMTracer` from Hardhat.
|
|
2
|
+
|
|
3
|
+
use napi::{
|
|
4
|
+
bindgen_prelude::{Either3, Either4, Undefined},
|
|
5
|
+
Either, Env, JsError,
|
|
6
|
+
};
|
|
7
|
+
use napi_derive::napi;
|
|
8
|
+
|
|
9
|
+
use crate::trace::{
|
|
10
|
+
message_trace::{
|
|
11
|
+
message_trace_to_napi, CallMessageTrace, CreateMessageTrace, PrecompileMessageTrace,
|
|
12
|
+
},
|
|
13
|
+
RawTrace,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/// N-API bindings for the Rust port of `VMTracer` from Hardhat.
|
|
17
|
+
#[napi]
|
|
18
|
+
pub struct VMTracer(edr_solidity::vm_tracer::VmTracer);
|
|
19
|
+
|
|
20
|
+
#[napi]
|
|
21
|
+
impl VMTracer {
|
|
22
|
+
#[napi(constructor)]
|
|
23
|
+
pub fn new() -> napi::Result<Self> {
|
|
24
|
+
Ok(Self(edr_solidity::vm_tracer::VmTracer::new()))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Observes a trace, collecting information about the execution of the EVM.
|
|
28
|
+
#[napi]
|
|
29
|
+
pub fn observe(&mut self, trace: &RawTrace) {
|
|
30
|
+
self.0.observe(&trace.inner);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Explicitly return undefined as `Option<T>` by default returns `null` in JS
|
|
34
|
+
// and the null/undefined checks we use in JS are strict
|
|
35
|
+
#[napi]
|
|
36
|
+
pub fn get_last_top_level_message_trace(
|
|
37
|
+
&self,
|
|
38
|
+
env: Env,
|
|
39
|
+
) -> napi::Result<
|
|
40
|
+
Either4<PrecompileMessageTrace, CallMessageTrace, CreateMessageTrace, Undefined>,
|
|
41
|
+
> {
|
|
42
|
+
Ok(
|
|
43
|
+
match self
|
|
44
|
+
.0
|
|
45
|
+
.get_last_top_level_message_trace_ref()
|
|
46
|
+
.map(|x| {
|
|
47
|
+
x.try_borrow()
|
|
48
|
+
.expect("cannot be executed concurrently with `VMTracer::observe`")
|
|
49
|
+
.clone()
|
|
50
|
+
})
|
|
51
|
+
.map(|msg| message_trace_to_napi(msg, env))
|
|
52
|
+
.transpose()?
|
|
53
|
+
{
|
|
54
|
+
Some(Either3::A(precompile)) => Either4::A(precompile),
|
|
55
|
+
Some(Either3::B(call)) => Either4::B(call),
|
|
56
|
+
Some(Either3::C(create)) => Either4::C(create),
|
|
57
|
+
None => Either4::D(()),
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Explicitly return undefined as `Option<T>` by default returns `null` in JS
|
|
63
|
+
// and the null/undefined checks we use in JS are strict
|
|
64
|
+
#[napi]
|
|
65
|
+
pub fn get_last_error(&self) -> Either<JsError, Undefined> {
|
|
66
|
+
match self.0.get_last_error() {
|
|
67
|
+
Some(err) => Either::A(napi::Error::from_reason(err).into()),
|
|
68
|
+
None => Either::B(()),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/trace.rs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// In contrast to the functions in the `#[napi] impl XYZ` block,
|
|
2
|
+
// the free functions `#[napi] pub fn` are exported by napi-rs but
|
|
3
|
+
// are considered dead code in the (lib test) target.
|
|
4
|
+
// For now, we silence the relevant warnings, as we need to mimick
|
|
5
|
+
// the original API while we rewrite the stack trace refinement to Rust.
|
|
6
|
+
#![cfg_attr(test, allow(dead_code))]
|
|
7
|
+
|
|
1
8
|
use std::sync::Arc;
|
|
2
9
|
|
|
3
10
|
use edr_evm::{interpreter::OpCode, trace::BeforeMessage};
|
|
@@ -9,6 +16,21 @@ use napi_derive::napi;
|
|
|
9
16
|
|
|
10
17
|
use crate::result::ExecutionResult;
|
|
11
18
|
|
|
19
|
+
mod compiler;
|
|
20
|
+
mod library_utils;
|
|
21
|
+
mod model;
|
|
22
|
+
|
|
23
|
+
mod debug;
|
|
24
|
+
mod error_inferrer;
|
|
25
|
+
mod exit;
|
|
26
|
+
mod mapped_inlined_internal_functions_heuristics;
|
|
27
|
+
mod message_trace;
|
|
28
|
+
mod return_data;
|
|
29
|
+
mod solidity_stack_trace;
|
|
30
|
+
mod solidity_tracer;
|
|
31
|
+
mod vm_trace_decoder;
|
|
32
|
+
mod vm_tracer;
|
|
33
|
+
|
|
12
34
|
#[napi(object)]
|
|
13
35
|
pub struct TracingMessage {
|
|
14
36
|
/// Sender address
|
|
@@ -151,7 +173,7 @@ pub struct TracingMessageResult {
|
|
|
151
173
|
|
|
152
174
|
#[napi]
|
|
153
175
|
pub struct RawTrace {
|
|
154
|
-
inner: Arc<edr_evm::trace::Trace>,
|
|
176
|
+
pub(crate) inner: Arc<edr_evm::trace::Trace>,
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
impl RawTrace {
|