revm_statetest_types/
test_unit.rs

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