geth_tracer/
fourbyte.rs

1//! Fourbyte tracing inspector
2//!
3//! Solidity contract functions are addressed using the first four byte of the
4//! Keccak-256 hash of their signature. Therefore when calling the function of a
5//! contract, the caller must send this function selector as well as the
6//! ABI-encoded arguments as call data.
7//!
8//! The 4byteTracer collects the function selectors of every function executed
9//! in the lifetime of a transaction, along with the size of the supplied call
10//! data. The result is a map of SELECTOR-CALLDATASIZE to number of occurrences
11//! entries, where the keys are SELECTOR-CALLDATASIZE and the values are number
12//! of occurrences of this key. For example:
13//!
14//! ```json
15//! {
16//!   "0x27dc297e-128": 1,
17//!   "0x38cc4831-0": 2,
18//!   "0x524f3889-96": 1,
19//!   "0xadf59f99-288": 1,
20//!   "0xc281d19e-0": 1
21//! }
22//! ```
23
24use alloy_primitives::{hex, Selector};
25use alloy_rpc_types_trace::geth::{FourByteFrame, GethTrace};
26use cfx_vm_types::ActionParams;
27use std::collections::HashMap;
28
29/// Fourbyte tracing inspector that records all function selectors and their
30/// calldata sizes.
31#[derive(Clone, Debug, Default)]
32pub struct FourByteInspector {
33    /// The map of SELECTOR to number of occurrences entries
34    inner: HashMap<(Selector, usize), u64>,
35}
36
37impl FourByteInspector {
38    pub fn new() -> Self { Self::default() }
39
40    /// Returns the map of SELECTOR to number of occurrences entries
41    pub const fn inner(&self) -> &HashMap<(Selector, usize), u64> {
42        &self.inner
43    }
44
45    pub fn drain(self) -> GethTrace {
46        GethTrace::FourByteTracer(FourByteFrame::from(self))
47    }
48
49    pub fn record_call(&mut self, params: &ActionParams) {
50        if let Some(input) = &params.data {
51            if input.len() >= 4 {
52                let selector = Selector::try_from(&input[..4])
53                    .expect("input is at least 4 bytes");
54                let calldata_size = input[4..].len();
55                *self.inner.entry((selector, calldata_size)).or_default() += 1;
56            }
57        }
58    }
59}
60
61impl From<FourByteInspector> for FourByteFrame {
62    fn from(value: FourByteInspector) -> Self {
63        Self(
64            value
65                .inner
66                .into_iter()
67                .map(|((selector, calldata_size), count)| {
68                    let key = format!(
69                        "0x{}-{}",
70                        hex::encode(&selector[..]),
71                        calldata_size
72                    );
73                    (key, count)
74                })
75                .collect(),
76        )
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use cfx_vm_types::ActionParams;
84
85    fn record(data: Vec<u8>) -> FourByteInspector {
86        let mut inspector = FourByteInspector::new();
87        let params = ActionParams {
88            data: Some(data),
89            ..Default::default()
90        };
91        inspector.record_call(&params);
92        inspector
93    }
94
95    #[test]
96    fn record_call_selector_boundary() {
97        let selector = Selector::from([0x12, 0x34, 0x56, 0x78]);
98
99        // A bare selector (exactly 4 bytes, no arguments) must count as a
100        // `<selector>-0` entry, matching geth/reth. This is the case the old
101        // `> 4` guard dropped.
102        assert_eq!(
103            record(vec![0x12, 0x34, 0x56, 0x78])
104                .inner()
105                .get(&(selector, 0)),
106            Some(&1)
107        );
108
109        // calldata_size counts only the bytes after the 4-byte selector.
110        assert_eq!(
111            record(vec![0x12, 0x34, 0x56, 0x78, 0xaa])
112                .inner()
113                .get(&(selector, 1)),
114            Some(&1)
115        );
116
117        // Fewer than 4 bytes has no selector and is ignored.
118        assert!(record(vec![0x12, 0x34, 0x56]).inner().is_empty());
119    }
120}