stratus/eth/primitives/
execution.rs

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