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}