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    /// Bytecode is stored separately from the account (in the contracts map, keyed by
64    /// code hash) so that execution has to fetch it through `Database::code_by_hash`,
65    /// like a node's state provider would serve it.
66    ///
67    /// # Returns
68    ///
69    /// A [`CacheState`] object containing the pre-state accounts, storages and contracts.
70    pub fn state(&self) -> CacheState {
71        let mut cache_state = CacheState::new();
72        for (address, info) in &self.pre {
73            let code_hash = keccak256(&info.code);
74            if !info.code.is_empty() {
75                let bytecode = Bytecode::new_raw_checked(info.code.clone())
76                    .unwrap_or(Bytecode::new_legacy(info.code.clone()));
77                cache_state.contracts.insert(code_hash, bytecode);
78            }
79            let acc_info = state::AccountInfo {
80                balance: info.balance,
81                code_hash,
82                code: None,
83                nonce: info.nonce,
84                ..Default::default()
85            };
86            cache_state.insert_account_with_storage(*address, acc_info, info.storage.clone());
87        }
88        cache_state
89    }
90
91    /// Create a block environment from the test unit.
92    ///
93    /// This function sets up the block environment using the current test unit's
94    /// environment settings and the provided configuration.
95    ///
96    /// # Arguments
97    ///
98    /// * `cfg` - The configuration environment containing spec and blob settings
99    ///
100    /// # Returns
101    ///
102    /// A configured [`BlockEnv`] ready for execution
103    pub fn block_env(&self, cfg: &mut CfgEnv) -> BlockEnv {
104        let mut block = BlockEnv {
105            number: self.env.current_number,
106            beneficiary: self.env.current_coinbase,
107            timestamp: self.env.current_timestamp,
108            gas_limit: self.env.current_gas_limit.try_into().unwrap_or(u64::MAX),
109            basefee: self
110                .env
111                .current_base_fee
112                .unwrap_or_default()
113                .try_into()
114                .unwrap_or(u64::MAX),
115            difficulty: self.env.current_difficulty,
116            prevrandao: self.env.current_random,
117            slot_num: self
118                .env
119                .slot_number
120                .unwrap_or_default()
121                .try_into()
122                .unwrap_or(u64::MAX),
123            ..BlockEnv::default()
124        };
125
126        // Handle EIP-4844 blob gas
127        // Use spec-aware blob fee fraction: Cancun uses 3338477, Prague/Osaka use 5007716
128        if let Some(current_excess_blob_gas) = self.env.current_excess_blob_gas {
129            block.set_blob_excess_gas_and_price(
130                current_excess_blob_gas.to(),
131                cfg.blob_base_fee_update_fraction(),
132            );
133        }
134
135        // Set default prevrandao for merge
136        if cfg.spec().is_enabled_in(SpecId::MERGE) && block.prevrandao.is_none() {
137            block.prevrandao = Some(B256::default());
138        }
139
140        block
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use context_interface::block::calc_blob_gasprice;
148    use primitives::{
149        eip4844::{BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE},
150        Address, U256,
151    };
152
153    /// Creates a minimal TestUnit with excess blob gas set for testing blob fee calculation
154    fn create_test_unit_with_excess_blob_gas(excess_blob_gas: u64) -> TestUnit {
155        TestUnit {
156            info: None,
157            env: Env {
158                current_chain_id: None,
159                current_coinbase: Address::ZERO,
160                current_difficulty: U256::ZERO,
161                current_gas_limit: U256::from(1_000_000u64),
162                current_number: U256::from(1u64),
163                current_timestamp: U256::from(1u64),
164                current_base_fee: Some(U256::from(1u64)),
165                previous_hash: None,
166                current_random: None,
167                current_beacon_root: None,
168                current_withdrawals_root: None,
169                current_excess_blob_gas: Some(U256::from(excess_blob_gas)),
170                slot_number: Some(U256::from(1u64)),
171            },
172            pre: AddressMap::default(),
173            post: BTreeMap::default(),
174            transaction: TransactionParts {
175                tx_type: None,
176                chain_id: None,
177                data: vec![],
178                gas_limit: vec![],
179                gas_price: None,
180                nonce: U256::ZERO,
181                secret_key: Some(B256::ZERO),
182                sender: None,
183                to: None,
184                value: vec![],
185                max_fee_per_gas: None,
186                max_priority_fee_per_gas: None,
187                initcodes: None,
188                access_lists: vec![],
189                authorization_list: None,
190                blob_versioned_hashes: vec![],
191                max_fee_per_blob_gas: None,
192            },
193            out: None,
194        }
195    }
196
197    /// Test that block_env uses the correct blob base fee update fraction for Cancun
198    #[test]
199    fn test_block_env_blob_fee_fraction_cancun() {
200        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
201
202        let mut cfg = CfgEnv::new_with_spec(SpecId::CANCUN);
203
204        let block = unit.block_env(&mut cfg);
205
206        // Verify blob gas price is calculated with Cancun fraction
207        let blob_info = block
208            .blob_excess_gas_and_price
209            .expect("blob info should be set");
210        assert_eq!(blob_info.excess_blob_gas, 0x240000);
211
212        // Calculate expected price with Cancun fraction (3338477)
213        // blob_gasprice = fake_exponential(1, excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION)
214        // With excess_blob_gas=0x240000 and CANCUN fraction=3338477, price should be 2
215        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN);
216        assert_eq!(blob_info.blob_gasprice, expected_price);
217        assert_eq!(blob_info.blob_gasprice, 2); // With Cancun fraction, price is 2
218    }
219
220    /// Test that block_env uses the correct blob base fee update fraction for Prague
221    #[test]
222    fn test_block_env_blob_fee_fraction_prague() {
223        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
224
225        let mut cfg = CfgEnv::new_with_spec(SpecId::PRAGUE);
226
227        let block = unit.block_env(&mut cfg);
228
229        // Verify blob gas price is calculated with Prague fraction
230        let blob_info = block
231            .blob_excess_gas_and_price
232            .expect("blob info should be set");
233        assert_eq!(blob_info.excess_blob_gas, 0x240000);
234
235        // Calculate expected price with Prague fraction (5007716)
236        // With excess_blob_gas=0x240000 and PRAGUE fraction=5007716, price should be 1
237        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
238        assert_eq!(blob_info.blob_gasprice, expected_price);
239        assert_eq!(blob_info.blob_gasprice, 1); // With Prague fraction, price is 1
240    }
241
242    /// Test that block_env uses the correct blob base fee update fraction for Osaka
243    #[test]
244    fn test_block_env_blob_fee_fraction_osaka() {
245        let unit = create_test_unit_with_excess_blob_gas(0x240000); // 2,359,296
246
247        let mut cfg = CfgEnv::new_with_spec(SpecId::OSAKA);
248
249        let block = unit.block_env(&mut cfg);
250
251        // Osaka should use Prague fraction (same as Prague)
252        let blob_info = block
253            .blob_excess_gas_and_price
254            .expect("blob info should be set");
255        let expected_price = calc_blob_gasprice(0x240000, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
256        assert_eq!(blob_info.blob_gasprice, expected_price);
257        assert_eq!(blob_info.blob_gasprice, 1); // With Prague fraction, price is 1
258    }
259
260    /// Test that demonstrates the bug scenario from IMPLEMENTATION_PROMPT.md
261    /// With excess_blob_gas=0x240000 and maxFeePerBlobGas=0x01:
262    /// - Cancun fraction (3338477): blob_price = 2, tx FAILS (insufficient fee)
263    /// - Prague fraction (5007716): blob_price = 1, tx SUCCEEDS
264    #[test]
265    fn test_blob_fee_difference_affects_tx_validity() {
266        let excess_blob_gas = 0x240000u64;
267
268        // Calculate prices with both fractions
269        let cancun_price =
270            calc_blob_gasprice(excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN);
271        let prague_price =
272            calc_blob_gasprice(excess_blob_gas, BLOB_BASE_FEE_UPDATE_FRACTION_PRAGUE);
273
274        // Verify the prices are different
275        assert_eq!(cancun_price, 2, "Cancun blob price should be 2");
276        assert_eq!(prague_price, 1, "Prague blob price should be 1");
277
278        // With maxFeePerBlobGas=1:
279        // - Cancun: 1 < 2, tx would fail with insufficient fee
280        // - Prague: 1 >= 1, tx would succeed
281        let max_fee_per_blob_gas = 1u128;
282        assert!(
283            max_fee_per_blob_gas < cancun_price,
284            "Tx should fail with Cancun fraction"
285        );
286        assert!(
287            max_fee_per_blob_gas >= prague_price,
288            "Tx should succeed with Prague fraction"
289        );
290    }
291}