use std::collections::BTreeMap;
use alloy_primitives::B256;
use anyhow::anyhow;
use anyhow::Ok;
use display_json::DebugAsJson;
use hex_literal::hex;
use revm::primitives::alloy_primitives;
use crate::eth::primitives::Account;
use crate::eth::primitives::Address;
use crate::eth::primitives::Bytes;
use crate::eth::primitives::ExecutionAccountChanges;
use crate::eth::primitives::ExecutionResult;
use crate::eth::primitives::ExternalReceipt;
use crate::eth::primitives::Gas;
use crate::eth::primitives::Log;
use crate::eth::primitives::UnixTime;
use crate::eth::primitives::Wei;
use crate::ext::not;
use crate::log_and_err;
pub type ExecutionChanges = BTreeMap<Address, ExecutionAccountChanges>;
#[derive(DebugAsJson, Clone, PartialEq, Eq, fake::Dummy, serde::Serialize, serde::Deserialize)]
pub struct EvmExecution {
pub block_timestamp: UnixTime,
pub result: ExecutionResult,
pub output: Bytes,
pub logs: Vec<Log>,
pub gas: Gas,
pub changes: ExecutionChanges,
pub deployed_contract_address: Option<Address>,
}
impl EvmExecution {
pub fn from_failed_external_transaction(sender: Account, receipt: &ExternalReceipt, block_timestamp: UnixTime) -> anyhow::Result<Self> {
if receipt.is_success() {
return log_and_err!("cannot create failed execution for successful transaction");
}
if not(receipt.inner.logs().is_empty()) {
return log_and_err!("failed receipt should not have produced logs");
}
let mut sender_changes = ExecutionAccountChanges::from_original_values(sender); let sender_next_nonce = sender_changes
.nonce
.take_original_ref()
.ok_or_else(|| anyhow!("original nonce value not found when it should have been populated by from_original_values"))?
.next_nonce();
sender_changes.nonce.set_modified(sender_next_nonce);
let mut execution = Self {
block_timestamp,
result: ExecutionResult::new_reverted("reverted externally".into()), output: Bytes::default(), logs: Vec::new(),
gas: Gas::from(receipt.gas_used),
changes: BTreeMap::from([(sender_changes.address, sender_changes)]),
deployed_contract_address: None,
};
execution.apply_receipt(receipt)?;
Ok(execution)
}
pub fn is_success(&self) -> bool {
matches!(self.result, ExecutionResult::Success)
}
pub fn is_failure(&self) -> bool {
not(self.is_success())
}
pub fn contract_address(&self) -> Option<Address> {
if let Some(contract_address) = &self.deployed_contract_address {
return Some(contract_address.to_owned());
}
None
}
pub fn compare_with_receipt(&self, receipt: &ExternalReceipt) -> anyhow::Result<()> {
if self.is_success() != receipt.is_success() {
return log_and_err!(format!(
"transaction status mismatch | hash={} execution={:?} receipt={:?}",
receipt.hash(),
self.result,
receipt.status()
));
}
let receipt_logs = receipt.inner.logs();
if self.logs.len() != receipt_logs.len() {
tracing::trace!(logs = ?self.logs, "execution logs");
tracing::trace!(logs = ?receipt_logs, "receipt logs");
return log_and_err!(format!(
"logs length mismatch | hash={} execution={} receipt={}",
receipt.hash(),
self.logs.len(),
receipt_logs.len()
));
}
for (log_index, (execution_log, receipt_log)) in self.logs.iter().zip(receipt_logs).enumerate() {
if execution_log.topics_non_empty().len() != receipt_log.topics().len() {
return log_and_err!(format!(
"log topics length mismatch | hash={} log_index={} execution={} receipt={}",
receipt.hash(),
log_index,
execution_log.topics_non_empty().len(),
receipt_log.topics().len(),
));
}
for (topic_index, (execution_log_topic, receipt_log_topic)) in execution_log.topics_non_empty().iter().zip(receipt_log.topics().iter()).enumerate()
{
if B256::from(*execution_log_topic) != *receipt_log_topic {
return log_and_err!(format!(
"log topic content mismatch | hash={} log_index={} topic_index={} execution={} receipt={:#x}",
receipt.hash(),
log_index,
topic_index,
execution_log_topic,
receipt_log_topic,
));
}
}
if execution_log.data.as_ref() != receipt_log.data().data.as_ref() {
return log_and_err!(format!(
"log data content mismatch | hash={} log_index={} execution={} receipt={:#x}",
receipt.hash(),
log_index,
execution_log.data,
receipt_log.data().data,
));
}
}
Ok(())
}
pub fn apply_receipt(&mut self, receipt: &ExternalReceipt) -> anyhow::Result<()> {
self.gas = Gas::from(receipt.gas_used);
self.fix_logs_gas_left(receipt);
let execution_cost = receipt.execution_cost();
if execution_cost > Wei::ZERO {
let sender_address: Address = receipt.0.from.into();
let Some(sender_changes) = self.changes.get_mut(&sender_address) else {
return log_and_err!("sender changes not present in execution when applying execution costs");
};
let sender_balance = *sender_changes.balance.take_ref().ok_or(anyhow!("sender balance was None"))?;
let sender_new_balance = if sender_balance > execution_cost {
sender_balance - execution_cost
} else {
Wei::ZERO
};
sender_changes.balance.set_modified(sender_new_balance);
}
Ok(())
}
fn fix_logs_gas_left(&mut self, receipt: &ExternalReceipt) {
const ERC20_TRACE_EVENT_HASH: [u8; 32] = hex!("31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671");
const BALANCE_TRACKER_TRACE_EVENT_HASH: [u8; 32] = hex!("63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9");
const EVENT_HASHES: [&[u8]; 2] = [&ERC20_TRACE_EVENT_HASH, &BALANCE_TRACKER_TRACE_EVENT_HASH];
let receipt_logs = receipt.inner.logs();
for (execution_log, receipt_log) in self.logs.iter_mut().zip(receipt_logs) {
let execution_log_matches = || execution_log.topic0.is_some_and(|topic| EVENT_HASHES.contains(&topic.as_ref()));
let receipt_log_matches = || receipt_log.topics().first().is_some_and(|topic| EVENT_HASHES.contains(&topic.as_ref()));
let should_overwrite = execution_log_matches() && receipt_log_matches();
if !should_overwrite {
continue;
}
let (Some(destination), Some(source)) = (execution_log.data.get_mut(0..32), receipt_log.data().data.get(0..32)) else {
continue;
};
destination.copy_from_slice(source);
}
}
}
#[cfg(test)]
mod tests {
use fake::Fake;
use fake::Faker;
use super::*;
use crate::eth::primitives::CodeHash;
use crate::eth::primitives::Nonce;
#[test]
fn test_from_failed_external_transaction() {
let sender_address: Address = Faker.fake();
let sender = Account {
address: sender_address,
nonce: Nonce::from(1u16),
balance: Wei::from(1000u64),
bytecode: None,
code_hash: CodeHash::default(),
};
let mut receipt: ExternalReceipt = Faker.fake();
let mut inner_receipt = receipt.0.clone();
if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = inner_receipt.inner {
r.receipt.status = alloy_consensus::Eip658Value::Eip658(false);
r.receipt.logs.clear();
} else {
panic!("expected be legacy!")
}
inner_receipt.from = sender_address.into();
receipt.0 = inner_receipt;
let timestamp = UnixTime::now();
let execution = EvmExecution::from_failed_external_transaction(sender.clone(), &receipt, timestamp).unwrap();
assert_eq!(execution.block_timestamp, timestamp);
assert!(execution.is_failure());
assert_eq!(execution.output, Bytes::default());
assert!(execution.logs.is_empty());
assert_eq!(execution.gas, Gas::from(receipt.gas_used));
let sender_changes = execution.changes.get(&sender_address).unwrap();
assert_eq!(sender_changes.address, sender_address);
let modified_nonce = sender_changes.nonce.take_modified_ref().unwrap();
assert_eq!(*modified_nonce, Nonce::from(2u8));
if receipt.execution_cost() > Wei::ZERO {
let modified_balance = sender_changes.balance.take_modified_ref().unwrap();
assert!(sender.balance >= *modified_balance);
}
}
#[test]
fn test_compare_with_receipt_success_status_mismatch() {
let mut execution: EvmExecution = Faker.fake();
execution.result = ExecutionResult::Success;
let mut receipt: ExternalReceipt = Faker.fake();
if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = &mut receipt.0.inner {
r.receipt.status = alloy_consensus::Eip658Value::Eip658(false);
} else {
panic!("expected be legacy!")
}
assert!(execution.compare_with_receipt(&receipt).is_err());
}
#[test]
fn test_compare_with_receipt_logs_length_mismatch() {
let mut execution: EvmExecution = Faker.fake();
execution.result = ExecutionResult::Success;
execution.logs = vec![Faker.fake(), Faker.fake()]; let mut receipt: ExternalReceipt = Faker.fake();
if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = &mut receipt.0.inner {
r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
r.receipt.logs = vec![alloy_rpc_types_eth::Log::default()]; } else {
panic!("expected be legacy!")
}
assert!(execution.compare_with_receipt(&receipt).is_err());
}
#[test]
fn test_compare_with_receipt_log_topics_length_mismatch() {
let mut log1: Log = Faker.fake();
log1.topic0 = Some(Faker.fake());
log1.topic1 = Some(Faker.fake());
log1.topic2 = None;
log1.topic3 = None;
let mut execution: EvmExecution = Faker.fake();
execution.result = ExecutionResult::Success;
execution.logs = vec![log1];
let mut receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
let topics = vec![B256::default()];
receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default());
let mut receipt: ExternalReceipt = Faker.fake();
if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = &mut receipt.0.inner {
r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
r.receipt.logs = vec![receipt_log.clone()];
} else {
panic!("expected be legacy!")
}
assert!(execution.compare_with_receipt(&receipt).is_err());
}
#[test]
fn test_compare_with_receipt_topic_content_mismatch() {
let topic_value = B256::default();
let different_topic = B256::default();
let mut log1: Log = Faker.fake();
log1.topic0 = Some(topic_value.into());
let mut execution: EvmExecution = Faker.fake();
execution.result = ExecutionResult::Success;
execution.logs = vec![log1];
let mut receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
let topics = vec![different_topic];
receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default());
let mut receipt: ExternalReceipt = Faker.fake();
if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = &mut receipt.0.inner {
r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
r.receipt.logs = vec![receipt_log.clone()];
} else {
panic!("expected be legacy!")
}
assert!(execution.compare_with_receipt(&receipt).is_err());
}
#[test]
fn test_compare_with_receipt_data_content_mismatch() {
let mut log1: Log = Faker.fake();
log1.topic0 = Some(Faker.fake());
log1.data = vec![1, 2, 3, 4].into();
let mut execution: EvmExecution = Faker.fake();
execution.result = ExecutionResult::Success;
execution.logs = vec![log1];
let mut receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
let topics = vec![B256::default()];
receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(topics, alloy_primitives::Bytes::default());
receipt_log.inner.data = alloy_primitives::LogData::new(vec![B256::default()], alloy_primitives::Bytes::from(vec![5, 6, 7, 8])).unwrap();
let mut receipt: ExternalReceipt = Faker.fake();
if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = &mut receipt.0.inner {
r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
r.receipt.logs = vec![receipt_log.clone()];
} else {
panic!("expected be legacy!")
}
assert!(execution.compare_with_receipt(&receipt).is_err());
}
#[test]
fn test_fix_logs_gas_left() {
const ERC20_TRACE_HASH: [u8; 32] = hex!("31738ac4a7c9a10ecbbfd3fed5037971ba81b8f6aa4f72a23f5364e9bc76d671");
const BALANCE_TRACKER_TRACE_HASH: [u8; 32] = hex!("63f1e32b72965e2be75e03024856287aff9e4cdbcec65869c51014fc2c1c95d9");
let mut execution: EvmExecution = Faker.fake();
execution.result = ExecutionResult::Success;
let mut erc20_log: Log = Faker.fake();
erc20_log.topic0 = Some(ERC20_TRACE_HASH.into());
let execution_gas_left = vec![0u8; 32]; let mut log_data = Vec::with_capacity(execution_gas_left.len() + 32);
log_data.extend_from_slice(&execution_gas_left);
log_data.extend_from_slice(&[99u8; 32]); erc20_log.data = log_data.into();
let mut balance_log: Log = Faker.fake();
balance_log.topic0 = Some(BALANCE_TRACKER_TRACE_HASH.into());
let balance_gas_left = vec![0u8; 32]; balance_log.data = balance_gas_left.into();
let regular_log: Log = Faker.fake();
execution.logs = vec![erc20_log, balance_log, regular_log.clone()];
let receipt_erc20_gas_left = vec![42u8; 32]; let mut erc20_receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
let erc20_topics = vec![B256::from_slice(&ERC20_TRACE_HASH)];
let mut erc20_receipt_data = Vec::with_capacity(receipt_erc20_gas_left.len() + 32);
erc20_receipt_data.extend_from_slice(&receipt_erc20_gas_left);
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));
let receipt_balance_gas_left = vec![24u8; 32];
let mut balance_receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
let balance_topics = vec![B256::from_slice(&BALANCE_TRACKER_TRACE_HASH)];
balance_receipt_log.inner.data =
alloy_primitives::LogData::new_unchecked(balance_topics, alloy_primitives::Bytes::from(receipt_balance_gas_left.clone()));
let mut regular_receipt_log = alloy_rpc_types_eth::Log::<alloy_primitives::LogData>::default();
let regular_topics = Vec::new();
regular_receipt_log.inner.data = alloy_primitives::LogData::new_unchecked(regular_topics, alloy_primitives::Bytes::default());
let mut receipt: ExternalReceipt = Faker.fake();
if let alloy_consensus::ReceiptEnvelope::Legacy(ref mut r) = &mut receipt.0.inner {
r.receipt.status = alloy_consensus::Eip658Value::Eip658(true);
r.receipt.logs = vec![erc20_receipt_log.clone(), balance_receipt_log.clone(), regular_receipt_log.clone()];
} else {
panic!("expected be legacy!")
}
execution.fix_logs_gas_left(&receipt);
let updated_erc20_data = execution.logs[0].data.as_ref();
assert_eq!(&updated_erc20_data[0..32], &receipt_erc20_gas_left[..]);
assert_eq!(&updated_erc20_data[32..], &[99u8; 32]);
assert_eq!(execution.logs[1].data.as_ref(), &receipt_balance_gas_left[..]);
assert_eq!(execution.logs[2].data, regular_log.data);
}
#[test]
fn test_apply_receipt() {
let sender_address: Address = Faker.fake();
let sender = Account {
address: sender_address,
nonce: Nonce::from(1u16),
balance: Wei::from(1000u64),
bytecode: None,
code_hash: CodeHash::default(),
};
let mut execution: EvmExecution = Faker.fake();
let sender_changes = ExecutionAccountChanges::from_original_values(sender);
execution.changes = BTreeMap::from([(sender_address, sender_changes)]);
execution.gas = Gas::from(100u64);
let mut receipt: ExternalReceipt = Faker.fake();
receipt.0.from = sender_address.into();
receipt.0.gas_used = 100u64; let gas_price = Wei::from(1u64);
receipt.0.effective_gas_price = gas_price.into();
execution.apply_receipt(&receipt).unwrap();
let sender_changes = execution.changes.get(&sender_address).unwrap();
let modified_balance = sender_changes.balance.take_modified_ref().unwrap();
assert_eq!(*modified_balance, Wei::from(900u64)); }
}