Skip to main content

evm_state_diff/
trace.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::primitives::{Address, B256, Bytes, U256, de_quantity_u64};
7
8/// One account as reported by geth's `prestateTracer` in diff mode. In `pre`
9/// it is the state before the transaction (only accounts that changed); in
10/// `post` only the fields that changed are present.
11#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct AccountState {
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub balance: Option<U256>,
16    #[serde(default, deserialize_with = "de_quantity_u64", skip_serializing_if = "Option::is_none")]
17    pub nonce: Option<u64>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub code: Option<Bytes>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub code_hash: Option<B256>,
22    #[serde(default, deserialize_with = "de_storage", skip_serializing_if = "Option::is_none")]
23    pub storage: Option<BTreeMap<B256, B256>>,
24}
25
26fn de_storage<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<BTreeMap<B256, B256>>, D::Error> {
27    let raw = Option::<BTreeMap<String, String>>::deserialize(d)?;
28    raw.map(|m| {
29        m.into_iter()
30            .map(|(k, v)| Ok((B256::from_word(&k)?, B256::from_word(&v)?)))
31            .collect::<Result<_, crate::primitives::ParseError>>()
32            .map_err(serde::de::Error::custom)
33    })
34    .transpose()
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
38pub struct PrestateDiff {
39    #[serde(default)]
40    pub pre: BTreeMap<Address, AccountState>,
41    #[serde(default)]
42    pub post: BTreeMap<Address, AccountState>,
43}
44
45/// The diff of one transaction, with its hash when the source provides it.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct TxTrace {
48    pub tx_hash: Option<B256>,
49    pub diff: PrestateDiff,
50}
51
52#[derive(Debug, thiserror::Error)]
53pub enum TraceError {
54    #[error("invalid JSON: {0}")]
55    Json(#[from] serde_json::Error),
56    #[error("JSON-RPC error {code}: {message}")]
57    Rpc { code: i64, message: String },
58    #[error("transaction {tx} failed to trace: {message}")]
59    TxFailed { tx: String, message: String },
60    #[error(
61        "unrecognized trace format: expected a prestateTracer diffMode result ({{\"pre\", \"post\"}}) or a list of them"
62    )]
63    UnknownFormat,
64    #[error("trace is not in diff mode: rerun the tracer with tracerConfig.diffMode = true")]
65    NotDiffMode,
66}
67
68fn is_diff(v: &Value) -> bool {
69    v.as_object().is_some_and(|o| {
70        (o.contains_key("pre") || o.contains_key("post")) && o.keys().all(|k| k == "pre" || k == "post")
71    })
72}
73
74fn looks_like_plain_prestate(v: &Value) -> bool {
75    v.as_object().is_some_and(|o| !o.is_empty() && o.keys().all(|k| k.parse::<Address>().is_ok()))
76}
77
78fn parse_diff(v: Value) -> Result<PrestateDiff, TraceError> {
79    if is_diff(&v) {
80        Ok(serde_json::from_value(v)?)
81    } else if looks_like_plain_prestate(&v) {
82        Err(TraceError::NotDiffMode)
83    } else if v.as_object().is_some_and(|o| o.is_empty()) {
84        Ok(PrestateDiff::default())
85    } else {
86        Err(TraceError::UnknownFormat)
87    }
88}
89
90fn parse_block_item(item: Value) -> Result<TxTrace, TraceError> {
91    let tx_hash = item.get("txHash").and_then(Value::as_str).map(B256::from_word).transpose().ok().flatten();
92    if let Some(err) = item.get("error") {
93        return Err(TraceError::TxFailed {
94            tx: tx_hash.map_or_else(|| "?".into(), |h| h.to_string()),
95            message: err.as_str().map_or_else(|| err.to_string(), str::to_owned),
96        });
97    }
98    match item.get("result") {
99        Some(result) => Ok(TxTrace { tx_hash, diff: parse_diff(result.clone())? }),
100        None => Ok(TxTrace { tx_hash: None, diff: parse_diff(item)? }),
101    }
102}
103
104/// Parses any of the shapes produced by `debug_traceTransaction` and
105/// `debug_traceBlockBy*` with the `prestateTracer` in diff mode: a bare
106/// result, an array of per-transaction results, or a full JSON-RPC response.
107pub fn parse_traces(value: Value) -> Result<Vec<TxTrace>, TraceError> {
108    if let Some(obj) = value.as_object() {
109        if obj.contains_key("jsonrpc") || obj.contains_key("result") && obj.contains_key("id") {
110            if let Some(err) = obj.get("error") {
111                return Err(TraceError::Rpc {
112                    code: err.get("code").and_then(Value::as_i64).unwrap_or(0),
113                    message: err.get("message").and_then(Value::as_str).unwrap_or("").to_owned(),
114                });
115            }
116            return parse_traces(obj.get("result").cloned().unwrap_or(Value::Null));
117        }
118    }
119    match value {
120        Value::Array(items) => items.into_iter().map(parse_block_item).collect(),
121        other => Ok(vec![TxTrace { tx_hash: None, diff: parse_diff(other)? }]),
122    }
123}
124
125pub fn parse_traces_str(s: &str) -> Result<Vec<TxTrace>, TraceError> {
126    parse_traces(serde_json::from_str(s)?)
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    const TX: &str = r#"{
134        "post": {
135            "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": {"balance": "0x21e19e0c9bab2400000"},
136            "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": {"balance": "0x21e19df85a3ae4b4c00", "nonce": 1}
137        },
138        "pre": {
139            "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": {"balance": "0x21e19e0c9bab2400000"},
140            "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": {"balance": "0x21e19e0c9bab2400000"}
141        }
142    }"#;
143
144    #[test]
145    fn parses_bare_result() {
146        let traces = parse_traces_str(TX).unwrap();
147        assert_eq!(traces.len(), 1);
148        let diff = &traces[0].diff;
149        let sender: Address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".parse().unwrap();
150        assert_eq!(diff.post[&sender].nonce, Some(1));
151        assert_eq!(diff.pre[&sender].nonce, None);
152    }
153
154    #[test]
155    fn parses_rpc_envelope_and_block_arrays() {
156        let rpc = format!(r#"{{"jsonrpc":"2.0","id":1,"result":{TX}}}"#);
157        assert_eq!(parse_traces_str(&rpc).unwrap().len(), 1);
158
159        let hash = format!("0x{}", "ab".repeat(32));
160        let block = format!(r#"[{{"txHash":"{hash}","result":{TX}}},{{"txHash":"{hash}","result":{{}}}}]"#);
161        let traces = parse_traces_str(&block).unwrap();
162        assert_eq!(traces.len(), 2);
163        assert_eq!(traces[0].tx_hash.unwrap().to_string(), hash);
164        assert!(traces[1].diff.pre.is_empty());
165    }
166
167    #[test]
168    fn reports_errors() {
169        let err = parse_traces_str(
170            r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found"}}"#,
171        );
172        assert!(matches!(err, Err(TraceError::Rpc { code: -32601, .. })));
173        let failed = parse_traces_str(r#"[{"txHash":"0x01","error":"execution timeout"}]"#);
174        assert!(matches!(failed, Err(TraceError::TxFailed { .. })));
175        let plain = parse_traces_str(r#"{"0x70997970c51812dc3a010c7d01b50e0d17dc79c8":{"balance":"0x1"}}"#);
176        assert!(matches!(plain, Err(TraceError::NotDiffMode)));
177        assert!(matches!(parse_traces_str(r#"{"calls":[]}"#), Err(TraceError::UnknownFormat)));
178        assert!(matches!(parse_traces_str("not json"), Err(TraceError::Json(_))));
179    }
180
181    #[test]
182    fn accepts_quantity_variants() {
183        let s = r#"{"pre":{"0x70997970c51812dc3a010c7d01b50e0d17dc79c8":{"balance":"100","nonce":"0x5","storage":{"0x1":"0x2"}}}}"#;
184        let t = parse_traces_str(s).unwrap();
185        let acct = t[0].diff.pre.values().next().unwrap();
186        assert_eq!(acct.nonce, Some(5));
187        assert_eq!(acct.balance, Some(U256::from_u128(100)));
188        let slot = B256::from_word("0x1").unwrap();
189        assert_eq!(acct.storage.as_ref().unwrap()[&slot], B256::from_word("0x2").unwrap());
190    }
191}