Skip to main content

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, AddressMap, Bytes, 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: AddressMap<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            slot_num: self
111                .env
112                .slot_number
113                .unwrap_or_default()
114                .try_into()
115                .unwrap_or(u64::MAX),
116            ..BlockEnv::default()
117        };
118
119        // Handle EIP-4844 blob gas
120        // Use spec-aware blob fee fraction: Cancun uses 3338477, Prague/Osaka use 5007716
121        if let Some(current_excess_blob_gas) = self.env.current_excess_blob_gas {
122            block.set_blob_excess_gas_and_price(
123                current_excess_blob_gas.to(),
124                cfg.blob_base_fee_update_fraction(),
125            );
126        }
127
128        // Set default prevrandao for merge
129        if cfg.spec().is_enabled_in(SpecId::MERGE) && block.prevrandao.is_none() {
130            block.prevrandao = Some(B256::default());
131        }
132
133        block
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use context_interface::block::calc_blob_gasprice;
141    use primitives::{
142        eip4844::{BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE},
143        Address, U256,
144    };
145
146    /// Creates a minimal TestUnit with excess blob gas set for testing blob fee calculation
147    fn create_test_unit_with_excess_blob_gas(excess_blob_gas: u64) -> TestUnit {
148        TestUnit {
149            info: None,
150            env: Env {
151                current_chain_id: None,
152                current_coinbase: Address::ZERO,
153                current_difficulty: U256::ZERO,
154                current_gas_limit: U256::from(1_000_000u64),
155                current_number: U256::from(1u64),
156                current_timestamp: U256::from(1u64),
157                current_base_fee: Some(U256::from(1u64)),
158                previous_hash: None,
159                current_random: None,
160                current_beacon_root: None,
161                current_withdrawals_root: None,
162                current_excess_blob_gas: Some(U256::from(excess_blob_gas)),
163                slot_number: Some(U256::from(1u64)),
164            },
165            pre: AddressMap::default(),
166            post: BTreeMap::default(),
167            transaction: TransactionParts {
168                tx_type: None,
169                data: vec![],
170                gas_limit: vec![],
171                gas_price: None,
172                nonce: U256::ZERO,
173                secret_key: B256::ZERO,
174                sender: None,
175                to: None,
176                value: vec![],
177                max_fee_per_gas: None,
178                max_priority_fee_per_gas: None,
179                initcodes: None,
180                access_lists: vec![],
181                authorization_list: None,
182                blob_versioned_hashes: vec![],
183                max_fee_per_blob_gas: None,
184            },
185            out: None,
186        }
187    }
188
189    /// Test that block_env uses the correct blob base fee update fraction for Cancun
190    #[test]
191    fn test_block_env_blob_fee_fraction_cancun() {
192        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
193
194        let mut cfg = CfgEnv::new_with_spec(SpecId::CANCUN);
195
196        let block = unit.block_env(&mut cfg);
197
198        // Verify blob gas price is calculated with Cancun fraction
199        let blob_info = block
200            .blob_excess_gas_and_price
201            .expect("blob info should be set");
202        assert_eq!(blob_info.excess_blob_gas, 0x240000);
203
204        // Calculate expected price with Cancun fraction (3338477)
205        // blob_gasprice = fake_exponential(1, excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION)
206        // With excess_blob_gas=0x240000 and CANCUN fraction=3338477, price should be 2
207        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN);
208        assert_eq!(blob_info.blob_gasprice, expected_price);
209        assert_eq!(blob_info.blob_gasprice, 2); // With Cancun fraction, price is 2
210    }
211
212    /// Test that block_env uses the correct blob base fee update fraction for Prague
213    #[test]
214    fn test_block_env_blob_fee_fraction_prague() {
215        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
216
217        let mut cfg = CfgEnv::new_with_spec(SpecId::PRAGUE);
218
219        let block = unit.block_env(&mut cfg);
220
221        // Verify blob gas price is calculated with Prague fraction
222        let blob_info = block
223            .blob_excess_gas_and_price
224            .expect("blob info should be set");
225        assert_eq!(blob_info.excess_blob_gas, 0x240000);
226
227        // Calculate expected price with Prague fraction (5007716)
228        // With excess_blob_gas=0x240000 and PRAGUE fraction=5007716, price should be 1
229        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
230        assert_eq!(blob_info.blob_gasprice, expected_price);
231        assert_eq!(blob_info.blob_gasprice, 1); // With Prague fraction, price is 1
232    }
233
234    /// Test that block_env uses the correct blob base fee update fraction for Osaka
235    #[test]
236    fn test_block_env_blob_fee_fraction_osaka() {
237        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
238
239        let mut cfg = CfgEnv::new_with_spec(SpecId::OSAKA);
240
241        let block = unit.block_env(&mut cfg);
242
243        // Osaka should use Prague fraction (same as Prague)
244        let blob_info = block
245            .blob_excess_gas_and_price
246            .expect("blob info should be set");
247        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
248        assert_eq!(blob_info.blob_gasprice, expected_price);
249        assert_eq!(blob_info.blob_gasprice, 1); // With Prague fraction, price is 1
250    }
251
252    /// Test that demonstrates the bug scenario from IMPLEMENTATION_PROMPT.md
253    /// With excess_blob_gas=0x240000 and maxFeePerBlobGas=0x01:
254    /// - Cancun fraction (3338477): blob_price = 2, tx FAILS (insufficient fee)
255    /// - Prague fraction (5007716): blob_price = 1, tx SUCCEEDS
256    #[test]
257    fn test_blob_fee_difference_affects_tx_validity() {
258        let excess_blob_gas = 0x240000u64;
259
260        // Calculate prices with both fractions
261        let cancun_price =
262            calc_blob_gasprice(excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN);
263        let prague_price =
264            calc_blob_gasprice(excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
265
266        // Verify the prices are different
267        assert_eq!(cancun_price, 2, "Cancun blob price should be 2");
268        assert_eq!(prague_price, 1, "Prague blob price should be 1");
269
270        // With maxFeePerBlobGas=1:
271        // - Cancun: 1 < 2, tx would fail with insufficient fee
272        // - Prague: 1 >= 1, tx would succeed
273        let max_fee_per_blob_gas = 1u128;
274        assert!(
275            max_fee_per_blob_gas < cancun_price,
276            "Tx should fail with Cancun fraction"
277        );
278        assert!(
279            max_fee_per_blob_gas >= prague_price,
280            "Tx should succeed with Prague fraction"
281        );
282    }
283}