use soldb_core::{SoldbError, SoldbResult, TransactionTrace};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootCall {
pub entry_step: usize,
pub record: RootRecord,
pub kind: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RootRecord {
Trace,
Call(usize),
Creation(usize),
}
impl RootCall {
#[must_use]
pub fn address<'a>(&self, trace: &'a TransactionTrace) -> Option<&'a str> {
match self.record {
RootRecord::Trace => trace
.to_addr
.as_deref()
.or(trace.contract_address.as_deref()),
RootRecord::Call(index) => trace
.artifacts
.calls
.get(index)
.map(|call| call.bytecode_address.as_str()),
RootRecord::Creation(index) => trace
.artifacts
.creations
.get(index)
.and_then(|creation| creation.address.as_deref()),
}
}
#[must_use]
pub fn creation(&self, trace: &TransactionTrace) -> bool {
match self.record {
RootRecord::Trace => trace.to_addr.is_none() && trace.contract_address.is_some(),
RootRecord::Call(_) => false,
RootRecord::Creation(_) => true,
}
}
#[must_use]
pub fn input<'a>(&self, trace: &'a TransactionTrace) -> &'a str {
match self.record {
RootRecord::Trace => &trace.input_data,
RootRecord::Call(index) => trace
.artifacts
.calls
.get(index)
.map_or("", |call| call.input.as_str()),
RootRecord::Creation(index) => trace
.artifacts
.creations
.get(index)
.map_or("", |creation| creation.init_code.as_str()),
}
}
}
#[must_use]
pub fn root_calls(trace: &TransactionTrace) -> Vec<RootCall> {
let mut roots = Vec::new();
for (index, call) in trace.artifacts.calls.iter().enumerate() {
if call.parent_id.is_some() {
continue;
}
let Some(entry_step) = root_entry(trace, call.entry_step, call.exit_step) else {
continue;
};
roots.push(RootCall {
entry_step,
record: RootRecord::Call(index),
kind: None,
});
}
for (index, creation) in trace.artifacts.creations.iter().enumerate() {
if creation.parent_id.is_some() {
continue;
}
let Some(entry_step) = root_entry(trace, creation.entry_step, creation.exit_step) else {
continue;
};
roots.push(RootCall {
entry_step,
record: RootRecord::Creation(index),
kind: None,
});
}
roots.sort_by_key(|root| root.entry_step);
if roots.first().is_none_or(|root| root.entry_step != 0) {
roots.insert(
0,
RootCall {
entry_step: 0,
record: RootRecord::Trace,
kind: None,
},
);
}
for root in &mut roots {
let kind = trace
.artifacts
.root_kinds
.iter()
.find(|kind| kind.entry_step == root.entry_step);
root.kind = kind.map(|kind| kind.kind.clone());
}
roots
}
fn root_entry(
trace: &TransactionTrace,
entry: Option<usize>,
exit: Option<usize>,
) -> Option<usize> {
let entry = entry?;
if exit.is_some_and(|exit| exit <= entry) {
return None;
}
let root_depth = trace.steps.first().map_or(0, |step| step.depth);
let step = trace.steps.get(entry)?;
(step.depth == root_depth).then_some(entry)
}
pub fn check_root_calls(trace: &TransactionTrace) -> SoldbResult<()> {
let roots = root_calls(trace);
let Some(pair) = roots
.windows(2)
.find(|pair| pair[0].entry_step == pair[1].entry_step)
else {
return Ok(());
};
Err(SoldbError::Message(format!(
"invalid trace: two top-level calls start at step {}\n\
the trace records more than one call or creation without a parent entered there",
pair[0].entry_step
)))
}
#[must_use]
pub fn root_index(roots: &[RootCall], step: usize) -> usize {
let position = roots.partition_point(|root| root.entry_step <= step);
position.saturating_sub(1)
}
#[cfg(test)]
mod tests {
use soldb_core::{
ContractCreation, ExecutionCall, RootKind, StepSnapshot, TraceStep, TransactionTrace,
};
use super::{check_root_calls, root_calls, root_index, RootCall, RootRecord};
fn step(depth: u64) -> TraceStep {
TraceStep::new(0, "STOP".into(), 0, 0, depth, None, StepSnapshot::default())
}
fn trace(depths: &[u64]) -> TransactionTrace {
TransactionTrace {
tx_hash: None,
from_addr: "0x1".to_owned(),
to_addr: Some("0xaa".to_owned()),
value: "0x0".to_owned(),
input_data: "0x01".to_owned(),
gas_used: 0,
output: "0x".to_owned(),
success: true,
error: None,
debug_trace_available: true,
contract_address: None,
backend: None,
capabilities: Default::default(),
artifacts: Default::default(),
steps: depths.iter().map(|depth| step(*depth)).collect(),
}
}
fn call(parent_id: Option<usize>, entry: usize, exit: usize, address: &str) -> ExecutionCall {
ExecutionCall {
id: 0,
parent_id,
depth: 1,
entry_step: Some(entry),
exit_step: Some(exit),
call_type: "CALL".to_owned(),
from: "0x1".to_owned(),
to: address.to_owned(),
bytecode_address: address.to_owned(),
value: "0x0".to_owned(),
input: format!("0x{entry:02x}"),
gas_limit: 0,
gas_used: None,
output: None,
success: Some(true),
error: None,
}
}
#[test]
fn without_records_the_trace_fields_are_the_root() {
let trace = trace(&[1, 1]);
let roots = root_calls(&trace);
assert_eq!(
roots,
vec![RootCall {
entry_step: 0,
record: RootRecord::Trace,
kind: None,
}]
);
assert_eq!(roots[0].address(&trace), Some("0xaa"));
assert!(!roots[0].creation(&trace));
assert_eq!(roots[0].input(&trace), "0x01");
}
#[test]
fn top_level_records_are_roots_in_execution_order() {
let mut trace = trace(&[1, 1, 2, 1, 1]);
trace.artifacts.calls = vec![
call(None, 3, 5, "0xbb"),
call(None, 0, 3, "0xaa"),
call(Some(1), 2, 3, "0xcc"),
call(None, 2, 3, "0xdd"),
call(None, 5, 5, "0xee"),
];
let roots = root_calls(&trace);
let entries = roots.iter().map(|root| root.entry_step).collect::<Vec<_>>();
assert_eq!(entries, vec![0, 3]);
assert_eq!(roots[root_index(&roots, 2)].address(&trace), Some("0xaa"));
assert_eq!(roots[root_index(&roots, 4)].input(&trace), "0x03");
}
#[test]
fn two_roots_starting_at_one_step_are_invalid() {
let mut trace = trace(&[1, 1, 1, 1]);
trace.artifacts.calls = vec![
call(None, 0, 1, "0xaa"),
call(None, 1, 2, "0xbb"),
call(None, 2, 4, "0xdd"),
];
assert!(check_root_calls(&trace).is_ok());
trace.artifacts.calls.push(call(None, 1, 2, "0xcc"));
let error = check_root_calls(&trace).expect_err("two roots at step 1");
assert!(error
.to_string()
.starts_with("invalid trace: two top-level calls start at step 1\n"));
}
#[test]
fn a_later_root_creation_follows_the_trace_root() {
let mut trace = trace(&[1, 1]);
trace.artifacts.creations.push(ContractCreation {
id: 0,
parent_id: None,
depth: 1,
entry_step: Some(1),
exit_step: None,
create_type: "CREATE".to_owned(),
caller: "0x1".to_owned(),
address: Some("0xcc".to_owned()),
value: "0x0".to_owned(),
init_code: "0x6000".to_owned(),
gas_limit: 0,
gas_used: None,
output: None,
success: Some(true),
error: None,
});
let roots = root_calls(&trace);
assert_eq!(roots.len(), 2);
assert_eq!(roots[0].input(&trace), "0x01");
assert!(roots[1].creation(&trace));
assert_eq!(roots[1].address(&trace), Some("0xcc"));
assert_eq!(roots[1].input(&trace), "0x6000");
}
#[test]
fn roots_carry_the_kind_recorded_at_their_entry_step() {
let mut trace = trace(&[1, 1, 1]);
trace.artifacts.calls = vec![call(None, 0, 2, "0xaa"), call(None, 2, 3, "0xbb")];
trace.artifacts.root_kinds = vec![
RootKind {
entry_step: 2,
kind: "execution".to_owned(),
},
RootKind {
entry_step: 0,
kind: "setup".to_owned(),
},
RootKind {
entry_step: 1,
kind: "deployment".to_owned(),
},
];
let kinds = root_calls(&trace)
.into_iter()
.map(|root| root.kind)
.collect::<Vec<_>>();
assert_eq!(
kinds,
vec![Some("setup".to_owned()), Some("execution".to_owned())]
);
}
}