cfx_execute_helper/observer/
access_list.rs

1use cfx_executor::observer::{
2    CallTracer, CheckpointTracer, DrainTrace, InternalTransferTracer,
3    OpcodeTracer, SetAuthTracer, StorageTracer,
4};
5use cfx_types::{u256_to_address_be, u256_to_h256_be, Address, H256};
6use cfx_vm_interpreter::instructions::Instruction;
7use cfx_vm_types::InterpreterInfo;
8use primitives::{AccessList, AccessListItem};
9use std::collections::{BTreeMap, BTreeSet, HashSet};
10use typemap::ShareDebugMap;
11
12/// An [Inspector] that collects touched accounts and storage slots.
13///
14/// This can be used to construct an [AccessList] for a transaction via
15/// `eth_createAccessList`
16#[derive(Debug, Default)]
17pub struct AccessListInspector {
18    /// All addresses that should be excluded from the final accesslist
19    excluded: HashSet<Address>,
20    /// All addresses and touched slots
21    touched_slots: BTreeMap<Address, BTreeSet<H256>>,
22}
23
24impl From<(AccessList, HashSet<Address>)> for AccessListInspector {
25    fn from(data: (AccessList, HashSet<Address>)) -> Self {
26        Self::new(data.0, data.1)
27    }
28}
29
30impl AccessListInspector {
31    /// Creates a new [AccessListInspector] with the given excluded addresses.
32    pub fn new(access_list: AccessList, excluded: HashSet<Address>) -> Self {
33        let mut touched_slots = BTreeMap::<Address, BTreeSet<H256>>::new();
34        for item in access_list {
35            if excluded.contains(&item.address) {
36                continue;
37            }
38            touched_slots
39                .entry(item.address)
40                .or_default()
41                .extend(item.storage_keys);
42        }
43        Self {
44            excluded,
45            touched_slots,
46        }
47    }
48
49    /// Returns the excluded addresses.
50    pub fn excluded(&self) -> &HashSet<Address> { &self.excluded }
51
52    /// Returns a reference to the map of addresses and their corresponding
53    /// touched storage slots.
54    pub fn touched_slots(&self) -> &BTreeMap<Address, BTreeSet<H256>> {
55        &self.touched_slots
56    }
57
58    /// Consumes the inspector and returns the map of addresses and their
59    /// corresponding touched storage slots.
60    pub fn into_touched_slots(self) -> BTreeMap<Address, BTreeSet<H256>> {
61        self.touched_slots
62    }
63
64    /// Returns list of addresses and storage keys used by the transaction. It
65    /// gives you the list of addresses and storage keys that were touched
66    /// during execution.
67    pub fn into_access_list(self) -> AccessList {
68        let items = self.touched_slots.into_iter().map(|(address, slots)| {
69            AccessListItem {
70                address,
71                storage_keys: slots.into_iter().collect(),
72            }
73        });
74        items.collect()
75    }
76
77    /// Returns list of addresses and storage keys used by the transaction. It
78    /// gives you the list of addresses and storage keys that were touched
79    /// during execution.
80    pub fn access_list(&self) -> AccessList {
81        let items =
82            self.touched_slots
83                .iter()
84                .map(|(address, slots)| AccessListItem {
85                    address: *address,
86                    storage_keys: slots.iter().copied().collect(),
87                });
88        items.collect()
89    }
90
91    pub fn collect_excluded_addresses(&mut self, item: Address) {
92        self.excluded.insert(item);
93    }
94
95    /// Marks an address as touched, unless it is one that never belongs in an
96    /// access list: the sender, the callee, the precompiles and the 7702
97    /// authorities are warm already.
98    fn record_address(&mut self, address: Address) {
99        if !self.excluded.contains(&address) {
100            self.touched_slots.entry(address).or_default();
101        }
102    }
103}
104
105impl DrainTrace for AccessListInspector {
106    fn drain_trace(self, map: &mut ShareDebugMap) {
107        map.insert::<AccessListKey>(self.into_access_list());
108    }
109}
110
111pub struct AccessListKey;
112
113impl typemap::Key for AccessListKey {
114    type Value = AccessList;
115}
116
117impl OpcodeTracer for AccessListInspector {
118    fn do_trace_opcode(&self, enabled: &mut bool) { *enabled |= true; }
119
120    fn step(&mut self, interp: &dyn InterpreterInfo) {
121        let Some(ins) = Instruction::from_u8(interp.current_opcode()) else {
122            return;
123        };
124        match ins {
125            Instruction::SLOAD | Instruction::SSTORE => {
126                if let Some(slot) = interp.stack().last() {
127                    let cur_contract = interp.contract_address();
128                    self.touched_slots
129                        .entry(cur_contract)
130                        .or_default()
131                        .insert(u256_to_h256_be(*slot));
132                }
133            }
134            Instruction::EXTCODECOPY
135            | Instruction::EXTCODEHASH
136            | Instruction::EXTCODESIZE
137            | Instruction::BALANCE
138            | Instruction::SUICIDE => {
139                let operands = match ins {
140                    Instruction::EXTCODECOPY => 4,
141                    _ => 1,
142                };
143                let stack = interp.stack();
144                if stack.len() >= operands {
145                    // The address is these instructions' first operand, so it
146                    // sits on top of the stack.
147                    let address = stack[stack.len() - 1];
148                    self.record_address(u256_to_address_be(address));
149                }
150            }
151            Instruction::DELEGATECALL
152            | Instruction::CALL
153            | Instruction::STATICCALL
154            | Instruction::CALLCODE => {
155                // Every call instruction takes `gas` first and the callee
156                // second, so the address sits one below the top of the stack.
157                // Requiring the full operand count keeps us off a stack that
158                // the instruction itself is about to reject as underflowed.
159                let operands = match ins {
160                    // gas, address, value, in_off, in_size, out_off, out_size
161                    Instruction::CALL | Instruction::CALLCODE => 7,
162                    // the same, without `value`
163                    _ => 6,
164                };
165                let stack = interp.stack();
166                if stack.len() >= operands {
167                    let address = stack[stack.len() - 2];
168                    self.record_address(u256_to_address_be(address));
169                }
170            }
171            _ => (),
172        }
173    }
174}
175
176impl CallTracer for AccessListInspector {}
177impl CheckpointTracer for AccessListInspector {}
178impl InternalTransferTracer for AccessListInspector {}
179impl StorageTracer for AccessListInspector {}
180impl SetAuthTracer for AccessListInspector {}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn caller_access_list_merges_slots_for_duplicate_addresses() {
188        let address = Address::from_low_u64_be(1);
189        let lower_slot = H256::from_low_u64_be(7);
190        let higher_slot = H256::from_low_u64_be(8);
191        let inspector = AccessListInspector::new(
192            vec![
193                AccessListItem {
194                    address,
195                    storage_keys: vec![lower_slot],
196                },
197                AccessListItem {
198                    address,
199                    storage_keys: vec![higher_slot],
200                },
201            ],
202            HashSet::new(),
203        );
204
205        assert_eq!(
206            inspector.access_list(),
207            vec![AccessListItem {
208                address,
209                storage_keys: vec![lower_slot, higher_slot],
210            }]
211        );
212    }
213
214    #[test]
215    fn caller_access_list_omits_excluded_addresses() {
216        let excluded_address = Address::from_low_u64_be(1);
217        let included_address = Address::from_low_u64_be(2);
218        let excluded_slot = H256::from_low_u64_be(7);
219        let included_slot = H256::from_low_u64_be(8);
220        let inspector = AccessListInspector::new(
221            vec![
222                AccessListItem {
223                    address: excluded_address,
224                    storage_keys: vec![excluded_slot],
225                },
226                AccessListItem {
227                    address: included_address,
228                    storage_keys: vec![included_slot],
229                },
230            ],
231            HashSet::from([excluded_address]),
232        );
233
234        assert_eq!(
235            inspector.access_list(),
236            vec![AccessListItem {
237                address: included_address,
238                storage_keys: vec![included_slot],
239            }]
240        );
241    }
242}