revm_statetest_types/
test_unit.rs

1use crate::{AccountInfo, Env, SpecName, Test, TransactionParts};
2use context::{block::BlockEnv, cfg::CfgEnv};
3use database::CacheState;
4use primitives::{hardfork::SpecId, keccak256, Address, Bytes, HashMap, B256};
5use serde::Deserialize;
6use state::Bytecode;
7use std::collections::BTreeMap;
8
9/// Single test unit struct
10#[derive(Debug, PartialEq, Eq, Deserialize)]
11//#[serde(deny_unknown_fields)]
12// field config
13pub struct TestUnit {
14    /// Test info is optional.
15    #[serde(default, rename = "_info")]
16    pub info: Option<serde_json::Value>,
17
18    /// Test environment configuration.
19    ///
20    /// Contains the environmental information for executing the test, including
21    /// block information, coinbase address, difficulty, gas limit, and other
22    /// blockchain state parameters required for proper test execution.
23    pub env: Env,
24
25    /// Pre-execution state.
26    ///
27    /// A mapping of addresses to their account information before the transaction
28    /// is executed. This represents the initial state of all accounts involved
29    /// in the test, including their balances, nonces, code, and storage.
30    pub pre: HashMap<Address, AccountInfo>,
31
32    /// Post-execution expectations per specification.
33    ///
34    /// Maps each Ethereum specification name (hardfork) to a vector of expected
35    /// test results. This allows a single test to define different expected outcomes
36    /// for different protocol versions, enabling comprehensive testing across
37    /// multiple Ethereum upgrades.
38    pub post: BTreeMap<SpecName, Vec<Test>>,
39
40    /// Transaction details to be executed.
41    ///
42    /// Contains the transaction parameters that will be executed against the
43    /// pre-state. This includes sender, recipient, value, data, gas limits,
44    /// and other transaction fields that may vary based on indices.
45    pub transaction: TransactionParts,
46
47    /// Expected output data from the transaction execution.
48    ///
49    /// Optional field containing the expected return data from the transaction.
50    /// This is typically used for testing contract calls that return specific
51    /// values or for CREATE operations that return deployed contract addresses.
52    #[serde(default)]
53    pub out: Option<Bytes>,
54    //pub config
55}
56
57impl TestUnit {
58    /// Prepare the state from the test unit.
59    ///
60    /// This function uses [`TestUnit::pre`] to prepare the pre-state from the test unit.
61    /// It creates a new cache state and inserts the accounts from the test unit.
62    ///
63    /// # Returns
64    ///
65    /// A [`CacheState`] object containing the pre-state accounts and storages.
66    pub fn state(&self) -> CacheState {
67        let mut cache_state = CacheState::new(false);
68        for (address, info) in &self.pre {
69            let code_hash = keccak256(&info.code);
70            let bytecode = Bytecode::new_raw_checked(info.code.clone())
71                .unwrap_or(Bytecode::new_legacy(info.code.clone()));
72            let acc_info = state::AccountInfo {
73                balance: info.balance,
74                code_hash,
75                code: Some(bytecode),
76                nonce: info.nonce,
77                ..Default::default()
78            };
79            cache_state.insert_account_with_storage(*address, acc_info, info.storage.clone());
80        }
81        cache_state
82    }
83
84    /// Create a block environment from the test unit.
85    ///
86    /// This function sets up the block environment using the current test unit's
87    /// environment settings and the provided configuration.
88    ///
89    /// # Arguments
90    ///
91    /// * `cfg` - The configuration environment containing spec and blob settings
92    ///
93    /// # Returns
94    ///
95    /// A configured [`BlockEnv`] ready for execution
96    pub fn block_env(&self, cfg: &mut CfgEnv) -> BlockEnv {
97        let mut block = BlockEnv {
98            number: self.env.current_number,
99            beneficiary: self.env.current_coinbase,
100            timestamp: self.env.current_timestamp,
101            gas_limit: self.env.current_gas_limit.try_into().unwrap_or(u64::MAX),
102            basefee: self
103                .env
104                .current_base_fee
105                .unwrap_or_default()
106                .try_into()
107                .unwrap_or(u64::MAX),
108            difficulty: self.env.current_difficulty,
109            prevrandao: self.env.current_random,
110            ..BlockEnv::default()
111        };
112
113        // Handle EIP-4844 blob gas
114        // Use spec-aware blob fee fraction: Cancun uses 3338477, Prague/Osaka use 5007716
115        if let Some(current_excess_blob_gas) = self.env.current_excess_blob_gas {
116            block.set_blob_excess_gas_and_price(
117                current_excess_blob_gas.to(),
118                cfg.blob_base_fee_update_fraction(),
119            );
120        }
121
122        // Set default prevrandao for merge
123        if cfg.spec.is_enabled_in(SpecId::MERGE) && block.prevrandao.is_none() {
124            block.prevrandao = Some(B256::default());
125        }
126
127        block
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use context_interface::block::calc_blob_gasprice;
135    use primitives::{
136        eip4844::{BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE},
137        U256,
138    };
139
140    /// Creates a minimal TestUnit with excess blob gas set for testing blob fee calculation
141    fn create_test_unit_with_excess_blob_gas(excess_blob_gas: u64) -> TestUnit {
142        TestUnit {
143            info: None,
144            env: Env {
145                current_chain_id: None,
146                current_coinbase: Address::ZERO,
147                current_difficulty: U256::ZERO,
148                current_gas_limit: U256::from(1_000_000u64),
149                current_number: U256::from(1u64),
150                current_timestamp: U256::from(1u64),
151                current_base_fee: Some(U256::from(1u64)),
152                previous_hash: None,
153                current_random: None,
154                current_beacon_root: None,
155                current_withdrawals_root: None,
156                current_excess_blob_gas: Some(U256::from(excess_blob_gas)),
157            },
158            pre: HashMap::default(),
159            post: BTreeMap::default(),
160            transaction: TransactionParts {
161                tx_type: None,
162                data: vec![],
163                gas_limit: vec![],
164                gas_price: None,
165                nonce: U256::ZERO,
166                secret_key: B256::ZERO,
167                sender: None,
168                to: None,
169                value: vec![],
170                max_fee_per_gas: None,
171                max_priority_fee_per_gas: None,
172                initcodes: None,
173                access_lists: vec![],
174                authorization_list: None,
175                blob_versioned_hashes: vec![],
176                max_fee_per_blob_gas: None,
177            },
178            out: None,
179        }
180    }
181
182    /// Test that block_env uses the correct blob base fee update fraction for Cancun
183    #[test]
184    fn test_block_env_blob_fee_fraction_cancun() {
185        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
186
187        let mut cfg = CfgEnv::default();
188        cfg.spec = SpecId::CANCUN;
189
190        let block = unit.block_env(&mut cfg);
191
192        // Verify blob gas price is calculated with Cancun fraction
193        let blob_info = block
194            .blob_excess_gas_and_price
195            .expect("blob info should be set");
196        assert_eq!(blob_info.excess_blob_gas, 0x240000);
197
198        // Calculate expected price with Cancun fraction (3338477)
199        // blob_gasprice = fake_exponential(1, excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION)
200        // With excess_blob_gas=0x240000 and CANCUN fraction=3338477, price should be 2
201        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN);
202        assert_eq!(blob_info.blob_gasprice, expected_price);
203        assert_eq!(blob_info.blob_gasprice, 2); // With Cancun fraction, price is 2
204    }
205
206    /// Test that block_env uses the correct blob base fee update fraction for Prague
207    #[test]
208    fn test_block_env_blob_fee_fraction_prague() {
209        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
210
211        let mut cfg = CfgEnv::default();
212        cfg.spec = SpecId::PRAGUE;
213
214        let block = unit.block_env(&mut cfg);
215
216        // Verify blob gas price is calculated with Prague fraction
217        let blob_info = block
218            .blob_excess_gas_and_price
219            .expect("blob info should be set");
220        assert_eq!(blob_info.excess_blob_gas, 0x240000);
221
222        // Calculate expected price with Prague fraction (5007716)
223        // With excess_blob_gas=0x240000 and PRAGUE fraction=5007716, price should be 1
224        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
225        assert_eq!(blob_info.blob_gasprice, expected_price);
226        assert_eq!(blob_info.blob_gasprice, 1); // With Prague fraction, price is 1
227    }
228
229    /// Test that block_env uses the correct blob base fee update fraction for Osaka
230    #[test]
231    fn test_block_env_blob_fee_fraction_osaka() {
232        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
233
234        let mut cfg = CfgEnv::default();
235        cfg.spec = SpecId::OSAKA;
236
237        let block = unit.block_env(&mut cfg);
238
239        // Osaka should use Prague fraction (same as Prague)
240        let blob_info = block
241            .blob_excess_gas_and_price
242            .expect("blob info should be set");
243        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
244        assert_eq!(blob_info.blob_gasprice, expected_price);
245        assert_eq!(blob_info.blob_gasprice, 1); // With Prague fraction, price is 1
246    }
247
248    /// Test that demonstrates the bug scenario from IMPLEMENTATION_PROMPT.md
249    /// With excess_blob_gas=0x240000 and maxFeePerBlobGas=0x01:
250    /// - Cancun fraction (3338477): blob_price = 2, tx FAILS (insufficient fee)
251    /// - Prague fraction (5007716): blob_price = 1, tx SUCCEEDS
252    #[test]
253    fn test_blob_fee_difference_affects_tx_validity() {
254        let excess_blob_gas = 0x240000u64;
255
256        // Calculate prices with both fractions
257        let cancun_price =
258            calc_blob_gasprice(excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN);
259        let prague_price =
260            calc_blob_gasprice(excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
261
262        // Verify the prices are different
263        assert_eq!(cancun_price, 2, "Cancun blob price should be 2");
264        assert_eq!(prague_price, 1, "Prague blob price should be 1");
265
266        // With maxFeePerBlobGas=1:
267        // - Cancun: 1 < 2, tx would fail with insufficient fee
268        // - Prague: 1 >= 1, tx would succeed
269        let max_fee_per_blob_gas = 1u128;
270        assert!(
271            max_fee_per_blob_gas < cancun_price,
272            "Tx should fail with Cancun fraction"
273        );
274        assert!(
275            max_fee_per_blob_gas >= prague_price,
276            "Tx should succeed with Prague fraction"
277        );
278    }
279}