stratus/eth/primitives/
execution.rs

1use std::collections::BTreeMap;
2
3use alloy_primitives::B256;
4use anyhow::Ok;
5use anyhow::anyhow;
6use display_json::DebugAsJson;
7use hex_literal::hex;
8use revm::primitives::alloy_primitives;
9
10use crate::eth::primitives::Account;
11use crate::eth::primitives::Address;
12use crate::eth::primitives::Bytes;
13use crate::eth::primitives::ExecutionAccountChanges;
14use crate::eth::primitives::ExecutionResult;
15use crate::eth::primitives::ExternalReceipt;
16use crate::eth::primitives::Gas;
17use crate::eth::primitives::Log;
18use crate::eth::primitives::UnixTime;
19use crate::eth::primitives::Wei;
20use crate::ext::not;
21use crate::log_and_err;
22
23pub type ExecutionChanges = BTreeMap<Address, ExecutionAccountChanges>;
24
25pub trait ExecutionChangesExt {
26    fn merge(&mut self, other: ExecutionChanges);
27}
28
29impl ExecutionChangesExt for ExecutionChanges {
30    fn merge(&mut self, other: ExecutionChanges) {
31        for (address, changes) in other {
32            match self.entry(address) {
33                std::collections::btree_map::Entry::Occupied(mut entry) => {
34                    entry.get_mut().merge(changes);
35                }
36                std::collections::btree_map::Entry::Vacant(entry) => {
37                    entry.insert(changes);
38                }
39            }
40        }
41    }
42}
43
44/// Output of a transaction executed in the EVM.
45#[derive(DebugAsJson, Clone, PartialEq, Eq, fake::Dummy, serde::Serialize, serde::Deserialize)]
46pub struct EvmExecution {
47    /// Assumed block timestamp during the execution.
48    pub block_timestamp: UnixTime,
49
50    /// Status of the execution.
51    pub result: ExecutionResult,
52
53    /// Output returned by the function execution (can be the function output or an exception).
54    pub output: Bytes,
55
56    /// Logs emitted by the function execution.
57    pub logs: Vec<Log>,
58
59    /// Consumed gas.
60    pub gas: Gas,
61
62    /// Storage changes that happened during the transaction execution.
63    pub changes: ExecutionChanges,
64
65    /// The contract address if the executed transaction deploys a contract.
66    pub deployed_contract_address: Option<Address>,
67}
68
69impl EvmExecution {
70    /// Creates an execution from an external transaction that failed.
71    pub fn from_failed_external_transaction(sender: Account, receipt: &ExternalReceipt, block_timestamp: UnixTime) -> anyhow::Result<Self> {
72        if receipt.is_success() {
73            return log_and_err!("cannot create failed execution for successful transaction");
74        }
75        if not(receipt.inner.logs().is_empty()) {
76            return log_and_err!("failed receipt should not have produced logs");
77        }
78
79        // generate sender changes incrementing the nonce
80        let addr = sender.address;
81        let mut sender_changes = ExecutionAccountChanges::from_original_values(sender); // NOTE: don't change from_original_values without updating .expect() below
82        let sender_next_nonce = sender_changes
83            .nonce
84            .take_original_ref()
85            .ok_or_else(|| anyhow!("original nonce value not found when it should have been populated by from_original_values"))?
86            .next_nonce();
87        sender_changes.nonce.set_modified(sender_next_nonce);
88
89        // crete execution and apply costs
90        let mut execution = Self {
91            block_timestamp,
92            result: ExecutionResult::new_reverted("reverted externally".into()), // assume it reverted
93            output: Bytes::default(),                                            // we cannot really know without performing an eth_call to the external system
94            logs: Vec::new(),
95            gas: Gas::from(receipt.gas_used),
96            changes: BTreeMap::from([(addr, sender_changes)]),
97            deployed_contract_address: None,
98        };
99        execution.apply_receipt(receipt)?;
100        Ok(execution)
101    }
102
103    /// Checks if the current transaction was completed normally.
104    pub fn is_success(&self) -> bool {
105        self.result.is_success()
106    }
107
108    /// Checks if the current transaction was completed with a failure (reverted or halted).
109    pub fn is_failure(&self) -> bool {
110        not(self.is_success())
111    }
112
113    /// Returns the address of the deployed contract if the transaction is a deployment.
114    pub fn contract_address(&self) -> Option<Address> {
115        if let Some(contract_address) = &self.deployed_contract_address {
116            return Some(contract_address.to_owned());
117        }
118
119        None
120    }
121
122    /// Checks if current execution state matches the information present in the external receipt.
123    pub fn compare_with_receipt(&self, receipt: &ExternalReceipt) -> anyhow::Result<()> {
124        // compare execution status
125        if self.is_success() != receipt.is_success() {
126            return log_and_err!(format!(
127                "transaction status mismatch | hash={} execution={:?} receipt={:?}",
128                receipt.hash(),
129                self.result,
130                receipt.status()
131            ));
132        }
133
134        let receipt_logs = receipt.inner.logs();
135
136        // compare logs length
137        if self.logs.len() != receipt_logs.len() {
138            tracing::trace!(logs = ?self.logs, "execution logs");
139            tracing::trace!(logs = ?receipt_logs, "receipt logs");
140            return log_and_err!(format!(
141                "logs length mismatch | hash={} execution={} receipt={}",
142                receipt.hash(),
143                self.logs.len(),
144                receipt_logs.len()
145            ));
146        }
147
148        // compare logs pairs
149        for (log_index, (execution_log, receipt_log)) in self.logs.iter().zip(receipt_logs).enumerate() {
150            // compare log topics length
151            if execution_log.topics_non_empty().len() != receipt_log.topics().len() {
152                return log_and_err!(format!(
153                    "log topics length mismatch | hash={} log_index={} execution={} receipt={}",
154                    receipt.hash(),
155                    log_index,
156                    execution_log.topics_non_empty().len(),
157                    receipt_log.topics().len(),
158                ));
159            }
160
161            // compare log topics content
162            for (topic_index, (execution_log_topic, receipt_log_topic)) in execution_log.topics_non_empty().iter().zip(receipt_log.topics().iter()).enumerate()
163            {
164                if B256::from(*execution_log_topic) != *receipt_log_topic {
165                    return log_and_err!(format!(
166                        "log topic content mismatch | hash={} log_index={} topic_index={} execution={} receipt={:#x}",
167                        receipt.hash(),
168                        log_index,
169                        topic_index,
170                        execution_log_topic,
171                        receipt_log_topic,
172                    ));
173                }
174            }
175
176            // compare log data content
177            if execution_log.data.as_ref() != receipt_log.data().data.as_ref() {
178                return log_and_err!(format!(
179                    "log data content mismatch | hash={} log_index={} execution={} receipt={:#x}",
180                    receipt.hash(),
181                    log_index,
182                    execution_log.data,
183                    receipt_log.data().data,
184                ));
185            }
186        }
187        Ok(())
188    }
189
190    /// External transactions are re-executed locally with max gas and zero gas price.
191    ///
192    /// This causes some attributes to be different from the original execution.
193    ///
194    /// This method updates the attributes that can diverge based on the receipt of the external transaction.
195    pub fn apply_receipt(&mut self, receipt: &ExternalReceipt) -> anyhow::Result<()> {
196        // fix gas
197        self.gas = Gas::from(receipt.gas_used);
198
199        // fix logs
200        self.fix_logs_gas_left(receipt);
201
202        // fix sender balance
203        let execution_cost = receipt.execution_cost();
204
205        if execution_cost > Wei::ZERO {
206            // find sender changes
207            let sender_address: Address = receipt.0.from.into();
208            let Some(sender_changes) = self.changes.get_mut(&sender_address) else {
209                return log_and_err!("sender changes not present in execution when applying execution costs");
210            };
211
212            // subtract execution cost from sender balance
213            let sender_balance = *sender_changes.balance.take_ref().ok_or(anyhow!("sender balance was None"))?;
214
215            let sender_new_balance = if sender_balance > execution_cost {
216                sender_balance - execution_cost
217            } else {
218                Wei::ZERO
219            };
220            sender_changes.balance.set_modified(sender_new_balance);
221        }
222
223        Ok(())
224    }
225
226    /// Apply `gasLeft` values from receipt to execution logs.
227    ///
228    /// External transactions are re-executed locally with a different amount of gas limit, so, rely
229    /// on the given receipt to copy the `gasLeft` values found in Logs.
230    ///
231    /// This is necessary if the contract emits an event that puts `gasLeft` in a log, this function
232    /// covers the following events that do the described:
233    ///
234    /// - `ERC20Trace` (topic0: `0x31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671`)
235    /// - `BalanceTrackerTrace` (topic0: `0x63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9`)
236    ///
237    /// The overwriting should be done by copying the first 32 bytes from the receipt to log in `self`.
238    fn fix_logs_gas_left(&mut self, receipt: &ExternalReceipt) {
239        const ERC20_TRACE_EVENT_HASH: [u8; 32] = hex!("31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671");
240        const BALANCE_TRACKER_TRACE_EVENT_HASH: [u8; 32] = hex!("63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9");
241
242        const EVENT_HASHES: [&[u8]; 2] = [&ERC20_TRACE_EVENT_HASH, &BALANCE_TRACKER_TRACE_EVENT_HASH];
243
244        let receipt_logs = receipt.inner.logs();
245
246        for (execution_log, receipt_log) in self.logs.iter_mut().zip(receipt_logs) {
247            let execution_log_matches = || execution_log.topic0.is_some_and(|topic| EVENT_HASHES.contains(&topic.as_ref()));
248            let receipt_log_matches = || receipt_log.topics().first().is_some_and(|topic| EVENT_HASHES.contains(&topic.as_ref()));
249
250            // only try overwriting if both logs refer to the target event
251            let should_overwrite = execution_log_matches() && receipt_log_matches();
252            if !should_overwrite {
253                continue;
254            }
255
256            let (Some(destination), Some(source)) = (execution_log.data.get_mut(0..32), receipt_log.data().data.get(0..32)) else {
257                continue;
258            };
259            destination.copy_from_slice(source);
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use fake::Fake;
267    use fake::Faker;
268
269    use super::*;
270    use crate::eth::primitives::Nonce;
271
272    #[test]
273    fn test_from_failed_external_transaction() {
274        // Create a mock sender account
275        let sender_address: Address = Faker.fake();
276        let sender = Account {
277            address: sender_address,
278            nonce: Nonce::from(1u64),
279            balance: Wei::from(1000u64),
280            bytecode: None,
281        };
282
283        // Create a mock failed receipt
284        let mut receipt: ExternalReceipt = Faker.fake();
285        let mut inner_receipt = receipt.0.clone();
286
287        // Clear logs for failed transaction
288        if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = inner_receipt.inner {
289            r.receipt.status = alloy_consensus::Eip658Value::Eip658(false);
290            r.receipt.logs.clear();
291        } else {
292            panic!("expected be legacy!")
293        }
294
295        // Update from address
296        inner_receipt.from = sender_address.into();
297        receipt.0 = inner_receipt;
298
299        // Set timestamp
300        let timestamp = UnixTime::now();
301
302        // Test the method
303        let execution = EvmExecution::from_failed_external_transaction(sender.clone(), &receipt, timestamp).unwrap();
304
305        // Verify execution state
306        assert_eq!(execution.block_timestamp, timestamp);
307        assert!(execution.is_failure());
308        assert_eq!(execution.output, Bytes::default());
309        assert!(execution.logs.is_empty());
310        assert_eq!(execution.gas, Gas::from(receipt.gas_used));
311
312        // Verify sender changes
313        let sender_changes = execution.changes.get(&sender_address).unwrap();
314
315        // Nonce should be incremented
316        let modified_nonce = sender_changes.nonce.take_modified_ref().unwrap();
317        assert_eq!(*modified_nonce, Nonce::from(2u64));
318
319        // Balance should be reduced by execution cost
320        if receipt.execution_cost() > Wei::ZERO {
321            let modified_balance = sender_changes.balance.take_modified_ref().unwrap();
322            assert!(sender.balance >= *modified_balance);
323        }
324    }
325
326    #[test]
327    fn test_compare_with_receipt_success_status_mismatch() {
328        // Create a mock execution (success)
329        let mut execution: EvmExecution = Faker.fake();
330        execution.result = ExecutionResult::Success;
331
332        // Create a mock receipt (failed)
333        let mut receipt: ExternalReceipt = Faker.fake();
334        if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner {
335            r.receipt.status = alloy_consensus::Eip658Value::Eip658(false);
336        } else {
337            panic!("expected be legacy!")
338        }
339
340        // Verify comparison fails
341        assert!(execution.compare_with_receipt(&receipt).is_err());
342    }
343
344    #[test]
345    fn test_compare_with_receipt_logs_length_mismatch() {
346        // Create a mock execution with logs
347        let mut execution: EvmExecution = Faker.fake();
348        execution.result = ExecutionResult::Success;
349        execution.logs = vec![Faker.fake(), Faker.fake()]; // Two logs
350
351        // Create a mock receipt with different number of logs
352        let mut receipt: ExternalReceipt = Faker.fake();
353        if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner {
354            r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
355            r.receipt.logs = vec![alloy_rpc_types_eth::Log::default()]; // Only one log
356        } else {
357            panic!("expected be legacy!")
358        }
359
360        // Verify comparison fails
361        assert!(execution.compare_with_receipt(&receipt).is_err());
362    }
363
364    #[test]
365    fn test_compare_with_receipt_log_topics_length_mismatch() {
366        // Create a mock log with topics
367        let mut log1: Log = Faker.fake();
368        log1.topic0 = Some(Faker.fake());
369        log1.topic1 = Some(Faker.fake());
370        log1.topic2 = None;
371        log1.topic3 = None;
372
373        // Create a mock execution with that log
374        let mut execution: EvmExecution = Faker.fake();
375        execution.result = ExecutionResult::Success;
376        execution.logs = vec![log1];
377
378        // Create receipt log with different number of topics
379        let mut receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
380        let topics = vec![B256::default()];
381        receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default());
382        // Only one topic instead of two
383
384        // Create a receipt with this log
385        let mut receipt: ExternalReceipt = Faker.fake();
386        if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner {
387            r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
388            r.receipt.logs = vec![receipt_log.clone()];
389        } else {
390            panic!("expected be legacy!")
391        }
392
393        // Verify comparison fails
394        assert!(execution.compare_with_receipt(&receipt).is_err());
395    }
396
397    #[test]
398    fn test_compare_with_receipt_topic_content_mismatch() {
399        // Create a topic
400        let topic_value = B256::default();
401        let different_topic = B256::default();
402
403        // Create a mock log with the topic
404        let mut log1: Log = Faker.fake();
405        log1.topic0 = Some(topic_value.into());
406
407        // Create execution with that log
408        let mut execution: EvmExecution = Faker.fake();
409        execution.result = ExecutionResult::Success;
410        execution.logs = vec![log1];
411
412        // Create receipt log with different topic content
413        let mut receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
414        let topics = vec![different_topic];
415        receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default());
416
417        // Create receipt with this log
418        let mut receipt: ExternalReceipt = Faker.fake();
419        if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner {
420            r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
421            r.receipt.logs = vec![receipt_log.clone()];
422        } else {
423            panic!("expected be legacy!")
424        }
425
426        // Verify comparison fails
427        assert!(execution.compare_with_receipt(&receipt).is_err());
428    }
429
430    #[test]
431    fn test_compare_with_receipt_data_content_mismatch() {
432        // Create a mock log with data
433        let mut log1: Log = Faker.fake();
434        log1.topic0 = Some(Faker.fake());
435        log1.data = vec![1, 2, 3, 4].into();
436
437        // Create execution with that log
438        let mut execution: EvmExecution = Faker.fake();
439        execution.result = ExecutionResult::Success;
440        execution.logs = vec![log1];
441
442        // Create receipt log with different data
443        let mut receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
444        let topics = vec![B256::default()];
445        receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default());
446        receipt_log.inner.data = alloy_primitives::LogData::new(vec![B256::default()], alloy_primitives::Bytes::from(vec![5, 6, 7, 8])).unwrap();
447
448        // Create receipt with this log
449        let mut receipt: ExternalReceipt = Faker.fake();
450        if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner {
451            r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
452            r.receipt.logs = vec![receipt_log.clone()];
453        } else {
454            panic!("expected be legacy!")
455        }
456
457        // Verify comparison fails
458        assert!(execution.compare_with_receipt(&receipt).is_err());
459    }
460
461    #[test]
462    fn test_fix_logs_gas_left() {
463        // Set up test constants
464        const ERC20_TRACE_HASH: [u8; 32] = hex!("31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671");
465        const BALANCE_TRACKER_TRACE_HASH: [u8; 32] = hex!("63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9");
466
467        // Create a mock execution with logs that have gasLeft value we want to override
468        let mut execution: EvmExecution = Faker.fake();
469        execution.result = ExecutionResult::Success;
470
471        // Create an ERC20 Trace log with mock gasLeft value
472        let mut erc20_log: Log = Faker.fake();
473        erc20_log.topic0 = Some(ERC20_TRACE_HASH.into());
474        let execution_gas_left = vec![0u8; 32]; // Initial value all zeros
475        let mut log_data = Vec::with_capacity(execution_gas_left.len() + 32);
476        log_data.extend_from_slice(&execution_gas_left);
477        log_data.extend_from_slice(&[99u8; 32]); // Add some additional data
478        erc20_log.data = log_data.into();
479
480        // Create a Balance Tracker Trace log
481        let mut balance_log: Log = Faker.fake();
482        balance_log.topic0 = Some(BALANCE_TRACKER_TRACE_HASH.into());
483        let balance_gas_left = vec![0u8; 32]; // Initial value all zeros
484        balance_log.data = balance_gas_left.into();
485
486        // Create a regular log (not one we're targeting)
487        let regular_log: Log = Faker.fake();
488
489        execution.logs = vec![erc20_log, balance_log, regular_log.clone()];
490
491        // Create receipt logs with different gasLeft values
492        let receipt_erc20_gas_left = vec![42u8; 32]; // Different value for comparison
493        let mut erc20_receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
494        let erc20_topics = vec![B256::from_slice(&ERC20_TRACE_HASH)];
495
496        let mut erc20_receipt_data = Vec::with_capacity(receipt_erc20_gas_left.len() + 32);
497        erc20_receipt_data.extend_from_slice(&receipt_erc20_gas_left);
498        erc20_receipt_data.extend_from_slice(&[99u8; 32]); // Match additional data
499
500        erc20_receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(erc20_topics, alloy_primitives::Bytes::from(erc20_receipt_data));
501
502        // Balance tracker receipt log
503        let receipt_balance_gas_left = vec![24u8; 32];
504        let mut balance_receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
505        let balance_topics = vec![B256::from_slice(&BALANCE_TRACKER_TRACE_HASH)];
506        balance_receipt_log.inner.data =
507            alloy_primitives::LogData::new_unchecked(balance_topics, alloy_primitives::Bytes::from(receipt_balance_gas_left.clone()));
508
509        // Regular log for receipt
510        let mut regular_receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
511        let regular_topics = Vec::new();
512        regular_receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(regular_topics, alloy_primitives::Bytes::default());
513
514        // Create receipt with these logs
515        let mut receipt: ExternalReceipt = Faker.fake();
516        if let alloy_consensus::ReceiptEnvelope::Legacy(r) = &mut receipt.0.inner {
517            r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
518            r.receipt.logs = vec![erc20_receipt_log.clone(), balance_receipt_log.clone(), regular_receipt_log.clone()];
519        } else {
520            panic!("expected be legacy!")
521        }
522
523        // Apply the fix
524        execution.fix_logs_gas_left(&receipt);
525
526        // Verify the first 32 bytes of ERC20 log data was overwritten
527        let updated_erc20_data = execution.logs[0].data.as_ref();
528        assert_eq!(&updated_erc20_data[0..32], &receipt_erc20_gas_left[..]);
529        // Rest of the data should remain unchanged
530        assert_eq!(&updated_erc20_data[32..], &[99u8; 32]);
531
532        // Verify the first 32 bytes of Balance Tracker log data was overwritten
533        assert_eq!(execution.logs[1].data.as_ref(), &receipt_balance_gas_left[..]);
534
535        // Verify regular log data wasn't modified
536        assert_eq!(execution.logs[2].data, regular_log.data);
537    }
538
539    #[test]
540    fn test_apply_receipt() {
541        // Create a mock sender account with balance
542        let sender_address: Address = Faker.fake();
543        let sender = Account {
544            address: sender_address,
545            nonce: Nonce::from(1u64),
546            balance: Wei::from(1000u64),
547            bytecode: None,
548        };
549
550        // Create a mock execution
551        let mut execution: EvmExecution = Faker.fake();
552
553        // Set up execution with sender account
554        let sender_changes = ExecutionAccountChanges::from_original_values(sender);
555        execution.changes = BTreeMap::from([(sender_address, sender_changes)]);
556        execution.gas = Gas::from(100u64);
557
558        // Create a receipt with higher gas used and execution cost
559        let mut receipt: ExternalReceipt = Faker.fake();
560        receipt.0.from = sender_address.into();
561        receipt.0.gas_used = 100u64; // Higher gas
562
563        // Make sure transaction has a cost
564        let gas_price = Wei::from(1u64);
565        receipt.0.effective_gas_price = gas_price.try_into().expect("wei was created with u64 which fits u128 qed.");
566
567        // Apply receipt
568        execution.apply_receipt(&receipt).unwrap();
569
570        // Verify sender balance was reduced by execution cost
571        let sender_changes = execution.changes.get(&sender_address).unwrap();
572        let modified_balance = sender_changes.balance.take_modified_ref().unwrap();
573        assert_eq!(*modified_balance, Wei::from(900u64)); // 1000 - 100
574    }
575}