1use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use soldb_core::{SoldbError, SoldbResult};
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct SourceLocation {
20 pub source_id: u64,
21 pub offset: u64,
22 pub length: u64,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub struct FunctionIdentity {
27 pub identifier: Option<String>,
28 pub declaration: Option<SourceLocation>,
29 pub activation: Option<String>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum FunctionExit {
35 Return,
36 Revert,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct Instruction {
41 pub offset: u64,
42 pub operation: Value,
43 #[serde(default)]
44 pub context: Option<Value>,
45}
46
47impl Instruction {
48 #[must_use]
49 pub fn mnemonic(&self) -> Option<&str> {
50 self.operation
51 .get("mnemonic")
52 .and_then(serde_json::Value::as_str)
53 }
54
55 #[must_use]
56 pub fn arguments(&self) -> Vec<&str> {
57 self.operation
58 .get("arguments")
59 .and_then(serde_json::Value::as_array)
60 .map(|arguments| {
61 arguments
62 .iter()
63 .filter_map(serde_json::Value::as_str)
64 .collect()
65 })
66 .unwrap_or_default()
67 }
68
69 #[must_use]
70 pub fn source_location(&self) -> Option<SourceLocation> {
71 let mut locations = self.source_locations();
72 (locations.len() == 1).then(|| locations.remove(0))
73 }
74
75 #[must_use]
77 pub fn source_locations(&self) -> Vec<SourceLocation> {
78 let mut locations = Vec::new();
79 if let Some(context) = &self.context {
80 collect_source_locations(context, &mut locations);
81 }
82 locations
83 }
84
85 #[must_use]
87 pub fn function_invocations(&self) -> Vec<FunctionIdentity> {
88 let mut functions = Vec::new();
89 if let Some(context) = &self.context {
90 collect_function_invocations(context, &mut functions);
91 }
92 functions
93 }
94
95 #[must_use]
97 pub fn function_exit(&self) -> Option<FunctionExit> {
98 let context = self.context.as_ref()?;
99 if context.get("return").is_some() {
100 Some(FunctionExit::Return)
101 } else if context.get("revert").is_some() {
102 Some(FunctionExit::Revert)
103 } else {
104 None
105 }
106 }
107
108 #[must_use]
110 pub fn modifier_depth(&self) -> Option<i64> {
111 self.context.as_ref()?.get("modifierDepth")?.as_i64()
112 }
113}
114
115fn collect_source_locations(context: &Value, locations: &mut Vec<SourceLocation>) {
116 if let Some(location) = context.get("code").and_then(parse_source_location) {
117 if !locations.contains(&location) {
118 locations.push(location);
119 }
120 }
121 if let Some(pick) = context.get("pick").and_then(Value::as_array) {
122 for alternative in pick {
123 collect_source_locations(alternative, locations);
124 }
125 }
126}
127
128fn collect_function_invocations(context: &Value, functions: &mut Vec<FunctionIdentity>) {
129 if let Some(invoke) = context.get("invoke") {
130 let function = FunctionIdentity {
131 identifier: invoke
132 .get("identifier")
133 .and_then(Value::as_str)
134 .map(str::to_owned),
135 declaration: invoke.get("declaration").and_then(parse_source_location),
136 activation: invoke
137 .get("activation")
138 .and_then(Value::as_str)
139 .map(str::to_owned),
140 };
141 if !functions.contains(&function) {
142 functions.push(function);
143 }
144 }
145 if let Some(pick) = context.get("pick").and_then(Value::as_array) {
146 for alternative in pick {
147 collect_function_invocations(alternative, functions);
148 }
149 }
150}
151
152fn parse_source_location(value: &Value) -> Option<SourceLocation> {
153 let source_id = value.get("source")?.get("id")?.as_u64()?;
154 let range = value.get("range")?;
155 Some(SourceLocation {
156 source_id,
157 offset: range.get("offset")?.as_u64()?,
158 length: range.get("length")?.as_u64()?,
159 })
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct VariableLocation {
164 pub name: String,
165 pub ty: String,
166 pub location_type: String,
167 pub offset: u64,
168 pub pc_range: (u64, u64),
169}
170
171impl VariableLocation {
172 #[must_use]
173 pub fn is_active_at_pc(&self, pc: u64) -> bool {
174 self.pc_range.0 <= pc && pc <= self.pc_range.1
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179pub struct EthdebugInfo {
180 pub compilation: Value,
181 pub contract_name: String,
182 pub environment: String,
183 pub instructions: Vec<Instruction>,
184 pub sources: BTreeMap<u64, String>,
185 #[serde(default)]
186 pub variable_locations: BTreeMap<u64, Vec<VariableLocation>>,
187}
188
189impl EthdebugInfo {
190 pub fn from_artifacts(
202 contract_name: &str,
203 environment: &str,
204 metadata: &Value,
205 program: &Value,
206 ) -> SoldbResult<Self> {
207 let instructions = program
208 .get("instructions")
209 .cloned()
210 .map(serde_json::from_value::<Vec<Instruction>>)
211 .transpose()
212 .map_err(|error| {
213 SoldbError::Message(format!(
214 "invalid `instructions` in ETHDebug program artifact: {error}"
215 ))
216 })?
217 .unwrap_or_default();
218 let compilation = metadata
219 .get("compilation")
220 .cloned()
221 .unwrap_or_else(|| metadata.clone());
222 let sources = parse_compilation_sources(&compilation);
223 let variable_locations = parse_variable_locations(program)?;
224 Ok(Self {
225 compilation,
226 contract_name: contract_name.to_owned(),
227 environment: environment.to_owned(),
228 instructions,
229 sources,
230 variable_locations,
231 })
232 }
233
234 #[must_use]
235 pub fn instruction_at_pc(&self, pc: u64) -> Option<&Instruction> {
236 self.instructions
237 .iter()
238 .find(|instruction| instruction.offset == pc)
239 }
240
241 #[must_use]
242 pub fn source_info(&self, pc: u64) -> Option<(&str, u64, u64)> {
243 let instruction = self.instruction_at_pc(pc)?;
244 let source_location = instruction.source_location()?;
245 let source_path = self.sources.get(&source_location.source_id)?;
246 Some((source_path, source_location.offset, source_location.length))
247 }
248
249 #[must_use]
255 pub fn has_variable_locations(&self) -> bool {
256 !self.variable_locations.is_empty()
257 }
258
259 pub fn variables_at_pc(&self, pc: u64) -> Vec<&VariableLocation> {
260 if let Some(exact) = self.variable_locations.get(&pc) {
261 return exact.iter().collect();
262 }
263
264 self.variable_locations
265 .values()
266 .flat_map(|variables| variables.iter())
267 .filter(|variable| variable.is_active_at_pc(pc))
268 .collect()
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct EthdebugSpec {
274 pub address: Option<String>,
275 pub name: Option<String>,
276 pub path: String,
277}
278
279pub fn parse_ethdebug_spec(input: &str) -> EthdebugSpec {
280 if let Ok(spec) = parse_single_contract_spec(input) {
281 return spec;
282 }
283
284 parse_multi_contract_spec(input).unwrap_or_else(|_| EthdebugSpec {
285 address: None,
286 name: None,
287 path: input.to_owned(),
288 })
289}
290
291pub fn parse_single_contract_spec(input: &str) -> SoldbResult<EthdebugSpec> {
292 if !input.starts_with("0x") {
293 return Err(SoldbError::Message(format!(
294 "Must use format 'address:name:path' (got: {input})"
295 )));
296 }
297
298 let parts = input.splitn(3, ':').collect::<Vec<_>>();
299 if parts.len() != 3 || parts[1].is_empty() || parts[2].is_empty() {
300 return Err(SoldbError::Message(format!(
301 "Must use format 'address:name:path' (got: {input})"
302 )));
303 }
304
305 Ok(EthdebugSpec {
306 address: Some(parts[0].to_owned()),
307 name: Some(parts[1].to_owned()),
308 path: parts[2].to_owned(),
309 })
310}
311
312pub fn parse_multi_contract_spec(input: &str) -> SoldbResult<EthdebugSpec> {
313 if input.is_empty() {
314 return Err(SoldbError::Message("Path cannot be empty".to_owned()));
315 }
316
317 if input.starts_with("0x") && input.contains(':') {
318 let parts = input.splitn(2, ':').collect::<Vec<_>>();
319 if parts.len() != 2 || parts[1].is_empty() {
320 return Err(SoldbError::Message(format!(
321 "Must use format 'address:path' (got: {input})"
322 )));
323 }
324
325 return Ok(EthdebugSpec {
326 address: Some(parts[0].to_owned()),
327 name: None,
328 path: parts[1].to_owned(),
329 });
330 }
331
332 Ok(EthdebugSpec {
333 address: None,
334 name: None,
335 path: input.to_owned(),
336 })
337}
338
339#[must_use]
344pub fn parse_compilation_sources(compilation: &Value) -> BTreeMap<u64, String> {
345 compilation
346 .get("sources")
347 .and_then(Value::as_array)
348 .into_iter()
349 .flatten()
350 .filter_map(|source| {
351 Some((
352 source.get("id")?.as_u64()?,
353 source.get("path")?.as_str()?.to_owned(),
354 ))
355 })
356 .collect()
357}
358
359#[must_use]
365pub fn read_compilation_source(compilation: &Value, source_id: u64) -> Option<String> {
366 compilation
367 .get("sources")
368 .and_then(Value::as_array)?
369 .iter()
370 .find(|source| source.get("id").and_then(Value::as_u64) == Some(source_id))
371 .and_then(|source| {
372 source
373 .get("contents")
374 .or_else(|| source.get("content"))
375 .and_then(Value::as_str)
376 })
377 .map(str::to_owned)
378}
379
380pub fn parse_variable_locations(
381 contract_data: &Value,
382) -> SoldbResult<BTreeMap<u64, Vec<VariableLocation>>> {
383 let mut variable_locations = BTreeMap::<u64, Vec<VariableLocation>>::new();
384
385 if let Some(instructions) = contract_data
386 .get("instructions")
387 .and_then(serde_json::Value::as_array)
388 {
389 for instruction in instructions {
390 let pc = instruction
391 .get("offset")
392 .and_then(serde_json::Value::as_u64)
393 .ok_or_else(|| SoldbError::Message("Instruction missing offset".to_owned()))?;
394
395 let Some(variables) = instruction
396 .get("context")
397 .and_then(|context| context.get("variables"))
398 .and_then(serde_json::Value::as_array)
399 else {
400 continue;
401 };
402
403 for variable in variables {
404 variable_locations
405 .entry(pc)
406 .or_default()
407 .push(parse_context_variable(variable, pc));
408 }
409 }
410 }
411
412 if let Some(variables) = contract_data
413 .get("variables")
414 .and_then(serde_json::Value::as_array)
415 {
416 for variable in variables {
417 let variable_location = parse_top_level_variable(variable);
418 for pc in variable_location.pc_range.0..=variable_location.pc_range.1 {
419 variable_locations
420 .entry(pc)
421 .or_default()
422 .push(variable_location.clone());
423 }
424 }
425 }
426
427 Ok(variable_locations)
428}
429
430fn parse_context_variable(variable: &Value, pc: u64) -> VariableLocation {
431 let location = variable.get("location");
432 let scope = variable.get("scope");
433
434 VariableLocation {
435 name: get_string(variable, "name", "unknown"),
436 ty: get_string(variable, "type", "unknown"),
437 location_type: location
438 .and_then(|value| value.get("type"))
439 .and_then(serde_json::Value::as_str)
440 .unwrap_or("stack")
441 .to_owned(),
442 offset: location
443 .and_then(|value| value.get("offset"))
444 .and_then(serde_json::Value::as_u64)
445 .unwrap_or(0),
446 pc_range: (
447 scope
448 .and_then(|value| value.get("start"))
449 .and_then(serde_json::Value::as_u64)
450 .unwrap_or(pc),
451 scope
452 .and_then(|value| value.get("end"))
453 .and_then(serde_json::Value::as_u64)
454 .unwrap_or(pc),
455 ),
456 }
457}
458
459fn parse_top_level_variable(variable: &Value) -> VariableLocation {
460 VariableLocation {
461 name: get_string(variable, "name", "unknown"),
462 ty: get_string(variable, "type", "unknown"),
463 location_type: get_string(variable, "location_type", "stack"),
464 offset: variable
465 .get("offset")
466 .and_then(serde_json::Value::as_u64)
467 .unwrap_or(0),
468 pc_range: (
469 variable
470 .get("pc_start")
471 .and_then(serde_json::Value::as_u64)
472 .unwrap_or(0),
473 variable
474 .get("pc_end")
475 .and_then(serde_json::Value::as_u64)
476 .unwrap_or(0),
477 ),
478 }
479}
480
481fn get_string(value: &Value, key: &str, default: &str) -> String {
482 value
483 .get(key)
484 .and_then(serde_json::Value::as_str)
485 .unwrap_or(default)
486 .to_owned()
487}
488
489#[cfg(test)]
490mod tests {
491 use std::collections::BTreeMap;
492
493 use super::{
494 parse_compilation_sources, parse_ethdebug_spec, parse_multi_contract_spec,
495 parse_single_contract_spec, parse_variable_locations, read_compilation_source,
496 EthdebugInfo, Instruction,
497 };
498 use serde_json::json;
499
500 #[test]
501 fn reads_sources_embedded_in_the_compilation() {
502 let compilation = json!({
503 "sources": [
504 {"id": 0, "path": "A.sol", "contents": "contract A {}"},
505 {"id": 1, "path": "B.sol", "content": "contract B {}"},
506 {"id": 2, "path": "C.sol"}
507 ]
508 });
509
510 assert_eq!(
511 read_compilation_source(&compilation, 0).as_deref(),
512 Some("contract A {}")
513 );
514 assert_eq!(
515 read_compilation_source(&compilation, 1).as_deref(),
516 Some("contract B {}")
517 );
518 assert_eq!(read_compilation_source(&compilation, 2), None);
519 assert_eq!(read_compilation_source(&compilation, 9), None);
520 assert_eq!(read_compilation_source(&json!({}), 0), None);
521 }
522
523 #[test]
524 fn builds_info_from_parsed_artifacts() {
525 let metadata = json!({
526 "compilation": {
527 "sources": [
528 {"id": 0, "path": "Counter.sol"},
529 {"id": 1, "path": "lib/Math.sol"},
530 {"path": "missing-id.sol"},
531 {"id": 2}
532 ]
533 },
534 "types": {},
535 "pointers": {}
536 });
537 let program = json!({
538 "instructions": [
539 {
540 "offset": 0,
541 "operation": {"mnemonic": "PUSH1", "arguments": ["0x80"]},
542 "context": {
543 "code": {"source": {"id": 0}, "range": {"offset": 10, "length": 4}},
544 "variables": [
545 {"identifier": "x", "pointer": {"location": "stack", "slot": 0}}
546 ]
547 }
548 },
549 {"offset": 2, "operation": {"mnemonic": "STOP"}}
550 ]
551 });
552
553 let info = EthdebugInfo::from_artifacts("Counter", "runtime", &metadata, &program)
554 .expect("artifacts");
555
556 assert_eq!(info.contract_name, "Counter");
557 assert_eq!(info.environment, "runtime");
558 assert_eq!(info.compilation, metadata["compilation"]);
559 assert_eq!(
560 info.sources,
561 BTreeMap::from([
562 (0, "Counter.sol".to_owned()),
563 (1, "lib/Math.sol".to_owned())
564 ])
565 );
566 assert_eq!(info.instructions.len(), 2);
567 assert_eq!(info.source_info(0), Some(("Counter.sol", 10, 4)));
568 assert_eq!(info.variables_at_pc(0).len(), 1);
569 assert!(info.variables_at_pc(2).is_empty());
570 }
571
572 #[test]
573 fn treats_legacy_metadata_without_compilation_wrapper_as_the_compilation() {
574 let metadata = json!({"sources": [{"id": 3, "path": "Legacy.sol"}]});
575 let program = json!({});
576
577 let info = EthdebugInfo::from_artifacts("Legacy", "create", &metadata, &program)
578 .expect("artifacts");
579
580 assert_eq!(info.compilation, metadata);
581 assert_eq!(info.sources, BTreeMap::from([(3, "Legacy.sol".to_owned())]));
582 assert!(info.instructions.is_empty());
583 assert!(info.variable_locations.is_empty());
584 }
585
586 #[test]
587 fn rejects_malformed_instructions() {
588 let metadata = json!({"compilation": {"sources": []}});
589 let program = json!({"instructions": [{"operation": {"mnemonic": "STOP"}}]});
590
591 let error = EthdebugInfo::from_artifacts("Broken", "runtime", &metadata, &program)
592 .expect_err("missing offset must fail");
593
594 assert!(
595 error.to_string().contains("`instructions`"),
596 "unexpected error: {error}"
597 );
598 }
599
600 #[test]
601 fn parses_compilation_sources_skipping_incomplete_entries() {
602 let compilation = json!({
603 "sources": [
604 {"id": 0, "path": "A.sol"},
605 {"id": "1", "path": "B.sol"},
606 {"id": 2, "path": 7},
607 {"id": 3, "path": "D.sol"}
608 ]
609 });
610
611 assert_eq!(
612 parse_compilation_sources(&compilation),
613 BTreeMap::from([(0, "A.sol".to_owned()), (3, "D.sol".to_owned())])
614 );
615 assert!(parse_compilation_sources(&json!({})).is_empty());
616 }
617
618 #[test]
619 fn parses_single_and_multi_contract_specs() {
620 let spec = parse_single_contract_spec("0xabc:Token:out").expect("single spec");
621 assert_eq!(spec.address.as_deref(), Some("0xabc"));
622 assert_eq!(spec.name.as_deref(), Some("Token"));
623 assert_eq!(spec.path, "out");
624
625 let multi = parse_multi_contract_spec("0xabc:out").expect("multi spec");
626 assert_eq!(multi.address.as_deref(), Some("0xabc"));
627 assert_eq!(multi.name, None);
628 assert_eq!(multi.path, "out");
629
630 let plain = parse_ethdebug_spec("out");
631 assert_eq!(plain.address, None);
632 assert_eq!(plain.path, "out");
633
634 assert!(parse_single_contract_spec("out").is_err());
635 assert!(parse_multi_contract_spec("").is_err());
636 }
637
638 #[test]
639 fn extracts_instruction_source_locations() {
640 let instruction: Instruction = serde_json::from_value(json!({
641 "offset": 12,
642 "operation": {"mnemonic": "PUSH1", "arguments": ["0x2a"]},
643 "context": {
644 "code": {
645 "source": {"id": 7},
646 "range": {"offset": 20, "length": 4}
647 }
648 }
649 }))
650 .expect("instruction");
651
652 let source_location = instruction.source_location().expect("source location");
653 assert_eq!(instruction.mnemonic(), Some("PUSH1"));
654 assert_eq!(instruction.arguments(), ["0x2a"]);
655 assert_eq!(source_location.source_id, 7);
656 assert_eq!(source_location.offset, 20);
657 assert_eq!(source_location.length, 4);
658 }
659
660 #[test]
661 fn extracts_source_picks_and_function_events() {
662 let invoke: Instruction = serde_json::from_value(json!({
663 "offset": 12,
664 "operation": {"mnemonic": "JUMPDEST"},
665 "context": {
666 "invoke": {
667 "identifier": "calculate",
668 "declaration": {
669 "source": {"id": 7},
670 "range": {"offset": 100, "length": 80}
671 },
672 "jump": true
673 }
674 }
675 }))
676 .expect("invoke instruction");
677 let shared: Instruction = serde_json::from_value(json!({
678 "offset": 13,
679 "operation": {"mnemonic": "SSTORE"},
680 "context": {
681 "pick": [
682 {"code": {
683 "source": {"id": 7},
684 "range": {"offset": 20, "length": 4}
685 }},
686 {"code": {
687 "source": {"id": 7},
688 "range": {"offset": 140, "length": 4}
689 }}
690 ]
691 }
692 }))
693 .expect("shared instruction");
694 let returned: Instruction = serde_json::from_value(json!({
695 "offset": 14,
696 "operation": {"mnemonic": "STOP"},
697 "context": {"return": {}}
698 }))
699 .expect("return instruction");
700
701 let function = invoke.function_invocations().remove(0);
702 assert_eq!(function.identifier.as_deref(), Some("calculate"));
703 assert_eq!(function.declaration.map(|range| range.offset), Some(100));
704 assert_eq!(shared.source_location(), None);
705 assert_eq!(
706 shared
707 .source_locations()
708 .into_iter()
709 .map(|location| location.offset)
710 .collect::<Vec<_>>(),
711 [20, 140]
712 );
713 assert_eq!(returned.function_exit(), Some(super::FunctionExit::Return));
714 }
715
716 #[test]
717 fn reports_source_and_variable_info_at_pc() {
718 let instruction: Instruction = serde_json::from_value(json!({
719 "offset": 3,
720 "operation": {"mnemonic": "SLOAD"},
721 "context": {
722 "code": {
723 "source": {"id": 0},
724 "range": {"offset": 9, "length": 5}
725 }
726 }
727 }))
728 .expect("instruction");
729
730 let variable = super::VariableLocation {
731 name: "stored".to_owned(),
732 ty: "uint256".to_owned(),
733 location_type: "storage".to_owned(),
734 offset: 0,
735 pc_range: (2, 5),
736 };
737 let mut sources = BTreeMap::new();
738 sources.insert(0, "Counter.sol".to_owned());
739 let mut variable_locations = BTreeMap::new();
740 variable_locations.insert(2, vec![variable]);
741 let info = EthdebugInfo {
742 compilation: json!({}),
743 contract_name: "Counter".to_owned(),
744 environment: "runtime".to_owned(),
745 instructions: vec![instruction],
746 sources,
747 variable_locations,
748 };
749
750 assert_eq!(info.source_info(3), Some(("Counter.sol", 9, 5)));
751 assert_eq!(info.variables_at_pc(3)[0].name, "stored");
752 assert!(info.variables_at_pc(9).is_empty());
753 }
754
755 #[test]
756 fn parses_variable_locations_from_contract_data() {
757 let variables = parse_variable_locations(&json!({
758 "instructions": [
759 {
760 "offset": 10,
761 "context": {
762 "variables": [
763 {
764 "name": "amount",
765 "type": "uint256",
766 "location": {"type": "stack", "offset": 1},
767 "scope": {"start": 10, "end": 12}
768 }
769 ]
770 }
771 }
772 ],
773 "variables": [
774 {
775 "name": "stored",
776 "type": "uint256",
777 "location_type": "storage",
778 "offset": 0,
779 "pc_start": 20,
780 "pc_end": 21
781 }
782 ]
783 }))
784 .expect("variables");
785
786 assert_eq!(variables.get(&10).expect("pc 10")[0].name, "amount");
787 assert_eq!(
788 variables.get(&20).expect("pc 20")[0].location_type,
789 "storage"
790 );
791 assert_eq!(variables.get(&21).expect("pc 21")[0].name, "stored");
792 }
793}