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#[derive(DebugAsJson, Clone, PartialEq, Eq, fake::Dummy, serde::Serialize, serde::Deserialize)]
46pub struct EvmExecution {
47 pub block_timestamp: UnixTime,
49
50 pub result: ExecutionResult,
52
53 pub output: Bytes,
55
56 pub logs: Vec<Log>,
58
59 pub gas: Gas,
61
62 pub changes: ExecutionChanges,
64
65 pub deployed_contract_address: Option<Address>,
67}
68
69impl EvmExecution {
70 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 let addr = sender.address;
81 let mut sender_changes = ExecutionAccountChanges::from_original_values(sender); 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 let mut execution = Self {
91 block_timestamp,
92 result: ExecutionResult::new_reverted("reverted externally".into()), output: Bytes::default(), 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 pub fn is_success(&self) -> bool {
105 self.result.is_success()
106 }
107
108 pub fn is_failure(&self) -> bool {
110 not(self.is_success())
111 }
112
113 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 pub fn compare_with_receipt(&self, receipt: &ExternalReceipt) -> anyhow::Result<()> {
124 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 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 for (log_index, (execution_log, receipt_log)) in self.logs.iter().zip(receipt_logs).enumerate() {
150 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 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 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 pub fn apply_receipt(&mut self, receipt: &ExternalReceipt) -> anyhow::Result<()> {
196 self.gas = Gas::from(receipt.gas_used);
198
199 self.fix_logs_gas_left(receipt);
201
202 let execution_cost = receipt.execution_cost();
204
205 if execution_cost > Wei::ZERO {
206 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 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 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 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 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 let mut receipt: ExternalReceipt = Faker.fake();
285 let mut inner_receipt = receipt.0.clone();
286
287 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 inner_receipt.from = sender_address.into();
297 receipt.0 = inner_receipt;
298
299 let timestamp = UnixTime::now();
301
302 let execution = EvmExecution::from_failed_external_transaction(sender.clone(), &receipt, timestamp).unwrap();
304
305 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 let sender_changes = execution.changes.get(&sender_address).unwrap();
314
315 let modified_nonce = sender_changes.nonce.take_modified_ref().unwrap();
317 assert_eq!(*modified_nonce, Nonce::from(2u64));
318
319 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 let mut execution: EvmExecution = Faker.fake();
330 execution.result = ExecutionResult::Success;
331
332 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 assert!(execution.compare_with_receipt(&receipt).is_err());
342 }
343
344 #[test]
345 fn test_compare_with_receipt_logs_length_mismatch() {
346 let mut execution: EvmExecution = Faker.fake();
348 execution.result = ExecutionResult::Success;
349 execution.logs = vec![Faker.fake(), Faker.fake()]; 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()]; } else {
357 panic!("expected be legacy!")
358 }
359
360 assert!(execution.compare_with_receipt(&receipt).is_err());
362 }
363
364 #[test]
365 fn test_compare_with_receipt_log_topics_length_mismatch() {
366 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 let mut execution: EvmExecution = Faker.fake();
375 execution.result = ExecutionResult::Success;
376 execution.logs = vec![log1];
377
378 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 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 assert!(execution.compare_with_receipt(&receipt).is_err());
395 }
396
397 #[test]
398 fn test_compare_with_receipt_topic_content_mismatch() {
399 let topic_value = B256::default();
401 let different_topic = B256::default();
402
403 let mut log1: Log = Faker.fake();
405 log1.topic0 = Some(topic_value.into());
406
407 let mut execution: EvmExecution = Faker.fake();
409 execution.result = ExecutionResult::Success;
410 execution.logs = vec![log1];
411
412 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 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 assert!(execution.compare_with_receipt(&receipt).is_err());
428 }
429
430 #[test]
431 fn test_compare_with_receipt_data_content_mismatch() {
432 let mut log1: Log = Faker.fake();
434 log1.topic0 = Some(Faker.fake());
435 log1.data = vec![1, 2, 3, 4].into();
436
437 let mut execution: EvmExecution = Faker.fake();
439 execution.result = ExecutionResult::Success;
440 execution.logs = vec![log1];
441
442 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 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 assert!(execution.compare_with_receipt(&receipt).is_err());
459 }
460
461 #[test]
462 fn test_fix_logs_gas_left() {
463 const ERC20_TRACE_HASH: [u8; 32] = hex!("31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671");
465 const BALANCE_TRACKER_TRACE_HASH: [u8; 32] = hex!("63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9");
466
467 let mut execution: EvmExecution = Faker.fake();
469 execution.result = ExecutionResult::Success;
470
471 let mut erc20_log: Log = Faker.fake();
473 erc20_log.topic0 = Some(ERC20_TRACE_HASH.into());
474 let execution_gas_left = vec![0u8; 32]; 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]); erc20_log.data = log_data.into();
479
480 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]; balance_log.data = balance_gas_left.into();
485
486 let regular_log: Log = Faker.fake();
488
489 execution.logs = vec![erc20_log, balance_log, regular_log.clone()];
490
491 let receipt_erc20_gas_left = vec![42u8; 32]; 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]); erc20_receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(erc20_topics, alloy_primitives::Bytes::from(erc20_receipt_data));
501
502 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 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 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 execution.fix_logs_gas_left(&receipt);
525
526 let updated_erc20_data = execution.logs[0].data.as_ref();
528 assert_eq!(&updated_erc20_data[0..32], &receipt_erc20_gas_left[..]);
529 assert_eq!(&updated_erc20_data[32..], &[99u8; 32]);
531
532 assert_eq!(execution.logs[1].data.as_ref(), &receipt_balance_gas_left[..]);
534
535 assert_eq!(execution.logs[2].data, regular_log.data);
537 }
538
539 #[test]
540 fn test_apply_receipt() {
541 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 let mut execution: EvmExecution = Faker.fake();
552
553 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 let mut receipt: ExternalReceipt = Faker.fake();
560 receipt.0.from = sender_address.into();
561 receipt.0.gas_used = 100u64; 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 execution.apply_receipt(&receipt).unwrap();
569
570 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)); }
575}