arbos/l2_pricing/
model.rs

1use alloy_primitives::U256;
2use arb_primitives::multigas::{MultiGas, ResourceKind, NUM_RESOURCE_KIND};
3use revm::Database;
4
5use arb_chainspec::arbos_version as version;
6
7use super::L2PricingState;
8
9/// Which gas pricing model to use.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum GasModel {
12    Unknown,
13    Legacy,
14    SingleGasConstraints,
15    MultiGasConstraints,
16}
17
18/// Whether a backlog update grows or shrinks the backlog.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum BacklogOperation {
21    Shrink,
22    Grow,
23}
24
25// Initial constants for pricing model.
26// StorageReadCost (SloadGasEIP2200 = 800) + StorageWriteCost (SstoreSetGasEIP2200 = 20000)
27pub const MULTI_CONSTRAINT_STATIC_BACKLOG_UPDATE_COST: u64 = 20_800;
28
29impl<D: Database> L2PricingState<D> {
30    /// Determine which gas model to use based on ArbOS version and stored constraints.
31    pub fn gas_model_to_use(&self) -> Result<GasModel, ()> {
32        if self.arbos_version >= version::ARBOS_VERSION_60 {
33            let mgc_len = self.multi_gas_constraints_length()?;
34            if mgc_len > 0 {
35                return Ok(GasModel::MultiGasConstraints);
36            }
37        }
38        if self.arbos_version >= version::ARBOS_VERSION_50 {
39            let gc_len = self.gas_constraints_length()?;
40            if gc_len > 0 {
41                return Ok(GasModel::SingleGasConstraints);
42            }
43        }
44        Ok(GasModel::Legacy)
45    }
46
47    /// Grow the gas backlog for the active pricing model.
48    pub fn grow_backlog(&self, used_gas: u64, used_multi_gas: MultiGas) -> Result<(), ()> {
49        self.update_backlog(BacklogOperation::Grow, used_gas, used_multi_gas)
50    }
51
52    /// Shrink the gas backlog for the active pricing model.
53    pub fn shrink_backlog(&self, used_gas: u64, used_multi_gas: MultiGas) -> Result<(), ()> {
54        self.update_backlog(BacklogOperation::Shrink, used_gas, used_multi_gas)
55    }
56
57    /// Dispatch backlog update to the active pricing model.
58    fn update_backlog(
59        &self,
60        op: BacklogOperation,
61        used_gas: u64,
62        used_multi_gas: MultiGas,
63    ) -> Result<(), ()> {
64        match self.gas_model_to_use()? {
65            GasModel::Legacy | GasModel::Unknown => self.update_legacy_backlog_op(op, used_gas),
66            GasModel::SingleGasConstraints => {
67                self.update_single_gas_constraints_backlogs_op(op, used_gas)
68            }
69            GasModel::MultiGasConstraints => {
70                self.update_multi_gas_constraints_backlogs_op(op, used_multi_gas)
71            }
72        }
73    }
74
75    fn update_legacy_backlog_op(&self, op: BacklogOperation, gas: u64) -> Result<(), ()> {
76        let backlog = self.gas_backlog()?;
77        let new_backlog = apply_gas_delta_op(op, backlog, gas);
78        self.set_gas_backlog(new_backlog)
79    }
80
81    fn update_single_gas_constraints_backlogs_op(
82        &self,
83        op: BacklogOperation,
84        gas: u64,
85    ) -> Result<(), ()> {
86        let len = self.gas_constraints_length()?;
87        for i in 0..len {
88            let c = self.open_gas_constraint_at(i);
89            let backlog = c.backlog()?;
90            c.set_backlog(apply_gas_delta_op(op, backlog, gas))?;
91        }
92        Ok(())
93    }
94
95    fn update_multi_gas_constraints_backlogs_op(
96        &self,
97        op: BacklogOperation,
98        multi_gas: MultiGas,
99    ) -> Result<(), ()> {
100        let len = self.multi_gas_constraints_length()?;
101        for i in 0..len {
102            let c = self.open_multi_gas_constraint_at(i);
103            match op {
104                BacklogOperation::Grow => c.grow_backlog(multi_gas)?,
105                BacklogOperation::Shrink => c.shrink_backlog(multi_gas)?,
106            }
107        }
108        Ok(())
109    }
110
111    /// Update the pricing model for a new block.
112    pub fn update_pricing_model(&self, time_passed: u64, arbos_version: u64) -> Result<(), ()> {
113        let _ = arbos_version; // version gating handled by gas_model_to_use via self.arbos_version
114        match self.gas_model_to_use()? {
115            GasModel::Legacy | GasModel::Unknown => self.update_pricing_model_legacy(time_passed),
116            GasModel::SingleGasConstraints => {
117                self.update_pricing_model_single_constraints(time_passed)
118            }
119            GasModel::MultiGasConstraints => {
120                self.update_pricing_model_multi_constraints(time_passed)
121            }
122        }
123    }
124
125    fn update_pricing_model_legacy(&self, time_passed: u64) -> Result<(), ()> {
126        let speed_limit = self.speed_limit_per_second()?;
127        let drain = time_passed.saturating_mul(speed_limit);
128        self.update_legacy_backlog_op(BacklogOperation::Shrink, drain)?;
129
130        let inertia = self.pricing_inertia()?;
131        let tolerance = self.backlog_tolerance()?;
132        let backlog = self.gas_backlog()?;
133        let min_base_fee = self.min_base_fee_wei()?;
134
135        // Plain `tolerance * speedLimit` (wrapping on overflow).
136        let tolerance_limit = tolerance.wrapping_mul(speed_limit);
137        let base_fee = if backlog > tolerance_limit {
138            // Divisor: SaturatingUMul(inertia, speedLimit).
139            // Guard against division by zero (speed_limit/inertia are validated nonzero by
140            // ArbOwner).
141            let divisor = saturating_cast_to_i64(inertia.saturating_mul(speed_limit));
142            if divisor == 0 {
143                return self.set_base_fee_wei(min_base_fee);
144            }
145            // SaturatingCast]int64\](backlog - tolerance*speedLimit)
146            let excess = saturating_cast_to_i64(backlog.wrapping_sub(tolerance_limit));
147            // NaturalToBips(excess) / SaturatingCastToBips(SaturatingUMul(inertia, speedLimit))
148            let exponent_bips = natural_to_bips(excess) / divisor;
149            // BigMulByBips(minBaseFee, ApproxExpBasisPoints(exponentBips, 4))
150            self.calc_base_fee_from_exponent(exponent_bips.max(0) as u64)?
151        } else {
152            min_base_fee
153        };
154
155        self.set_base_fee_wei(base_fee)
156    }
157
158    fn update_pricing_model_single_constraints(&self, time_passed: u64) -> Result<(), ()> {
159        // Drain backlogs and compute total exponent (sum across all constraints).
160        // Uses signed Bips (int64) arithmetic matching Go.
161        let mut total_exponent: i64 = 0;
162        let len = self.gas_constraints_length()?;
163
164        for i in 0..len {
165            let c = self.open_gas_constraint_at(i);
166            let target = c.target()?;
167
168            // Pay off backlog: gas = SaturatingUMul(timePassed, target)
169            let backlog = c.backlog()?;
170            let gas = time_passed.saturating_mul(target);
171            let new_backlog = backlog.saturating_sub(gas);
172            c.set_backlog(new_backlog)?;
173
174            // Calculate exponent with the formula backlog/divisor
175            if new_backlog > 0 {
176                let window = c.adjustment_window()?;
177                // divisor = SaturatingCastToBips(SaturatingUMul(inertia, target))
178                let divisor = saturating_cast_to_i64(window.saturating_mul(target));
179                if divisor != 0 {
180                    // NaturalToBips(SaturatingCast]int64\](backlog))
181                    let exponent = natural_to_bips(saturating_cast_to_i64(new_backlog)) / divisor;
182                    total_exponent = total_exponent.saturating_add(exponent);
183                }
184            }
185        }
186
187        let base_fee = self.calc_base_fee_from_exponent(total_exponent.max(0) as u64)?;
188        self.set_base_fee_wei(base_fee)
189    }
190
191    fn update_pricing_model_multi_constraints(&self, time_passed: u64) -> Result<(), ()> {
192        self.update_multi_gas_constraints_backlogs(time_passed)?;
193
194        let exponent_per_kind = self.calc_multi_gas_constraints_exponents()?;
195
196        // Compute base fee per resource kind, store as next-block fee,
197        // and track the maximum for the overall base fee.
198        let mut max_base_fee = self.min_base_fee_wei()?;
199        let fees = &self.multi_gas_base_fees;
200
201        for (i, &exp) in exponent_per_kind.iter().enumerate() {
202            let base_fee = self.calc_base_fee_from_exponent(exp)?;
203            if let Some(kind) = ResourceKind::from_u8(i as u8) {
204                let mgf = super::multi_gas_fees::open_multi_gas_fees(fees.clone());
205                mgf.set_next_block_fee(kind, base_fee)?;
206            }
207            if base_fee > max_base_fee {
208                max_base_fee = base_fee;
209            }
210        }
211
212        self.set_base_fee_wei(max_base_fee)
213    }
214
215    fn update_multi_gas_constraints_backlogs(&self, time_passed: u64) -> Result<(), ()> {
216        let len = self.multi_gas_constraints_length()?;
217        for i in 0..len {
218            let c = self.open_multi_gas_constraint_at(i);
219            let target = c.target()?;
220            let backlog = c.backlog()?;
221            let gas = time_passed.saturating_mul(target);
222            let new_backlog = backlog.saturating_sub(gas);
223            c.set_backlog(new_backlog)?;
224        }
225        Ok(())
226    }
227
228    /// Calculate exponent (in basis points) per resource kind across all constraints.
229    ///
230    /// Aggregates weighted backlog contributions from each constraint into
231    /// a per-resource-kind exponent array.
232    ///
233    /// Uses signed saturation arithmetic with Bips (int64) computation:
234    /// dividend = NaturalToBips(SaturatingCast]int64\](SaturatingUMul(backlog, weight)))
235    /// divisor  = SaturatingCastToBips(SaturatingUMul(window, SaturatingUMul(target, maxWeight)))
236    /// exp      = dividend / divisor  (signed int64 division)
237    pub fn calc_multi_gas_constraints_exponents(&self) -> Result<[u64; NUM_RESOURCE_KIND], ()> {
238        let len = self.multi_gas_constraints_length()?;
239        let mut exponent_per_kind = [0i64; NUM_RESOURCE_KIND];
240
241        for i in 0..len {
242            let c = self.open_multi_gas_constraint_at(i);
243            let target = c.target()?;
244            let backlog = c.backlog()?;
245
246            if backlog == 0 {
247                continue;
248            }
249
250            let window = c.adjustment_window()?;
251            let max_weight = c.max_weight()?;
252
253            if target == 0 || window == 0 || max_weight == 0 {
254                continue;
255            }
256
257            // divisor = SaturatingCastToBips(SaturatingUMul(window, SaturatingUMul(target,
258            // maxWeight)))
259            let divisor_u64 = (window as u64).saturating_mul(target.saturating_mul(max_weight));
260            let divisor = saturating_cast_to_i64(divisor_u64);
261            if divisor == 0 {
262                continue;
263            }
264
265            for kind in ResourceKind::ALL {
266                let weight = c.resource_weight(kind)?;
267                if weight == 0 {
268                    continue;
269                }
270
271                // dividend = NaturalToBips(SaturatingCast]int64\](SaturatingUMul(backlog,
272                // weight)))
273                let product = backlog.saturating_mul(weight);
274                let cast = saturating_cast_to_i64(product);
275                let dividend = natural_to_bips(cast);
276
277                let exp = dividend / divisor;
278                exponent_per_kind[kind as usize] =
279                    exponent_per_kind[kind as usize].saturating_add(exp);
280            }
281        }
282
283        // Convert back to u64 for the caller (exponents are always non-negative).
284        let mut result = [0u64; NUM_RESOURCE_KIND];
285        for i in 0..NUM_RESOURCE_KIND {
286            result[i] = exponent_per_kind[i].max(0) as u64;
287        }
288        Ok(result)
289    }
290
291    /// Calculate base fee from an exponent in basis points.
292    /// base_fee = min_base_fee * exp(exponent_bips / 10000)
293    pub fn calc_base_fee_from_exponent(&self, exponent_bips: u64) -> Result<U256, ()> {
294        let min_base_fee = self.min_base_fee_wei()?;
295        if exponent_bips == 0 {
296            return Ok(min_base_fee);
297        }
298
299        let exp_result = approx_exp_basis_points(exponent_bips);
300        let base_fee = (min_base_fee * U256::from(exp_result)) / U256::from(10000u64);
301
302        if base_fee < min_base_fee {
303            Ok(min_base_fee)
304        } else {
305            Ok(base_fee)
306        }
307    }
308
309    /// Get multi-gas current-block base fee per resource kind.
310    ///
311    /// L1Calldata kind is always forced to the global base fee,
312    /// and any zero fee is replaced with the global base fee.
313    pub fn get_multi_gas_base_fee_per_resource(&self) -> Result<[U256; NUM_RESOURCE_KIND], ()> {
314        let base_fee = self.base_fee_wei()?;
315        let mgf = super::multi_gas_fees::open_multi_gas_fees(self.multi_gas_base_fees.clone());
316        let mut fees = [U256::ZERO; NUM_RESOURCE_KIND];
317        for kind in ResourceKind::ALL {
318            // L1Calldata always uses the global base fee.
319            if kind == ResourceKind::L1Calldata {
320                fees[kind as usize] = base_fee;
321                continue;
322            }
323            let fee = mgf.get_current_block_fee(kind)?;
324            fees[kind as usize] = if fee.is_zero() { base_fee } else { fee };
325        }
326        Ok(fees)
327    }
328
329    /// Rotate next-block multi-gas fees into current-block fees.
330    ///
331    /// Called at block start before executing transactions.
332    pub fn commit_multi_gas_fees(&self) -> Result<(), ()> {
333        if self.gas_model_to_use()? != GasModel::MultiGasConstraints {
334            return Ok(());
335        }
336        let mgf = super::multi_gas_fees::open_multi_gas_fees(self.multi_gas_base_fees.clone());
337        mgf.commit_next_to_current()
338    }
339
340    /// Calculate the cost for a backlog update operation.
341    ///
342    /// Version-gated cost accounting:
343    /// - v60+: static cost (StorageReadCost + StorageWriteCost)
344    /// - v51+: overhead for single-gas constraint traversal
345    /// - v50+: base overhead for GasModelToUse() read
346    /// - legacy: read + write for backlog
347    pub fn backlog_update_cost(&self) -> Result<u64, ()> {
348        use super::{STORAGE_READ_COST, STORAGE_WRITE_COST};
349
350        // v60+: charge a flat static price regardless of gas model
351        if self.arbos_version >= version::ARBOS_VERSION_60 {
352            return Ok(MULTI_CONSTRAINT_STATIC_BACKLOG_UPDATE_COST);
353        }
354
355        let mut result = 0u64;
356
357        // v50+: overhead for reading gas constraints length in GasModelToUse()
358        if self.arbos_version >= version::ARBOS_VERSION_50 {
359            result += STORAGE_READ_COST;
360        }
361
362        // v51+ (multi-constraint fix): per-constraint read+write costs
363        if self.arbos_version >= version::ARBOS_VERSION_MULTI_CONSTRAINT_FIX {
364            let constraints_length = self.gas_constraints_length()?;
365            if constraints_length > 0 {
366                // Read length to traverse
367                result += STORAGE_READ_COST;
368                // Read + write backlog for each constraint
369                result += constraints_length * (STORAGE_READ_COST + STORAGE_WRITE_COST);
370                return Ok(result);
371            }
372            // No return here -- fallthrough to legacy costs
373        }
374
375        // Legacy pricer: single read + write
376        result += STORAGE_READ_COST + STORAGE_WRITE_COST;
377
378        Ok(result)
379    }
380
381    /// Set gas constraints from legacy parameters (for upgrades).
382    pub fn set_gas_constraints_from_legacy(&self) -> Result<(), ()> {
383        self.clear_gas_constraints()?;
384        let target = self.speed_limit_per_second()?;
385        let adjustment_window = self.pricing_inertia()?;
386        let old_backlog = self.gas_backlog()?;
387        let backlog_tolerance = self.backlog_tolerance()?;
388
389        let backlog = old_backlog.saturating_sub(backlog_tolerance.saturating_mul(target));
390        self.add_gas_constraint(target, adjustment_window, backlog)
391    }
392
393    /// Convert single-gas constraints to multi-gas constraints (for upgrades).
394    ///
395    /// Iterates existing single-gas constraints, reads their target/window/backlog,
396    /// and creates corresponding multi-gas constraints with equal weights across
397    /// all resource dimensions.
398    pub fn set_multi_gas_constraints_from_single_gas_constraints(&self) -> Result<(), ()> {
399        self.clear_multi_gas_constraints()?;
400
401        let length = self.gas_constraints_length()?;
402
403        for i in 0..length {
404            let c = self.open_gas_constraint_at(i);
405
406            let target = c.target()?;
407            let window = c.adjustment_window()?;
408            let backlog = c.backlog()?;
409
410            // Equal weights for all resource kinds.
411            let weights = [1u64; NUM_RESOURCE_KIND];
412
413            // Cap adjustment_window to u32::MAX.
414            let adjustment_window: u32 = if window > u32::MAX as u64 {
415                u32::MAX
416            } else {
417                window as u32
418            };
419
420            self.add_multi_gas_constraint(target, adjustment_window, backlog, &weights)?;
421        }
422        Ok(())
423    }
424
425    /// Compute total cost for a multi-gas usage, for refund calculations.
426    ///
427    /// Returns `sum(gas_used[kind] * base_fee[kind])` across all resource kinds.
428    pub fn multi_dimensional_price_for_refund(&self, gas_used: MultiGas) -> Result<U256, ()> {
429        let fees = self.get_multi_gas_base_fee_per_resource()?;
430        let mut total = U256::ZERO;
431        for kind in ResourceKind::ALL {
432            let amount = gas_used.get(kind);
433            if amount == 0 {
434                continue;
435            }
436            total = total.saturating_add(U256::from(amount).saturating_mul(fees[kind as usize]));
437        }
438        Ok(total)
439    }
440}
441
442/// Approximate e^(x/10000) * 10000 using Horner's method (degree 4).
443///
444/// Matches `ApproxExpBasisPoints(value, 4)` exactly.
445fn approx_exp_basis_points(bips: u64) -> u64 {
446    const ACCURACY: u64 = 4;
447    const B: u64 = 10_000; // OneInBips
448
449    if bips == 0 {
450        return B;
451    }
452
453    // Horner's method: b*(1 + x/b*(1 + x/(2b)*(1 + x/(3b))))
454    let mut res = B.saturating_add(bips / ACCURACY);
455    let mut i = ACCURACY - 1;
456    while i > 0 {
457        res = B.saturating_add(res.saturating_mul(bips) / (i * B));
458        i -= 1;
459    }
460
461    res
462}
463
464/// Saturating cast from u64 to i64, capping at i64::MAX.
465fn saturating_cast_to_i64(value: u64) -> i64 {
466    if value > i64::MAX as u64 {
467        i64::MAX
468    } else {
469        value as i64
470    }
471}
472
473/// Convert a natural number to basis points (multiply by 10000), saturating.
474fn natural_to_bips(natural: i64) -> i64 {
475    natural.saturating_mul(10000)
476}
477
478/// Apply a gas delta to a backlog value (signed).
479pub fn apply_gas_delta(backlog: u64, delta: i64) -> u64 {
480    if delta > 0 {
481        backlog.saturating_add(delta as u64)
482    } else {
483        backlog.saturating_sub((-delta) as u64)
484    }
485}
486
487/// Apply a gas delta with a backlog operation.
488fn apply_gas_delta_op(op: BacklogOperation, backlog: u64, delta: u64) -> u64 {
489    match op {
490        BacklogOperation::Grow => backlog.saturating_add(delta),
491        BacklogOperation::Shrink => backlog.saturating_sub(delta),
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use alloy_primitives::{address, keccak256, Address, B256, U256};
498    use arb_primitives::multigas::MultiGas;
499    use arb_storage::Storage;
500    use revm::{database::StateBuilder, Database};
501
502    const ARBOS_STATE_ADDRESS: Address = address!("A4B05FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
503
504    #[derive(Default)]
505    struct EmptyDb;
506
507    impl Database for EmptyDb {
508        type Error = std::convert::Infallible;
509        fn basic(
510            &mut self,
511            _address: Address,
512        ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
513            Ok(None)
514        }
515        fn code_by_hash(&mut self, _code_hash: B256) -> Result<revm::state::Bytecode, Self::Error> {
516            Ok(revm::state::Bytecode::default())
517        }
518        fn storage(&mut self, _address: Address, _index: U256) -> Result<U256, Self::Error> {
519            Ok(U256::ZERO)
520        }
521        fn block_hash(&mut self, _number: u64) -> Result<B256, Self::Error> {
522            Ok(B256::ZERO)
523        }
524    }
525
526    /// Create ArbOS account in the cache if it doesn't exist.
527    fn ensure_cache_account(state: &mut revm::database::State<EmptyDb>, addr: Address) {
528        use revm::database::{states::account_status::AccountStatus, PlainAccount};
529
530        let _ = state.load_cache_account(addr);
531        if let Some(cached) = state.cache.accounts.get_mut(&addr) {
532            if cached.account.is_none() {
533                cached.account = Some(PlainAccount {
534                    info: revm::state::AccountInfo {
535                        balance: U256::ZERO,
536                        nonce: 0,
537                        code_hash: keccak256([]),
538                        code: None,
539                        account_id: None,
540                    },
541                    storage: Default::default(),
542                });
543                cached.status = AccountStatus::InMemoryChange;
544            }
545        }
546    }
547
548    #[test]
549    fn test_grow_backlog_through_l2_pricing_state() {
550        let mut state = StateBuilder::new()
551            .with_database(EmptyDb)
552            .with_bundle_update()
553            .build();
554
555        // Ensure ArbOS account exists with nonce=1
556        ensure_cache_account(&mut state, ARBOS_STATE_ADDRESS);
557        arb_storage::set_account_nonce(&mut state, ARBOS_STATE_ADDRESS, 1);
558
559        let state_ptr: *mut revm::database::State<EmptyDb> = &mut state;
560
561        // Create L2 pricing storage (subspace [1] off root)
562        let backing = Storage::new(state_ptr, B256::ZERO);
563        let l2_sto = backing.open_sub_storage(&[1]);
564
565        // Initialize L2 pricing state
566        super::super::initialize_l2_pricing_state(&l2_sto);
567
568        // Verify gasBacklog starts at 0
569        let l2_pricing = super::super::open_l2_pricing_state(
570            backing.open_sub_storage(&[1]),
571            10, // ArbOS v10
572        );
573        let initial_backlog = l2_pricing.gas_backlog().unwrap();
574        assert_eq!(initial_backlog, 0, "Initial gasBacklog should be 0");
575
576        // Grow backlog by 100000 gas
577        let result = l2_pricing.grow_backlog(100_000, MultiGas::default());
578        assert!(result.is_ok(), "grow_backlog should succeed");
579
580        // Verify gasBacklog is now 100000
581        let after_grow = l2_pricing.gas_backlog().unwrap();
582        assert_eq!(
583            after_grow, 100_000,
584            "gasBacklog should be 100000 after grow"
585        );
586
587        // Grow again by 50000
588        let result = l2_pricing.grow_backlog(50_000, MultiGas::default());
589        assert!(result.is_ok(), "second grow_backlog should succeed");
590
591        let after_second_grow = l2_pricing.gas_backlog().unwrap();
592        assert_eq!(
593            after_second_grow, 150_000,
594            "gasBacklog should be 150000 after second grow"
595        );
596
597        // Shrink by 30000
598        let result = l2_pricing.shrink_backlog(30_000, MultiGas::default());
599        assert!(result.is_ok(), "shrink_backlog should succeed");
600
601        let after_shrink = l2_pricing.gas_backlog().unwrap();
602        assert_eq!(
603            after_shrink, 120_000,
604            "gasBacklog should be 120000 after shrink"
605        );
606
607        // Verify bundle contains the gasBacklog change
608        use revm::database::states::bundle_state::BundleRetention;
609        state.merge_transitions(BundleRetention::Reverts);
610        let bundle = state.take_bundle();
611
612        let acct = bundle
613            .state
614            .get(&ARBOS_STATE_ADDRESS)
615            .expect("ArbOS account should be in bundle");
616
617        // The gasBacklog slot should be in the bundle storage
618        // Compute the expected slot
619        let l2_base = keccak256([1u8]); // open_sub_storage([1]) from root
620        let gas_backlog_offset: u64 = 4;
621        let slot = arb_storage::storage_key_map(l2_base.as_slice(), gas_backlog_offset);
622
623        let bundle_slot = acct
624            .storage
625            .get(&slot)
626            .expect("gasBacklog slot should be in bundle");
627        assert_eq!(
628            bundle_slot.present_value,
629            U256::from(120_000u64),
630            "Bundle should contain final gasBacklog value"
631        );
632    }
633
634    /// Reproduces the exact block 616862 Arbitrum Sepolia divergence scenario.
635    ///
636    /// Block 616862 contains:
637    ///   TX0: StartBlock internal (drain=0 because time_passed=0)
638    ///   TX1: SubmitRetryable (writes many ArbOS retryable storage slots, empty EVM commit)
639    ///   TX2: RetryTx auto-redeem (complex EVM commit with many accounts, then grow_backlog)
640    ///
641    /// The gasBacklog starts at 552,756 (pre-block state). After grow_backlog(357,751),
642    /// it should be 910,507. The bug: this write is lost from the bundle.
643    ///
644    /// This test uses a PreloadedDb so that write_storage_at sees the correct
645    /// `original_value` from the database (matching production behavior).
646    #[test]
647    fn test_block_616862_backlog_survives_full_flow() {
648        use alloy_primitives::map::HashMap;
649        use revm::{database::states::bundle_state::BundleRetention, DatabaseCommit};
650
651        // --- Compute the real ArbOS slot addresses ---
652        let l2_base = keccak256([1u8]); // L2 pricing subspace key
653        let gas_backlog_slot = arb_storage::storage_key_map(l2_base.as_slice(), 4);
654        let speed_limit_slot = arb_storage::storage_key_map(l2_base.as_slice(), 0);
655        let per_block_gas_limit_slot = arb_storage::storage_key_map(l2_base.as_slice(), 1);
656        let base_fee_slot = arb_storage::storage_key_map(l2_base.as_slice(), 2);
657        let min_base_fee_slot = arb_storage::storage_key_map(l2_base.as_slice(), 3);
658        let pricing_inertia_slot = arb_storage::storage_key_map(l2_base.as_slice(), 5);
659        let backlog_tolerance_slot = arb_storage::storage_key_map(l2_base.as_slice(), 6);
660
661        // ArbOS version slot (offset 0 from root key = B256::ZERO)
662        let version_slot = arb_storage::storage_key_map(&[], 0);
663
664        // --- PreloadedDb: returns realistic pre-block values for ArbOS storage ---
665        struct PreloadedDb {
666            slots: HashMap<(Address, U256), U256>,
667        }
668
669        impl PreloadedDb {
670            fn new() -> Self {
671                Self {
672                    slots: HashMap::default(),
673                }
674            }
675            fn set(&mut self, addr: Address, slot: U256, val: U256) {
676                self.slots.insert((addr, slot), val);
677            }
678        }
679
680        impl Database for PreloadedDb {
681            type Error = std::convert::Infallible;
682            fn basic(
683                &mut self,
684                addr: Address,
685            ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
686                if addr == ARBOS_STATE_ADDRESS {
687                    Ok(Some(revm::state::AccountInfo {
688                        nonce: 1,
689                        balance: U256::ZERO,
690                        code_hash: keccak256([]),
691                        code: None,
692                        account_id: None,
693                    }))
694                } else {
695                    Ok(None)
696                }
697            }
698            fn code_by_hash(&mut self, _: B256) -> Result<revm::state::Bytecode, Self::Error> {
699                Ok(revm::state::Bytecode::default())
700            }
701            fn storage(&mut self, addr: Address, index: U256) -> Result<U256, Self::Error> {
702                Ok(self
703                    .slots
704                    .get(&(addr, index))
705                    .copied()
706                    .unwrap_or(U256::ZERO))
707            }
708            fn block_hash(&mut self, _: u64) -> Result<B256, Self::Error> {
709                Ok(B256::ZERO)
710            }
711        }
712
713        let arbos = ARBOS_STATE_ADDRESS;
714
715        // Pre-block state matching Arbitrum Sepolia at block 616861
716        let mut db = PreloadedDb::new();
717        db.set(arbos, gas_backlog_slot, U256::from(552_756u64));
718        db.set(arbos, speed_limit_slot, U256::from(7_000_000u64));
719        db.set(arbos, per_block_gas_limit_slot, U256::from(32_000_000u64));
720        db.set(arbos, base_fee_slot, U256::from(100_000_000u64));
721        db.set(arbos, min_base_fee_slot, U256::from(100_000_000u64));
722        db.set(arbos, pricing_inertia_slot, U256::from(102u64));
723        db.set(arbos, backlog_tolerance_slot, U256::from(10u64));
724        db.set(arbos, version_slot, U256::from(20u64)); // ArbOS v20
725
726        let mut state = StateBuilder::new()
727            .with_database(db)
728            .with_bundle_update()
729            .build();
730
731        let state_ptr: *mut revm::database::State<PreloadedDb> = &mut state;
732
733        // ================================================================
734        // TX0: StartBlock internal transaction
735        // ================================================================
736        // update_pricing_model(time_passed=0) → drain=0 → no-op write
737        {
738            let backing = Storage::new(state_ptr, B256::ZERO);
739            let l2_sto = backing.open_sub_storage(&[1]);
740            let l2_pricing = super::super::open_l2_pricing_state(l2_sto, 20);
741
742            // This should read gasBacklog=552756, drain 0, try to write 552756 → no-op
743            let result = l2_pricing.update_pricing_model(0, 20);
744            assert!(result.is_ok(), "update_pricing_model should succeed");
745
746            // Verify gasBacklog is still readable as 552756
747            let backlog = l2_pricing.gas_backlog().unwrap();
748            assert_eq!(
749                backlog, 552_756,
750                "gasBacklog should be 552756 after no-op drain"
751            );
752        }
753
754        // Commit empty EVM state for StartBlock (internal tx has no EVM changes)
755        let empty_changes: HashMap<Address, revm::state::Account> = Default::default();
756        state.commit(empty_changes);
757
758        eprintln!("[TX0] After StartBlock commit");
759
760        // ================================================================
761        // TX1: SubmitRetryable — writes many ArbOS storage slots
762        // ================================================================
763        // Simulate retryable creation writing ~10 storage slots to ArbOS
764        {
765            // These are approximate retryable storage slots (different subspace)
766            let retryable_base = keccak256([2u8]); // retryable subspace
767            for i in 0u64..10 {
768                let slot = arb_storage::storage_key_map(retryable_base.as_slice(), i);
769                arb_storage::write_storage_at(
770                    unsafe { &mut *state_ptr },
771                    arbos,
772                    slot,
773                    U256::from(1000 + i),
774                );
775            }
776
777            // Write scratch slots (poster_fee, retryable_id, redeemer)
778            let scratch_slot_1 = arb_storage::storage_key_map(&[], 5); // approximate
779            let scratch_slot_2 = arb_storage::storage_key_map(&[], 6);
780            let scratch_slot_3 = arb_storage::storage_key_map(&[], 7);
781            arb_storage::write_storage_at(
782                unsafe { &mut *state_ptr },
783                arbos,
784                scratch_slot_1,
785                U256::from(42),
786            );
787            arb_storage::write_storage_at(
788                unsafe { &mut *state_ptr },
789                arbos,
790                scratch_slot_2,
791                U256::from(43),
792            );
793            arb_storage::write_storage_at(
794                unsafe { &mut *state_ptr },
795                arbos,
796                scratch_slot_3,
797                U256::from(44),
798            );
799        }
800
801        // Commit empty EVM state for SubmitRetryable (endTxNow=true, no EVM execution)
802        let empty_changes2: HashMap<Address, revm::state::Account> = Default::default();
803        state.commit(empty_changes2);
804
805        // Clear scratch slots (as done in commit_transaction)
806        {
807            let scratch_slot_1 = arb_storage::storage_key_map(&[], 5);
808            let scratch_slot_2 = arb_storage::storage_key_map(&[], 6);
809            let scratch_slot_3 = arb_storage::storage_key_map(&[], 7);
810            arb_storage::write_arbos_storage(
811                unsafe { &mut *state_ptr },
812                scratch_slot_1,
813                U256::ZERO,
814            );
815            arb_storage::write_arbos_storage(
816                unsafe { &mut *state_ptr },
817                scratch_slot_2,
818                U256::ZERO,
819            );
820            arb_storage::write_arbos_storage(
821                unsafe { &mut *state_ptr },
822                scratch_slot_3,
823                U256::ZERO,
824            );
825        }
826
827        eprintln!("[TX1] After SubmitRetryable commit + scratch clear");
828
829        // ================================================================
830        // TX2: RetryTx — complex EVM commit, then grow_backlog
831        // ================================================================
832
833        // Write scratch slots for RetryTx
834        {
835            let scratch_slot_1 = arb_storage::storage_key_map(&[], 5);
836            let scratch_slot_2 = arb_storage::storage_key_map(&[], 6);
837            let scratch_slot_3 = arb_storage::storage_key_map(&[], 7);
838            arb_storage::write_storage_at(
839                unsafe { &mut *state_ptr },
840                arbos,
841                scratch_slot_1,
842                U256::from(99),
843            );
844            arb_storage::write_storage_at(
845                unsafe { &mut *state_ptr },
846                arbos,
847                scratch_slot_2,
848                U256::from(100),
849            );
850            arb_storage::write_storage_at(
851                unsafe { &mut *state_ptr },
852                arbos,
853                scratch_slot_3,
854                U256::from(101),
855            );
856        }
857
858        // Simulate complex EVM execution touching MANY accounts (11 logs, 7+ contracts)
859        // This is the key difference from block 616861 (which had only 2 logs)
860        {
861            let mut evm_changes: HashMap<Address, revm::state::Account> = Default::default();
862
863            // Sender account
864            let sender = address!("fd86e9a33fd52e4085fb94d24b759448a621cd36");
865            let _ = state.load_cache_account(sender);
866            let mut sender_acct = revm::state::Account::default();
867            sender_acct.info.balance = U256::from(1_000_000_000u64);
868            sender_acct.info.nonce = 1;
869            sender_acct.mark_touch();
870            evm_changes.insert(sender, sender_acct);
871
872            // Target contract + 6 sub-contracts (simulating 7 accounts from 11 logs)
873            let contracts = [
874                address!("4453d0eaf066a61c9b81ddc18bb5a2bf2fc52224"),
875                address!("7c7db13e5d385bcc797422d3c767856d15d24c5c"),
876                address!("0057892cb8bb5f1ce1b3c6f5ade899732249713f"),
877                address!("35aa95ac4747d928e2cd42fe4461f6d9d1826346"),
878                address!("e1e3b1cbacc870cb6e5f4bdf246feb6eb5cd351b"),
879                address!("7348fdf6f3e090c635b23d970945093455214f3b"),
880                address!("d50e4a971bc8ed55af6aebc0a2178456069e87b5"),
881            ];
882
883            for (i, &contract) in contracts.iter().enumerate() {
884                let _ = state.load_cache_account(contract);
885                let mut acct = revm::state::Account::default();
886                acct.info.nonce = 1;
887                acct.info.code_hash = keccak256(format!("code_{}", i).as_bytes());
888                acct.mark_touch();
889                // Add some storage changes to simulate real contract execution
890                for j in 0u64..3 {
891                    let slot = U256::from(j);
892                    let mut evm_slot =
893                        revm::state::EvmStorageSlot::new(U256::from(i as u64 * 100 + j), 0);
894                    evm_slot.present_value = U256::from(i as u64 * 100 + j + 1);
895                    acct.storage.insert(slot, evm_slot);
896                }
897                evm_changes.insert(contract, acct);
898            }
899
900            state.commit(evm_changes);
901        }
902
903        eprintln!("[TX2] After RetryTx EVM commit ({} accounts)", 8);
904
905        // Clear scratch slots
906        {
907            let scratch_slot_1 = arb_storage::storage_key_map(&[], 5);
908            let scratch_slot_2 = arb_storage::storage_key_map(&[], 6);
909            let scratch_slot_3 = arb_storage::storage_key_map(&[], 7);
910            arb_storage::write_arbos_storage(
911                unsafe { &mut *state_ptr },
912                scratch_slot_1,
913                U256::ZERO,
914            );
915            arb_storage::write_arbos_storage(
916                unsafe { &mut *state_ptr },
917                scratch_slot_2,
918                U256::ZERO,
919            );
920            arb_storage::write_arbos_storage(
921                unsafe { &mut *state_ptr },
922                scratch_slot_3,
923                U256::ZERO,
924            );
925        }
926
927        // Delete retryable: clears the retryable storage slots
928        {
929            let retryable_base = keccak256([2u8]);
930            for i in 0u64..10 {
931                let slot = arb_storage::storage_key_map(retryable_base.as_slice(), i);
932                arb_storage::write_storage_at(unsafe { &mut *state_ptr }, arbos, slot, U256::ZERO);
933            }
934        }
935
936        // === THE CRITICAL OPERATION: grow_backlog ===
937        {
938            let backing = Storage::new(state_ptr, B256::ZERO);
939            let l2_sto = backing.open_sub_storage(&[1]);
940            let l2_pricing = super::super::open_l2_pricing_state(l2_sto, 20);
941
942            let backlog_before = l2_pricing.gas_backlog().unwrap();
943            eprintln!("[TX2] gasBacklog BEFORE grow: {}", backlog_before);
944            assert_eq!(
945                backlog_before, 552_756,
946                "gasBacklog should still be 552756 before grow"
947            );
948
949            let result = l2_pricing.grow_backlog(357_751, MultiGas::default());
950            assert!(result.is_ok(), "grow_backlog should succeed");
951
952            let backlog_after = l2_pricing.gas_backlog().unwrap();
953            eprintln!("[TX2] gasBacklog AFTER grow: {}", backlog_after);
954            assert_eq!(
955                backlog_after, 910_507,
956                "gasBacklog should be 910507 after grow"
957            );
958        }
959
960        eprintln!("[FINAL] Checking bundle...");
961
962        // ================================================================
963        // Post-block: merge transitions and verify bundle
964        // ================================================================
965        state.merge_transitions(BundleRetention::Reverts);
966        let mut bundle = state.take_bundle();
967
968        // --- Check 1: Is gasBacklog in the bundle BEFORE filtering? ---
969        let pre_filter_backlog = bundle
970            .state
971            .get(&arbos)
972            .and_then(|a| a.storage.get(&gas_backlog_slot))
973            .map(|s| s.present_value);
974        eprintln!("[BUNDLE] Pre-filter gasBacklog: {:?}", pre_filter_backlog);
975        assert_eq!(
976            pre_filter_backlog,
977            Some(U256::from(910_507u64)),
978            "gasBacklog should be in bundle before filter with value 910507"
979        );
980
981        // --- Simulate filter_unchanged_storage (inline, since it's private in producer.rs) ---
982        for (_addr, account) in bundle.state.iter_mut() {
983            account
984                .storage
985                .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
986        }
987
988        // --- Check 2: Is gasBacklog in the bundle AFTER filtering? ---
989        let post_filter_backlog = bundle
990            .state
991            .get(&arbos)
992            .and_then(|a| a.storage.get(&gas_backlog_slot))
993            .map(|s| s.present_value);
994        eprintln!("[BUNDLE] Post-filter gasBacklog: {:?}", post_filter_backlog);
995        assert_eq!(
996            post_filter_backlog,
997            Some(U256::from(910_507u64)),
998            "gasBacklog should survive filter_unchanged_storage with value 910507"
999        );
1000
1001        // --- Check 3: Verify the original value is correct ---
1002        let original = bundle
1003            .state
1004            .get(&arbos)
1005            .and_then(|a| a.storage.get(&gas_backlog_slot))
1006            .map(|s| s.previous_or_original_value);
1007        eprintln!("[BUNDLE] gasBacklog original_value: {:?}", original);
1008        assert_eq!(
1009            original,
1010            Some(U256::from(552_756u64)),
1011            "gasBacklog original should be the pre-block DB value 552756"
1012        );
1013
1014        // --- Check 4: Simulate augment_bundle_from_cache (simplified) ---
1015        // In production, augment_bundle_from_cache runs BEFORE filter.
1016        // But let's verify the cache has the right value too.
1017        let cache_backlog = state
1018            .cache
1019            .accounts
1020            .get(&arbos)
1021            .and_then(|ca| ca.account.as_ref())
1022            .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
1023        eprintln!("[CACHE] gasBacklog in cache: {:?}", cache_backlog);
1024        assert_eq!(
1025            cache_backlog,
1026            Some(U256::from(910_507u64)),
1027            "gasBacklog should be in cache with value 910507"
1028        );
1029
1030        eprintln!("[PASS] Block 616862 flow: gasBacklog correctly persisted as 910507");
1031    }
1032
1033    /// Same as test_block_616862 but the EVM commit INCLUDES the ArbOS account.
1034    /// This simulates the case where a precompile or SLOAD caused the EVM to
1035    /// "touch" the ArbOS account during the RetryTx execution.
1036    #[test]
1037    fn test_block_616862_with_arbos_in_evm_commit() {
1038        use alloy_primitives::map::HashMap;
1039        use revm::{database::states::bundle_state::BundleRetention, DatabaseCommit};
1040
1041        let l2_base = keccak256([1u8]);
1042        let gas_backlog_slot = arb_storage::storage_key_map(l2_base.as_slice(), 4);
1043        let speed_limit_slot = arb_storage::storage_key_map(l2_base.as_slice(), 0);
1044        let base_fee_slot = arb_storage::storage_key_map(l2_base.as_slice(), 2);
1045        let min_base_fee_slot = arb_storage::storage_key_map(l2_base.as_slice(), 3);
1046        let pricing_inertia_slot = arb_storage::storage_key_map(l2_base.as_slice(), 5);
1047        let backlog_tolerance_slot = arb_storage::storage_key_map(l2_base.as_slice(), 6);
1048        let version_slot = arb_storage::storage_key_map(&[], 0);
1049        // Scratch slots
1050        let scratch_1 = arb_storage::storage_key_map(&[], 5);
1051        let scratch_2 = arb_storage::storage_key_map(&[], 6);
1052
1053        struct PreloadedDb {
1054            slots: HashMap<(Address, U256), U256>,
1055        }
1056        impl PreloadedDb {
1057            fn new() -> Self {
1058                Self {
1059                    slots: HashMap::default(),
1060                }
1061            }
1062            fn set(&mut self, addr: Address, slot: U256, val: U256) {
1063                self.slots.insert((addr, slot), val);
1064            }
1065        }
1066        impl Database for PreloadedDb {
1067            type Error = std::convert::Infallible;
1068            fn basic(
1069                &mut self,
1070                addr: Address,
1071            ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
1072                if addr == ARBOS_STATE_ADDRESS {
1073                    Ok(Some(revm::state::AccountInfo {
1074                        nonce: 1,
1075                        balance: U256::ZERO,
1076                        code_hash: keccak256([]),
1077                        code: None,
1078                        account_id: None,
1079                    }))
1080                } else {
1081                    Ok(None)
1082                }
1083            }
1084            fn code_by_hash(&mut self, _: B256) -> Result<revm::state::Bytecode, Self::Error> {
1085                Ok(revm::state::Bytecode::default())
1086            }
1087            fn storage(&mut self, addr: Address, index: U256) -> Result<U256, Self::Error> {
1088                Ok(self
1089                    .slots
1090                    .get(&(addr, index))
1091                    .copied()
1092                    .unwrap_or(U256::ZERO))
1093            }
1094            fn block_hash(&mut self, _: u64) -> Result<B256, Self::Error> {
1095                Ok(B256::ZERO)
1096            }
1097        }
1098
1099        let arbos = ARBOS_STATE_ADDRESS;
1100        let mut db = PreloadedDb::new();
1101        db.set(arbos, gas_backlog_slot, U256::from(552_756u64));
1102        db.set(arbos, speed_limit_slot, U256::from(7_000_000u64));
1103        db.set(arbos, base_fee_slot, U256::from(100_000_000u64));
1104        db.set(arbos, min_base_fee_slot, U256::from(100_000_000u64));
1105        db.set(arbos, pricing_inertia_slot, U256::from(102u64));
1106        db.set(arbos, backlog_tolerance_slot, U256::from(10u64));
1107        db.set(arbos, version_slot, U256::from(20u64));
1108
1109        let mut state = StateBuilder::new()
1110            .with_database(db)
1111            .with_bundle_update()
1112            .build();
1113        let state_ptr: *mut revm::database::State<PreloadedDb> = &mut state;
1114
1115        // TX0: StartBlock (no-op drain)
1116        {
1117            let backing = Storage::new(state_ptr, B256::ZERO);
1118            let l2_sto = backing.open_sub_storage(&[1]);
1119            let l2_pricing = super::super::open_l2_pricing_state(l2_sto, 20);
1120            let _ = l2_pricing.update_pricing_model(0, 20);
1121        }
1122        state.commit(HashMap::default());
1123
1124        // TX1: SubmitRetryable — write scratch slots + retryable storage
1125        {
1126            arb_storage::write_storage_at(
1127                unsafe { &mut *state_ptr },
1128                arbos,
1129                scratch_1,
1130                U256::from(42),
1131            );
1132            arb_storage::write_storage_at(
1133                unsafe { &mut *state_ptr },
1134                arbos,
1135                scratch_2,
1136                U256::from(43),
1137            );
1138            let retryable_base = keccak256([2u8]);
1139            for i in 0u64..5 {
1140                let slot = arb_storage::storage_key_map(retryable_base.as_slice(), i);
1141                arb_storage::write_storage_at(
1142                    unsafe { &mut *state_ptr },
1143                    arbos,
1144                    slot,
1145                    U256::from(1000 + i),
1146                );
1147            }
1148        }
1149        state.commit(HashMap::default());
1150        // Clear scratch
1151        arb_storage::write_arbos_storage(unsafe { &mut *state_ptr }, scratch_1, U256::ZERO);
1152        arb_storage::write_arbos_storage(unsafe { &mut *state_ptr }, scratch_2, U256::ZERO);
1153
1154        // TX2: RetryTx — write scratch, then EVM commit WITH ArbOS account
1155        arb_storage::write_storage_at(unsafe { &mut *state_ptr }, arbos, scratch_1, U256::from(99));
1156        arb_storage::write_storage_at(
1157            unsafe { &mut *state_ptr },
1158            arbos,
1159            scratch_2,
1160            U256::from(100),
1161        );
1162
1163        // EVM commit that INCLUDES the ArbOS account (the critical difference!)
1164        {
1165            let mut evm_changes: HashMap<Address, revm::state::Account> = Default::default();
1166
1167            // Sender
1168            let sender = address!("fd86e9a33fd52e4085fb94d24b759448a621cd36");
1169            let _ = state.load_cache_account(sender);
1170            let mut sender_acct = revm::state::Account::default();
1171            sender_acct.info.balance = U256::from(1_000_000_000u64);
1172            sender_acct.info.nonce = 1;
1173            sender_acct.mark_touch();
1174            evm_changes.insert(sender, sender_acct);
1175
1176            // ArbOS account IN the EVM commit — simulates a precompile/SLOAD
1177            // that caused the EVM to track the ArbOS account
1178            let _ = state.load_cache_account(arbos);
1179            let mut arbos_acct = revm::state::Account {
1180                info: revm::state::AccountInfo {
1181                    nonce: 1,
1182                    balance: U256::ZERO,
1183                    code_hash: keccak256([]),
1184                    code: None,
1185                    account_id: None,
1186                },
1187                ..Default::default()
1188            };
1189            // The EVM "read" the scratch slot — it appears in the EVM's storage
1190            // with is_changed=false (just loaded, not modified)
1191            arbos_acct.storage.insert(
1192                scratch_1,
1193                revm::state::EvmStorageSlot::new(U256::from(99), 0),
1194            );
1195            arbos_acct.mark_touch();
1196            evm_changes.insert(arbos, arbos_acct);
1197
1198            state.commit(evm_changes);
1199        }
1200
1201        eprintln!("[VARIANT] After EVM commit with ArbOS account included");
1202
1203        // Check: is gasBacklog still readable?
1204        let backlog_check =
1205            arb_storage::read_storage_at(unsafe { &mut *state_ptr }, arbos, gas_backlog_slot);
1206        eprintln!("[VARIANT] gasBacklog after EVM commit: {}", backlog_check);
1207
1208        // Clear scratch
1209        arb_storage::write_arbos_storage(unsafe { &mut *state_ptr }, scratch_1, U256::ZERO);
1210        arb_storage::write_arbos_storage(unsafe { &mut *state_ptr }, scratch_2, U256::ZERO);
1211
1212        // Delete retryable
1213        {
1214            let retryable_base = keccak256([2u8]);
1215            for i in 0u64..5 {
1216                let slot = arb_storage::storage_key_map(retryable_base.as_slice(), i);
1217                arb_storage::write_storage_at(unsafe { &mut *state_ptr }, arbos, slot, U256::ZERO);
1218            }
1219        }
1220
1221        // THE CRITICAL OPERATION: grow_backlog
1222        {
1223            let backing = Storage::new(state_ptr, B256::ZERO);
1224            let l2_sto = backing.open_sub_storage(&[1]);
1225            let l2_pricing = super::super::open_l2_pricing_state(l2_sto, 20);
1226
1227            let backlog_before = l2_pricing.gas_backlog().unwrap();
1228            eprintln!("[VARIANT] gasBacklog BEFORE grow: {}", backlog_before);
1229            assert_eq!(
1230                backlog_before, 552_756,
1231                "gasBacklog should be 552756 before grow"
1232            );
1233
1234            let _ = l2_pricing.grow_backlog(357_751, MultiGas::default());
1235
1236            let backlog_after = l2_pricing.gas_backlog().unwrap();
1237            eprintln!("[VARIANT] gasBacklog AFTER grow: {}", backlog_after);
1238            assert_eq!(backlog_after, 910_507, "gasBacklog should be 910507");
1239        }
1240
1241        // Verify bundle
1242        state.merge_transitions(BundleRetention::Reverts);
1243        let mut bundle = state.take_bundle();
1244
1245        let pre_filter = bundle
1246            .state
1247            .get(&arbos)
1248            .and_then(|a| a.storage.get(&gas_backlog_slot))
1249            .map(|s| (s.present_value, s.previous_or_original_value));
1250        eprintln!("[VARIANT] Pre-filter gasBacklog: {:?}", pre_filter);
1251        assert_eq!(
1252            pre_filter.map(|p| p.0),
1253            Some(U256::from(910_507u64)),
1254            "gasBacklog should be 910507 in bundle before filter"
1255        );
1256
1257        // filter_unchanged_storage
1258        for (_addr, account) in bundle.state.iter_mut() {
1259            account
1260                .storage
1261                .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
1262        }
1263
1264        let post_filter = bundle
1265            .state
1266            .get(&arbos)
1267            .and_then(|a| a.storage.get(&gas_backlog_slot))
1268            .map(|s| s.present_value);
1269        eprintln!("[VARIANT] Post-filter gasBacklog: {:?}", post_filter);
1270        assert_eq!(
1271            post_filter,
1272            Some(U256::from(910_507u64)),
1273            "gasBacklog should survive filter when ArbOS is in EVM commit"
1274        );
1275
1276        eprintln!("[PASS] Variant with ArbOS in EVM commit: gasBacklog correctly persisted");
1277    }
1278
1279    /// Test what happens when transition_state is None (already consumed by
1280    /// a prior merge_transitions). This simulates a bug where merge_transitions
1281    /// is called mid-block, causing subsequent write_storage_at transitions to
1282    /// be silently dropped.
1283    #[test]
1284    fn test_block_616862_transition_state_consumed() {
1285        use alloy_primitives::map::HashMap;
1286        use revm::{
1287            database::states::{bundle_state::BundleRetention, plain_account::StorageSlot},
1288            DatabaseCommit,
1289        };
1290
1291        let l2_base = keccak256([1u8]);
1292        let gas_backlog_slot = arb_storage::storage_key_map(l2_base.as_slice(), 4);
1293        let speed_limit_slot = arb_storage::storage_key_map(l2_base.as_slice(), 0);
1294        let base_fee_slot = arb_storage::storage_key_map(l2_base.as_slice(), 2);
1295        let min_base_fee_slot = arb_storage::storage_key_map(l2_base.as_slice(), 3);
1296        let pricing_inertia_slot = arb_storage::storage_key_map(l2_base.as_slice(), 5);
1297        let backlog_tolerance_slot = arb_storage::storage_key_map(l2_base.as_slice(), 6);
1298        let version_slot = arb_storage::storage_key_map(&[], 0);
1299
1300        struct PreloadedDb(HashMap<(Address, U256), U256>);
1301        impl PreloadedDb {
1302            fn new() -> Self {
1303                Self(HashMap::default())
1304            }
1305            fn set(&mut self, a: Address, s: U256, v: U256) {
1306                self.0.insert((a, s), v);
1307            }
1308        }
1309        impl Database for PreloadedDb {
1310            type Error = std::convert::Infallible;
1311            fn basic(
1312                &mut self,
1313                addr: Address,
1314            ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
1315                if addr == ARBOS_STATE_ADDRESS {
1316                    Ok(Some(revm::state::AccountInfo {
1317                        nonce: 1,
1318                        balance: U256::ZERO,
1319                        code_hash: keccak256([]),
1320                        code: None,
1321                        account_id: None,
1322                    }))
1323                } else {
1324                    Ok(None)
1325                }
1326            }
1327            fn code_by_hash(&mut self, _: B256) -> Result<revm::state::Bytecode, Self::Error> {
1328                Ok(revm::state::Bytecode::default())
1329            }
1330            fn storage(&mut self, a: Address, i: U256) -> Result<U256, Self::Error> {
1331                Ok(self.0.get(&(a, i)).copied().unwrap_or(U256::ZERO))
1332            }
1333            fn block_hash(&mut self, _: u64) -> Result<B256, Self::Error> {
1334                Ok(B256::ZERO)
1335            }
1336        }
1337
1338        let arbos = ARBOS_STATE_ADDRESS;
1339        let mut db = PreloadedDb::new();
1340        db.set(arbos, gas_backlog_slot, U256::from(552_756u64));
1341        db.set(arbos, speed_limit_slot, U256::from(7_000_000u64));
1342        db.set(arbos, base_fee_slot, U256::from(100_000_000u64));
1343        db.set(arbos, min_base_fee_slot, U256::from(100_000_000u64));
1344        db.set(arbos, pricing_inertia_slot, U256::from(102u64));
1345        db.set(arbos, backlog_tolerance_slot, U256::from(10u64));
1346        db.set(arbos, version_slot, U256::from(20u64));
1347
1348        let mut state = StateBuilder::new()
1349            .with_database(db)
1350            .with_bundle_update()
1351            .build();
1352        let state_ptr: *mut revm::database::State<PreloadedDb> = &mut state;
1353
1354        // TX0: StartBlock (no-op drain)
1355        {
1356            let backing = Storage::new(state_ptr, B256::ZERO);
1357            let l2_sto = backing.open_sub_storage(&[1]);
1358            let l2_pricing = super::super::open_l2_pricing_state(l2_sto, 20);
1359            let _ = l2_pricing.update_pricing_model(0, 20);
1360        }
1361        state.commit(HashMap::default());
1362
1363        // === SIMULATE BUG: merge_transitions called mid-block ===
1364        // This consumes transition_state, setting it to None.
1365        // All subsequent write_storage_at calls will have their
1366        // transitions SILENTLY DROPPED.
1367        state.merge_transitions(BundleRetention::Reverts);
1368        let _mid_bundle = state.take_bundle();
1369        eprintln!("[BUG-SIM] transition_state consumed mid-block!");
1370
1371        // Check: is transition_state None?
1372        let ts_is_none = state.transition_state.is_none();
1373        eprintln!("[BUG-SIM] transition_state is None: {}", ts_is_none);
1374
1375        // TX2: grow_backlog — the write goes to cache but transition is dropped
1376        {
1377            let backing = Storage::new(state_ptr, B256::ZERO);
1378            let l2_sto = backing.open_sub_storage(&[1]);
1379            let l2_pricing = super::super::open_l2_pricing_state(l2_sto, 20);
1380
1381            let backlog_before = l2_pricing.gas_backlog().unwrap();
1382            eprintln!("[BUG-SIM] gasBacklog before grow: {}", backlog_before);
1383
1384            let _ = l2_pricing.grow_backlog(357_751, MultiGas::default());
1385
1386            let backlog_after = l2_pricing.gas_backlog().unwrap();
1387            eprintln!(
1388                "[BUG-SIM] gasBacklog after grow: {} (from cache)",
1389                backlog_after
1390            );
1391        }
1392
1393        // End of block: merge_transitions again
1394        state.merge_transitions(BundleRetention::Reverts);
1395        let mut bundle = state.take_bundle();
1396
1397        // Check: is gasBacklog in the bundle?
1398        let in_bundle = bundle
1399            .state
1400            .get(&arbos)
1401            .and_then(|a| a.storage.get(&gas_backlog_slot))
1402            .map(|s| s.present_value);
1403        eprintln!(
1404            "[BUG-SIM] gasBacklog in bundle after 2nd merge: {:?}",
1405            in_bundle
1406        );
1407
1408        // If transition_state was None, the gasBacklog transition was dropped.
1409        // The bundle from the 2nd merge would NOT have the gasBacklog.
1410        // Now simulate augment_bundle_from_cache which should rescue it from cache.
1411        {
1412            let cache_val = state
1413                .cache
1414                .accounts
1415                .get(&arbos)
1416                .and_then(|ca| ca.account.as_ref())
1417                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
1418            eprintln!("[BUG-SIM] gasBacklog in cache: {:?}", cache_val);
1419
1420            // Inline augment_bundle_from_cache for ArbOS account
1421            if let Some(bundle_acct) = bundle.state.get_mut(&arbos) {
1422                if let Some(cached_acc) = state.cache.accounts.get(&arbos) {
1423                    if let Some(ref plain) = cached_acc.account {
1424                        for (key, value) in &plain.storage {
1425                            if let Some(slot) = bundle_acct.storage.get_mut(key) {
1426                                slot.present_value = *value;
1427                            } else {
1428                                let original =
1429                                    state.database.storage(arbos, *key).unwrap_or(U256::ZERO);
1430                                if *value != original {
1431                                    bundle_acct.storage.insert(
1432                                        *key,
1433                                        StorageSlot {
1434                                            previous_or_original_value: original,
1435                                            present_value: *value,
1436                                        },
1437                                    );
1438                                }
1439                            }
1440                        }
1441                    }
1442                }
1443            } else {
1444                // ArbOS not in bundle — add it from cache
1445                if let Some(cached_acc) = state.cache.accounts.get(&arbos) {
1446                    if let Some(ref plain) = cached_acc.account {
1447                        let mut storage_changes: HashMap<U256, StorageSlot> = HashMap::default();
1448                        for (key, value) in &plain.storage {
1449                            let original =
1450                                state.database.storage(arbos, *key).unwrap_or(U256::ZERO);
1451                            if *value != original {
1452                                storage_changes.insert(
1453                                    *key,
1454                                    StorageSlot {
1455                                        previous_or_original_value: original,
1456                                        present_value: *value,
1457                                    },
1458                                );
1459                            }
1460                        }
1461                        if !storage_changes.is_empty() {
1462                            bundle.state.insert(
1463                                arbos,
1464                                revm::database::BundleAccount {
1465                                    info: Some(plain.info.clone()),
1466                                    original_info: None,
1467                                    storage: storage_changes,
1468                                    status: revm::database::AccountStatus::Changed,
1469                                },
1470                            );
1471                        }
1472                    }
1473                }
1474            }
1475        }
1476
1477        let after_augment = bundle
1478            .state
1479            .get(&arbos)
1480            .and_then(|a| a.storage.get(&gas_backlog_slot))
1481            .map(|s| s.present_value);
1482        eprintln!("[BUG-SIM] gasBacklog after augment: {:?}", after_augment);
1483
1484        // filter_unchanged_storage
1485        for (_addr, account) in bundle.state.iter_mut() {
1486            account
1487                .storage
1488                .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
1489        }
1490
1491        let after_filter = bundle
1492            .state
1493            .get(&arbos)
1494            .and_then(|a| a.storage.get(&gas_backlog_slot))
1495            .map(|s| s.present_value);
1496        eprintln!("[BUG-SIM] gasBacklog after filter: {:?}", after_filter);
1497
1498        assert_eq!(
1499            after_filter,
1500            Some(U256::from(910_507u64)),
1501            "gasBacklog MUST survive even when transition_state was consumed mid-block"
1502        );
1503
1504        eprintln!("[PASS] Bug simulation: augment_bundle_from_cache rescued gasBacklog");
1505    }
1506
1507    /// Simulates the full production flow step-by-step to find why
1508    /// gas_backlog writes are lost. Tests the interaction between:
1509    /// - ArbOS storage writes (via Storage/write_storage_at)
1510    /// - EVM state commits (state.commit)
1511    /// - Bundle construction (merge_transitions + take_bundle)
1512    /// - Post-bundle augmentation (augment_bundle_from_cache logic)
1513    /// - Storage filtering (filter_unchanged_storage logic)
1514    #[test]
1515    fn test_grow_backlog_survives_evm_commit_and_augment() {
1516        use revm::{
1517            database::states::{bundle_state::BundleRetention, plain_account::StorageSlot},
1518            DatabaseCommit,
1519        };
1520
1521        // Compute the actual gasBacklog slot for assertions
1522        let l2_base = keccak256([1u8]); // open_sub_storage([1]) from root
1523        let gas_backlog_offset: u64 = 4;
1524        let gas_backlog_slot = arb_storage::storage_key_map(l2_base.as_slice(), gas_backlog_offset);
1525
1526        eprintln!("[TEST] gas_backlog_slot = {:?}", gas_backlog_slot);
1527
1528        // ===== VARIANT A: EVM commit with EMPTY HashMap (no ArbOS account touched) =====
1529        eprintln!("\n===== VARIANT A: Empty EVM commit =====");
1530        {
1531            let mut state = StateBuilder::new()
1532                .with_database(EmptyDb)
1533                .with_bundle_update()
1534                .build();
1535
1536            // Step 1: Ensure ArbOS account exists with nonce=1
1537            ensure_cache_account(&mut state, ARBOS_STATE_ADDRESS);
1538            arb_storage::set_account_nonce(&mut state, ARBOS_STATE_ADDRESS, 1);
1539
1540            let state_ptr: *mut revm::database::State<EmptyDb> = &mut state;
1541
1542            // Step 2: Initialize L2 pricing state
1543            let backing = Storage::new(state_ptr, B256::ZERO);
1544            let l2_sto = backing.open_sub_storage(&[1]);
1545            super::super::initialize_l2_pricing_state(&l2_sto);
1546
1547            // Step 3: Set gas_backlog to 552756 (simulate pre-existing backlog)
1548            let l2_pricing =
1549                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1550            l2_pricing.set_gas_backlog(552756).unwrap();
1551            let pre_start = l2_pricing.gas_backlog().unwrap();
1552            assert_eq!(pre_start, 552756, "Pre-existing backlog should be 552756");
1553            eprintln!("[A] Step 3: gas_backlog set to {}", pre_start);
1554
1555            // Step 4: Simulate StartBlock: update_pricing_model(time_passed=0)
1556            l2_pricing.update_pricing_model(0, 10).unwrap();
1557            let after_start = l2_pricing.gas_backlog().unwrap();
1558            eprintln!(
1559                "[A] Step 4: after update_pricing_model(0): gas_backlog={}",
1560                after_start
1561            );
1562            assert_eq!(
1563                after_start, 552756,
1564                "time_passed=0 should not change backlog"
1565            );
1566
1567            // Step 5: EVM commit with empty HashMap
1568            let empty_state: alloy_primitives::map::HashMap<Address, revm::state::Account> =
1569                Default::default();
1570            state.commit(empty_state);
1571            eprintln!("[A] Step 5: committed empty EVM state");
1572
1573            // Check cache after commit
1574            let cache_val = state
1575                .cache
1576                .accounts
1577                .get(&ARBOS_STATE_ADDRESS)
1578                .and_then(|ca| ca.account.as_ref())
1579                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
1580            eprintln!(
1581                "[A] Step 5 cache check: gas_backlog in cache = {:?}",
1582                cache_val
1583            );
1584
1585            // Step 6: grow_backlog(357751)
1586            let l2_pricing2 =
1587                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1588            l2_pricing2
1589                .grow_backlog(357751, MultiGas::default())
1590                .unwrap();
1591            let after_grow = l2_pricing2.gas_backlog().unwrap();
1592            eprintln!(
1593                "[A] Step 6: after grow_backlog(357751): gas_backlog={}",
1594                after_grow
1595            );
1596            assert_eq!(after_grow, 552756 + 357751, "backlog should be sum");
1597
1598            // Check cache after grow
1599            let cache_val2 = state
1600                .cache
1601                .accounts
1602                .get(&ARBOS_STATE_ADDRESS)
1603                .and_then(|ca| ca.account.as_ref())
1604                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
1605            eprintln!(
1606                "[A] Step 6 cache check: gas_backlog in cache = {:?}",
1607                cache_val2
1608            );
1609
1610            // Step 7: merge_transitions + take_bundle
1611            state.merge_transitions(BundleRetention::Reverts);
1612            let mut bundle = state.take_bundle();
1613            eprintln!("[A] Step 7: bundle has {} accounts", bundle.state.len());
1614
1615            // Check bundle before augment
1616            let bundle_has_slot = bundle
1617                .state
1618                .get(&ARBOS_STATE_ADDRESS)
1619                .and_then(|a| a.storage.get(&gas_backlog_slot))
1620                .map(|s| s.present_value);
1621            eprintln!(
1622                "[A] Step 7 bundle pre-augment: gas_backlog = {:?}",
1623                bundle_has_slot
1624            );
1625
1626            // Step 8: Simulate augment_bundle_from_cache (inline replication)
1627            // In production, this is called on the same state after take_bundle
1628            for (addr, cache_acct) in &state.cache.accounts {
1629                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
1630                let current_storage = cache_acct
1631                    .account
1632                    .as_ref()
1633                    .map(|a| &a.storage)
1634                    .cloned()
1635                    .unwrap_or_default();
1636
1637                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
1638                    bundle_acct.info = current_info;
1639                    for (key, value) in &current_storage {
1640                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
1641                            slot.present_value = *value;
1642                        } else {
1643                            // Slot from cache not in bundle: compare with DB original (0 for
1644                            // EmptyDb)
1645                            let original_value = U256::ZERO;
1646                            if *value != original_value {
1647                                bundle_acct.storage.insert(
1648                                    *key,
1649                                    StorageSlot {
1650                                        previous_or_original_value: original_value,
1651                                        present_value: *value,
1652                                    },
1653                                );
1654                            }
1655                        }
1656                    }
1657                } else {
1658                    // Account not in bundle — add if changed
1659                    let storage_changes: alloy_primitives::map::HashMap<U256, StorageSlot> =
1660                        current_storage
1661                            .iter()
1662                            .filter_map(|(key, value)| {
1663                                let original_value = U256::ZERO;
1664                                if original_value != *value {
1665                                    Some((
1666                                        *key,
1667                                        StorageSlot {
1668                                            previous_or_original_value: original_value,
1669                                            present_value: *value,
1670                                        },
1671                                    ))
1672                                } else {
1673                                    None
1674                                }
1675                            })
1676                            .collect();
1677
1678                    let info_changed = current_info.is_some(); // was None in DB
1679                    if info_changed || !storage_changes.is_empty() {
1680                        bundle.state.insert(
1681                            *addr,
1682                            revm::database::BundleAccount {
1683                                info: current_info,
1684                                original_info: None,
1685                                storage: storage_changes,
1686                                status: revm::database::AccountStatus::InMemoryChange,
1687                            },
1688                        );
1689                    }
1690                }
1691            }
1692
1693            let bundle_after_augment = bundle
1694                .state
1695                .get(&ARBOS_STATE_ADDRESS)
1696                .and_then(|a| a.storage.get(&gas_backlog_slot))
1697                .map(|s| (s.present_value, s.previous_or_original_value));
1698            eprintln!(
1699                "[A] Step 8 bundle post-augment: gas_backlog = {:?}",
1700                bundle_after_augment
1701            );
1702
1703            // Step 9: filter_unchanged_storage
1704            for (_addr, account) in bundle.state.iter_mut() {
1705                account
1706                    .storage
1707                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
1708            }
1709
1710            let final_slot = bundle
1711                .state
1712                .get(&ARBOS_STATE_ADDRESS)
1713                .and_then(|a| a.storage.get(&gas_backlog_slot))
1714                .map(|s| s.present_value);
1715            eprintln!("[A] Step 9 FINAL: gas_backlog in bundle = {:?}", final_slot);
1716            assert!(
1717                final_slot.is_some(),
1718                "VARIANT A FAILED: gas_backlog slot MISSING from bundle after empty EVM commit"
1719            );
1720            assert_eq!(
1721                final_slot.unwrap(),
1722                U256::from(552756u64 + 357751u64),
1723                "VARIANT A: gas_backlog should be 910507"
1724            );
1725        }
1726
1727        // ===== VARIANT B: EVM commit WITH ArbOS account touched (simulates precompile read) =====
1728        eprintln!("\n===== VARIANT B: EVM commit with ArbOS account touched =====");
1729        {
1730            let mut state = StateBuilder::new()
1731                .with_database(EmptyDb)
1732                .with_bundle_update()
1733                .build();
1734
1735            ensure_cache_account(&mut state, ARBOS_STATE_ADDRESS);
1736            arb_storage::set_account_nonce(&mut state, ARBOS_STATE_ADDRESS, 1);
1737
1738            let state_ptr: *mut revm::database::State<EmptyDb> = &mut state;
1739
1740            let backing = Storage::new(state_ptr, B256::ZERO);
1741            let l2_sto = backing.open_sub_storage(&[1]);
1742            super::super::initialize_l2_pricing_state(&l2_sto);
1743
1744            let l2_pricing =
1745                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1746            l2_pricing.set_gas_backlog(552756).unwrap();
1747            l2_pricing.update_pricing_model(0, 10).unwrap();
1748            let before_commit = l2_pricing.gas_backlog().unwrap();
1749            eprintln!("[B] Pre-commit: gas_backlog={}", before_commit);
1750
1751            // Count cache slots before commit
1752            let cache_slots_before = state
1753                .cache
1754                .accounts
1755                .get(&ARBOS_STATE_ADDRESS)
1756                .and_then(|ca| ca.account.as_ref())
1757                .map(|a| a.storage.len())
1758                .unwrap_or(0);
1759            eprintln!("[B] Cache slots before EVM commit: {}", cache_slots_before);
1760
1761            // EVM commit with ArbOS account TOUCHED but no storage changes
1762            // This simulates what happens when EVM executes a precompile that
1763            // reads ArbOS state — the account appears in the EVM output with
1764            // is_touched=true but storage unchanged.
1765            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
1766            let mut arbos_evm_account = revm::state::Account {
1767                info: revm::state::AccountInfo {
1768                    balance: U256::ZERO,
1769                    nonce: 1,
1770                    code_hash: keccak256([]),
1771                    code: None,
1772                    account_id: None,
1773                },
1774                ..Default::default()
1775            };
1776            arbos_evm_account.mark_touch();
1777            // No storage entries — EVM read slots but didn't write them
1778            let mut evm_changes: alloy_primitives::map::HashMap<Address, revm::state::Account> =
1779                Default::default();
1780            evm_changes.insert(ARBOS_STATE_ADDRESS, arbos_evm_account);
1781            state.commit(evm_changes);
1782            eprintln!("[B] Committed EVM state with ArbOS account touched");
1783
1784            // Check cache after commit — this is the critical check!
1785            let cache_slots_after = state
1786                .cache
1787                .accounts
1788                .get(&ARBOS_STATE_ADDRESS)
1789                .and_then(|ca| ca.account.as_ref())
1790                .map(|a| a.storage.len())
1791                .unwrap_or(0);
1792            eprintln!(
1793                "[B] Cache slots AFTER EVM commit: {} (was {})",
1794                cache_slots_after, cache_slots_before
1795            );
1796
1797            let cache_val = state
1798                .cache
1799                .accounts
1800                .get(&ARBOS_STATE_ADDRESS)
1801                .and_then(|ca| ca.account.as_ref())
1802                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
1803            eprintln!("[B] gas_backlog in cache after EVM commit: {:?}", cache_val);
1804
1805            if cache_slots_after < cache_slots_before {
1806                eprintln!(
1807                    "[B] !!! CACHE SLOTS WERE LOST BY EVM COMMIT !!! ({} -> {})",
1808                    cache_slots_before, cache_slots_after
1809                );
1810            }
1811
1812            // Now grow_backlog AFTER the EVM commit
1813            let l2_pricing2 =
1814                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1815            let read_before_grow = l2_pricing2.gas_backlog().unwrap();
1816            eprintln!("[B] gas_backlog read before grow: {}", read_before_grow);
1817
1818            l2_pricing2
1819                .grow_backlog(357751, MultiGas::default())
1820                .unwrap();
1821            let after_grow = l2_pricing2.gas_backlog().unwrap();
1822            eprintln!("[B] gas_backlog after grow: {}", after_grow);
1823
1824            // Check cache after grow
1825            let cache_val2 = state
1826                .cache
1827                .accounts
1828                .get(&ARBOS_STATE_ADDRESS)
1829                .and_then(|ca| ca.account.as_ref())
1830                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
1831            eprintln!("[B] gas_backlog in cache after grow: {:?}", cache_val2);
1832
1833            // merge + take_bundle
1834            state.merge_transitions(BundleRetention::Reverts);
1835            let mut bundle = state.take_bundle();
1836
1837            let bundle_pre = bundle
1838                .state
1839                .get(&ARBOS_STATE_ADDRESS)
1840                .and_then(|a| a.storage.get(&gas_backlog_slot))
1841                .map(|s| (s.present_value, s.previous_or_original_value));
1842            eprintln!("[B] Bundle pre-augment: gas_backlog = {:?}", bundle_pre);
1843
1844            // augment_bundle_from_cache (inline)
1845            for (addr, cache_acct) in &state.cache.accounts {
1846                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
1847                let current_storage = cache_acct
1848                    .account
1849                    .as_ref()
1850                    .map(|a| &a.storage)
1851                    .cloned()
1852                    .unwrap_or_default();
1853
1854                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
1855                    bundle_acct.info = current_info;
1856                    for (key, value) in &current_storage {
1857                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
1858                            slot.present_value = *value;
1859                        } else {
1860                            let original_value = U256::ZERO;
1861                            if *value != original_value {
1862                                bundle_acct.storage.insert(
1863                                    *key,
1864                                    StorageSlot {
1865                                        previous_or_original_value: original_value,
1866                                        present_value: *value,
1867                                    },
1868                                );
1869                            }
1870                        }
1871                    }
1872                }
1873            }
1874
1875            let bundle_post = bundle
1876                .state
1877                .get(&ARBOS_STATE_ADDRESS)
1878                .and_then(|a| a.storage.get(&gas_backlog_slot))
1879                .map(|s| (s.present_value, s.previous_or_original_value));
1880            eprintln!("[B] Bundle post-augment: gas_backlog = {:?}", bundle_post);
1881
1882            // filter_unchanged_storage
1883            for (_addr, account) in bundle.state.iter_mut() {
1884                account
1885                    .storage
1886                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
1887            }
1888
1889            let final_slot = bundle
1890                .state
1891                .get(&ARBOS_STATE_ADDRESS)
1892                .and_then(|a| a.storage.get(&gas_backlog_slot))
1893                .map(|s| s.present_value);
1894            eprintln!("[B] FINAL: gas_backlog in bundle = {:?}", final_slot);
1895            assert!(
1896                final_slot.is_some(),
1897                "VARIANT B FAILED: gas_backlog slot MISSING from bundle after EVM commit with ArbOS touched"
1898            );
1899            assert_eq!(
1900                final_slot.unwrap(),
1901                U256::from(552756u64 + 357751u64),
1902                "VARIANT B: gas_backlog should be 910507"
1903            );
1904        }
1905
1906        // ===== VARIANT C: EVM commit WITH ArbOS account AND storage slot that was read =====
1907        // This simulates the most realistic case: EVM reads gasBacklog slot during
1908        // execution (e.g., GetPricesInWei precompile reads ArbOS state), and the
1909        // slot appears in EVM output with is_changed()=false but present in Account.storage
1910        eprintln!("\n===== VARIANT C: EVM commit with ArbOS storage slot read (unchanged) =====");
1911        {
1912            let mut state = StateBuilder::new()
1913                .with_database(EmptyDb)
1914                .with_bundle_update()
1915                .build();
1916
1917            ensure_cache_account(&mut state, ARBOS_STATE_ADDRESS);
1918            arb_storage::set_account_nonce(&mut state, ARBOS_STATE_ADDRESS, 1);
1919
1920            let state_ptr: *mut revm::database::State<EmptyDb> = &mut state;
1921
1922            let backing = Storage::new(state_ptr, B256::ZERO);
1923            let l2_sto = backing.open_sub_storage(&[1]);
1924            super::super::initialize_l2_pricing_state(&l2_sto);
1925
1926            let l2_pricing =
1927                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1928            l2_pricing.set_gas_backlog(552756).unwrap();
1929            l2_pricing.update_pricing_model(0, 10).unwrap();
1930            eprintln!(
1931                "[C] Pre-commit: gas_backlog={}",
1932                l2_pricing.gas_backlog().unwrap()
1933            );
1934
1935            let cache_slots_before = state
1936                .cache
1937                .accounts
1938                .get(&ARBOS_STATE_ADDRESS)
1939                .and_then(|ca| ca.account.as_ref())
1940                .map(|a| a.storage.len())
1941                .unwrap_or(0);
1942            eprintln!("[C] Cache slots before: {}", cache_slots_before);
1943
1944            // EVM commit with ArbOS account touched AND a storage slot that was
1945            // read but not written (EvmStorageSlot with original_value == present_value).
1946            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
1947            let mut arbos_evm_account = revm::state::Account {
1948                info: revm::state::AccountInfo {
1949                    balance: U256::ZERO,
1950                    nonce: 1,
1951                    code_hash: keccak256([]),
1952                    code: None,
1953                    account_id: None,
1954                },
1955                ..Default::default()
1956            };
1957            arbos_evm_account.mark_touch();
1958
1959            // Add gas_backlog slot as READ-ONLY (original == present, is_changed()=false)
1960            // This is what happens when the EVM loads a storage slot via SLOAD
1961            arbos_evm_account.storage.insert(
1962                gas_backlog_slot,
1963                revm::state::EvmStorageSlot::new(U256::from(552756u64), 0),
1964                // new() sets original_value = present_value, so is_changed() = false
1965            );
1966
1967            let mut evm_changes: alloy_primitives::map::HashMap<Address, revm::state::Account> =
1968                Default::default();
1969            evm_changes.insert(ARBOS_STATE_ADDRESS, arbos_evm_account);
1970            state.commit(evm_changes);
1971            eprintln!("[C] Committed EVM state with ArbOS + read-only storage slot");
1972
1973            let cache_slots_after = state
1974                .cache
1975                .accounts
1976                .get(&ARBOS_STATE_ADDRESS)
1977                .and_then(|ca| ca.account.as_ref())
1978                .map(|a| a.storage.len())
1979                .unwrap_or(0);
1980            eprintln!(
1981                "[C] Cache slots AFTER: {} (was {})",
1982                cache_slots_after, cache_slots_before
1983            );
1984
1985            let cache_val = state
1986                .cache
1987                .accounts
1988                .get(&ARBOS_STATE_ADDRESS)
1989                .and_then(|ca| ca.account.as_ref())
1990                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
1991            eprintln!("[C] gas_backlog in cache after commit: {:?}", cache_val);
1992
1993            if cache_slots_after < cache_slots_before {
1994                eprintln!(
1995                    "[C] !!! CACHE SLOTS LOST !!! {} -> {}",
1996                    cache_slots_before, cache_slots_after
1997                );
1998            }
1999
2000            // grow_backlog after commit
2001            let l2_pricing2 =
2002                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2003            let read_before_grow = l2_pricing2.gas_backlog().unwrap();
2004            eprintln!(
2005                "[C] gas_backlog read before grow: {} (expect 552756)",
2006                read_before_grow
2007            );
2008
2009            l2_pricing2
2010                .grow_backlog(357751, MultiGas::default())
2011                .unwrap();
2012            let after_grow = l2_pricing2.gas_backlog().unwrap();
2013            eprintln!("[C] gas_backlog after grow: {} (expect 910507)", after_grow);
2014
2015            // merge + take_bundle
2016            state.merge_transitions(BundleRetention::Reverts);
2017            let mut bundle = state.take_bundle();
2018
2019            let bundle_pre = bundle
2020                .state
2021                .get(&ARBOS_STATE_ADDRESS)
2022                .and_then(|a| a.storage.get(&gas_backlog_slot))
2023                .map(|s| (s.present_value, s.previous_or_original_value));
2024            eprintln!("[C] Bundle pre-augment: gas_backlog = {:?}", bundle_pre);
2025
2026            // augment (inline)
2027            for (addr, cache_acct) in &state.cache.accounts {
2028                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
2029                let current_storage = cache_acct
2030                    .account
2031                    .as_ref()
2032                    .map(|a| &a.storage)
2033                    .cloned()
2034                    .unwrap_or_default();
2035
2036                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
2037                    bundle_acct.info = current_info;
2038                    for (key, value) in &current_storage {
2039                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
2040                            slot.present_value = *value;
2041                        } else {
2042                            let original_value = U256::ZERO;
2043                            if *value != original_value {
2044                                bundle_acct.storage.insert(
2045                                    *key,
2046                                    StorageSlot {
2047                                        previous_or_original_value: original_value,
2048                                        present_value: *value,
2049                                    },
2050                                );
2051                            }
2052                        }
2053                    }
2054                }
2055            }
2056
2057            let bundle_post = bundle
2058                .state
2059                .get(&ARBOS_STATE_ADDRESS)
2060                .and_then(|a| a.storage.get(&gas_backlog_slot))
2061                .map(|s| (s.present_value, s.previous_or_original_value));
2062            eprintln!("[C] Bundle post-augment: gas_backlog = {:?}", bundle_post);
2063
2064            // filter_unchanged_storage
2065            for (_addr, account) in bundle.state.iter_mut() {
2066                account
2067                    .storage
2068                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2069            }
2070
2071            let final_slot = bundle
2072                .state
2073                .get(&ARBOS_STATE_ADDRESS)
2074                .and_then(|a| a.storage.get(&gas_backlog_slot))
2075                .map(|s| s.present_value);
2076            eprintln!("[C] FINAL: gas_backlog in bundle = {:?}", final_slot);
2077            assert!(
2078                final_slot.is_some(),
2079                "VARIANT C FAILED: gas_backlog slot MISSING from bundle after EVM commit with ArbOS storage read"
2080            );
2081            assert_eq!(
2082                final_slot.unwrap(),
2083                U256::from(552756u64 + 357751u64),
2084                "VARIANT C: gas_backlog should be 910507"
2085            );
2086        }
2087
2088        // ===== VARIANT D: Two EVM commits (StartBlock + user tx) then grow_backlog =====
2089        // Most realistic production sequence
2090        eprintln!("\n===== VARIANT D: Two EVM commits then grow_backlog =====");
2091        {
2092            let mut state = StateBuilder::new()
2093                .with_database(EmptyDb)
2094                .with_bundle_update()
2095                .build();
2096
2097            ensure_cache_account(&mut state, ARBOS_STATE_ADDRESS);
2098            arb_storage::set_account_nonce(&mut state, ARBOS_STATE_ADDRESS, 1);
2099
2100            let state_ptr: *mut revm::database::State<EmptyDb> = &mut state;
2101
2102            let backing = Storage::new(state_ptr, B256::ZERO);
2103            let l2_sto = backing.open_sub_storage(&[1]);
2104            super::super::initialize_l2_pricing_state(&l2_sto);
2105
2106            // Set initial backlog
2107            let l2_pricing =
2108                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2109            l2_pricing.set_gas_backlog(552756).unwrap();
2110
2111            // Simulate StartBlock: update_pricing_model writes base_fee
2112            l2_pricing.update_pricing_model(0, 10).unwrap();
2113            eprintln!(
2114                "[D] After StartBlock: backlog={}",
2115                l2_pricing.gas_backlog().unwrap()
2116            );
2117
2118            // First EVM commit (StartBlock internal tx - empty output)
2119            state.commit(Default::default());
2120            eprintln!("[D] Committed StartBlock (empty)");
2121
2122            // Second EVM commit (user tx - touches sender + receiver, NOT ArbOS)
2123            let sender = address!("1111111111111111111111111111111111111111");
2124            let receiver = address!("2222222222222222222222222222222222222222");
2125            let _ = state.load_cache_account(sender);
2126            let _ = state.load_cache_account(receiver);
2127
2128            let mut user_changes: alloy_primitives::map::HashMap<Address, revm::state::Account> =
2129                Default::default();
2130            let mut sender_acct = revm::state::Account::default();
2131            sender_acct.info.balance = U256::from(999_000u64);
2132            sender_acct.info.nonce = 1;
2133            sender_acct.mark_touch();
2134            user_changes.insert(sender, sender_acct);
2135
2136            let mut receiver_acct = revm::state::Account::default();
2137            receiver_acct.info.balance = U256::from(1_000u64);
2138            receiver_acct.mark_touch();
2139            user_changes.insert(receiver, receiver_acct);
2140
2141            state.commit(user_changes);
2142            eprintln!("[D] Committed user tx (sender+receiver)");
2143
2144            // Check cache
2145            let cache_val_after_user = state
2146                .cache
2147                .accounts
2148                .get(&ARBOS_STATE_ADDRESS)
2149                .and_then(|ca| ca.account.as_ref())
2150                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
2151            eprintln!(
2152                "[D] gas_backlog in cache after user tx commit: {:?}",
2153                cache_val_after_user
2154            );
2155
2156            // Post-commit: grow_backlog (this is what happens in production after
2157            // commit_transaction)
2158            let l2_pricing2 =
2159                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2160            let read_val = l2_pricing2.gas_backlog().unwrap();
2161            eprintln!("[D] gas_backlog read before grow: {}", read_val);
2162            l2_pricing2
2163                .grow_backlog(357751, MultiGas::default())
2164                .unwrap();
2165            let after_grow = l2_pricing2.gas_backlog().unwrap();
2166            eprintln!("[D] gas_backlog after grow: {}", after_grow);
2167            assert_eq!(after_grow, 552756 + 357751, "backlog should be sum");
2168
2169            // merge + take_bundle
2170            state.merge_transitions(BundleRetention::Reverts);
2171            let mut bundle = state.take_bundle();
2172
2173            let bundle_pre = bundle
2174                .state
2175                .get(&ARBOS_STATE_ADDRESS)
2176                .and_then(|a| a.storage.get(&gas_backlog_slot))
2177                .map(|s| (s.present_value, s.previous_or_original_value));
2178            eprintln!("[D] Bundle pre-augment: gas_backlog = {:?}", bundle_pre);
2179
2180            // augment (inline)
2181            for (addr, cache_acct) in &state.cache.accounts {
2182                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
2183                let current_storage = cache_acct
2184                    .account
2185                    .as_ref()
2186                    .map(|a| &a.storage)
2187                    .cloned()
2188                    .unwrap_or_default();
2189
2190                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
2191                    bundle_acct.info = current_info;
2192                    for (key, value) in &current_storage {
2193                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
2194                            slot.present_value = *value;
2195                        } else {
2196                            let original_value = U256::ZERO;
2197                            if *value != original_value {
2198                                bundle_acct.storage.insert(
2199                                    *key,
2200                                    StorageSlot {
2201                                        previous_or_original_value: original_value,
2202                                        present_value: *value,
2203                                    },
2204                                );
2205                            }
2206                        }
2207                    }
2208                } else {
2209                    let storage_changes: alloy_primitives::map::HashMap<U256, StorageSlot> =
2210                        current_storage
2211                            .iter()
2212                            .filter_map(|(key, value)| {
2213                                let original_value = U256::ZERO;
2214                                if original_value != *value {
2215                                    Some((
2216                                        *key,
2217                                        StorageSlot {
2218                                            previous_or_original_value: original_value,
2219                                            present_value: *value,
2220                                        },
2221                                    ))
2222                                } else {
2223                                    None
2224                                }
2225                            })
2226                            .collect();
2227                    let info_changed = current_info.is_some();
2228                    if info_changed || !storage_changes.is_empty() {
2229                        bundle.state.insert(
2230                            *addr,
2231                            revm::database::BundleAccount {
2232                                info: current_info,
2233                                original_info: None,
2234                                storage: storage_changes,
2235                                status: revm::database::AccountStatus::InMemoryChange,
2236                            },
2237                        );
2238                    }
2239                }
2240            }
2241
2242            let bundle_post = bundle
2243                .state
2244                .get(&ARBOS_STATE_ADDRESS)
2245                .and_then(|a| a.storage.get(&gas_backlog_slot))
2246                .map(|s| (s.present_value, s.previous_or_original_value));
2247            eprintln!("[D] Bundle post-augment: gas_backlog = {:?}", bundle_post);
2248
2249            // filter
2250            for (_addr, account) in bundle.state.iter_mut() {
2251                account
2252                    .storage
2253                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2254            }
2255
2256            let final_slot = bundle
2257                .state
2258                .get(&ARBOS_STATE_ADDRESS)
2259                .and_then(|a| a.storage.get(&gas_backlog_slot))
2260                .map(|s| s.present_value);
2261            eprintln!("[D] FINAL: gas_backlog in bundle = {:?}", final_slot);
2262            assert!(
2263                final_slot.is_some(),
2264                "VARIANT D FAILED: gas_backlog slot MISSING from bundle"
2265            );
2266            assert_eq!(
2267                final_slot.unwrap(),
2268                U256::from(552756u64 + 357751u64),
2269                "VARIANT D: gas_backlog should be 910507"
2270            );
2271        }
2272
2273        // ===== VARIANT E: Database with PRE-EXISTING gas_backlog (production scenario) =====
2274        // In production, the state provider has the previous block's gas_backlog.
2275        // write_storage_at reads original_value from DB. If the DB already has the
2276        // value, the transition's original_value matches, and filter_unchanged_storage
2277        // may remove it.
2278        eprintln!("\n===== VARIANT E: Pre-existing DB state =====");
2279        {
2280            // Create a DB that returns the pre-existing backlog value
2281            struct PrePopulatedDb {
2282                gas_backlog_slot: U256,
2283                pre_existing_backlog: U256,
2284            }
2285
2286            impl Database for PrePopulatedDb {
2287                type Error = std::convert::Infallible;
2288                fn basic(
2289                    &mut self,
2290                    _address: Address,
2291                ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
2292                    // ArbOS account exists with nonce=1
2293                    Ok(Some(revm::state::AccountInfo {
2294                        balance: U256::ZERO,
2295                        nonce: 1,
2296                        code_hash: keccak256([]),
2297                        code: None,
2298                        account_id: None,
2299                    }))
2300                }
2301                fn code_by_hash(
2302                    &mut self,
2303                    _code_hash: B256,
2304                ) -> Result<revm::state::Bytecode, Self::Error> {
2305                    Ok(revm::state::Bytecode::default())
2306                }
2307                fn storage(&mut self, _address: Address, index: U256) -> Result<U256, Self::Error> {
2308                    // Return pre-existing backlog for the gas_backlog slot
2309                    if index == self.gas_backlog_slot {
2310                        Ok(self.pre_existing_backlog)
2311                    } else {
2312                        Ok(U256::ZERO)
2313                    }
2314                }
2315                fn block_hash(&mut self, _number: u64) -> Result<B256, Self::Error> {
2316                    Ok(B256::ZERO)
2317                }
2318            }
2319
2320            let pre_existing_backlog = U256::from(552756u64);
2321            let mut state = StateBuilder::new()
2322                .with_database(PrePopulatedDb {
2323                    gas_backlog_slot,
2324                    pre_existing_backlog,
2325                })
2326                .with_bundle_update()
2327                .build();
2328
2329            // Load ArbOS account from DB (nonce=1 already in DB)
2330            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
2331
2332            let state_ptr: *mut revm::database::State<PrePopulatedDb> = &mut state;
2333
2334            // Open L2 pricing state — gas_backlog already in DB as 552756
2335            let backing = Storage::new(state_ptr, B256::ZERO);
2336            let l2_pricing =
2337                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2338
2339            // Read current backlog — should come from DB
2340            let current = l2_pricing.gas_backlog().unwrap();
2341            eprintln!("[E] Initial gas_backlog (from DB): {}", current);
2342            assert_eq!(current, 552756, "Should read from DB");
2343
2344            // Simulate StartBlock: update_pricing_model(time_passed=0)
2345            l2_pricing.update_pricing_model(0, 10).unwrap();
2346            let after_start = l2_pricing.gas_backlog().unwrap();
2347            eprintln!(
2348                "[E] After StartBlock (time_passed=0): gas_backlog={}",
2349                after_start
2350            );
2351
2352            // EVM commit: empty (StartBlock internal tx)
2353            {
2354                use revm::DatabaseCommit;
2355                let empty: alloy_primitives::map::HashMap<Address, revm::state::Account> =
2356                    Default::default();
2357                state.commit(empty);
2358            }
2359
2360            // EVM commit: user tx touching only sender/receiver (NOT ArbOS)
2361            {
2362                use revm::DatabaseCommit;
2363                let sender = address!("1111111111111111111111111111111111111111");
2364                let _ = state.load_cache_account(sender);
2365                let mut user_changes: alloy_primitives::map::HashMap<
2366                    Address,
2367                    revm::state::Account,
2368                > = Default::default();
2369                let mut sender_acct = revm::state::Account::default();
2370                sender_acct.info.balance = U256::from(999_000u64);
2371                sender_acct.info.nonce = 1;
2372                sender_acct.mark_touch();
2373                user_changes.insert(sender, sender_acct);
2374                state.commit(user_changes);
2375            }
2376
2377            eprintln!("[E] After two EVM commits");
2378
2379            // Post-commit: grow_backlog
2380            let l2_pricing2 =
2381                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2382            let read_before = l2_pricing2.gas_backlog().unwrap();
2383            eprintln!("[E] gas_backlog before grow: {}", read_before);
2384
2385            l2_pricing2
2386                .grow_backlog(357751, MultiGas::default())
2387                .unwrap();
2388            let after_grow = l2_pricing2.gas_backlog().unwrap();
2389            eprintln!(
2390                "[E] gas_backlog after grow: {} (expect {})",
2391                after_grow,
2392                552756 + 357751
2393            );
2394
2395            // Check cache
2396            let cache_val = state
2397                .cache
2398                .accounts
2399                .get(&ARBOS_STATE_ADDRESS)
2400                .and_then(|ca| ca.account.as_ref())
2401                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
2402            eprintln!("[E] gas_backlog in cache: {:?}", cache_val);
2403
2404            // merge + take_bundle
2405            state.merge_transitions(BundleRetention::Reverts);
2406            let mut bundle = state.take_bundle();
2407
2408            let bundle_pre = bundle
2409                .state
2410                .get(&ARBOS_STATE_ADDRESS)
2411                .and_then(|a| a.storage.get(&gas_backlog_slot))
2412                .map(|s| (s.present_value, s.previous_or_original_value));
2413            eprintln!("[E] Bundle pre-augment: gas_backlog = {:?}", bundle_pre);
2414
2415            // augment (inline) — for PrePopulatedDb, original values come from DB
2416            for (addr, cache_acct) in &state.cache.accounts {
2417                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
2418                let current_storage = cache_acct
2419                    .account
2420                    .as_ref()
2421                    .map(|a| &a.storage)
2422                    .cloned()
2423                    .unwrap_or_default();
2424
2425                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
2426                    bundle_acct.info = current_info;
2427                    for (key, value) in &current_storage {
2428                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
2429                            slot.present_value = *value;
2430                        } else {
2431                            // Use pre_existing_backlog for DB lookup simulation
2432                            let original_value =
2433                                if *addr == ARBOS_STATE_ADDRESS && *key == gas_backlog_slot {
2434                                    pre_existing_backlog
2435                                } else {
2436                                    U256::ZERO
2437                                };
2438                            if *value != original_value {
2439                                bundle_acct.storage.insert(
2440                                    *key,
2441                                    StorageSlot {
2442                                        previous_or_original_value: original_value,
2443                                        present_value: *value,
2444                                    },
2445                                );
2446                            }
2447                        }
2448                    }
2449                } else {
2450                    // Account not in bundle
2451                    let storage_changes: alloy_primitives::map::HashMap<U256, StorageSlot> =
2452                        current_storage
2453                            .iter()
2454                            .filter_map(|(key, value)| {
2455                                let original_value =
2456                                    if *addr == ARBOS_STATE_ADDRESS && *key == gas_backlog_slot {
2457                                        pre_existing_backlog
2458                                    } else {
2459                                        U256::ZERO
2460                                    };
2461                                if original_value != *value {
2462                                    Some((
2463                                        *key,
2464                                        StorageSlot {
2465                                            previous_or_original_value: original_value,
2466                                            present_value: *value,
2467                                        },
2468                                    ))
2469                                } else {
2470                                    None
2471                                }
2472                            })
2473                            .collect();
2474                    let info_changed = false; // account existed in DB
2475                    if info_changed || !storage_changes.is_empty() {
2476                        bundle.state.insert(
2477                            *addr,
2478                            revm::database::BundleAccount {
2479                                info: current_info,
2480                                original_info: None,
2481                                storage: storage_changes,
2482                                status: revm::database::AccountStatus::Changed,
2483                            },
2484                        );
2485                    }
2486                }
2487            }
2488
2489            let bundle_post = bundle
2490                .state
2491                .get(&ARBOS_STATE_ADDRESS)
2492                .and_then(|a| a.storage.get(&gas_backlog_slot))
2493                .map(|s| (s.present_value, s.previous_or_original_value));
2494            eprintln!("[E] Bundle post-augment: gas_backlog = {:?}", bundle_post);
2495
2496            // filter_unchanged_storage
2497            for (_addr, account) in bundle.state.iter_mut() {
2498                account
2499                    .storage
2500                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2501            }
2502
2503            let final_slot = bundle
2504                .state
2505                .get(&ARBOS_STATE_ADDRESS)
2506                .and_then(|a| a.storage.get(&gas_backlog_slot))
2507                .map(|s| s.present_value);
2508            eprintln!("[E] FINAL: gas_backlog in bundle = {:?}", final_slot);
2509            assert!(
2510                final_slot.is_some(),
2511                "VARIANT E FAILED: gas_backlog slot MISSING from bundle (pre-populated DB)"
2512            );
2513            assert_eq!(
2514                final_slot.unwrap(),
2515                U256::from(552756u64 + 357751u64),
2516                "VARIANT E: gas_backlog should be 910507"
2517            );
2518        }
2519
2520        // ===== VARIANT F: Pre-existing DB + StartBlock DRAIN (time_passed > 0) =====
2521        // The most realistic production scenario: gas_backlog exists in DB,
2522        // StartBlock drains some, then user tx grows it back.
2523        // The drain writes the same slot, and the grow writes it again.
2524        // If drain writes backlog=0 and grow writes backlog=357751,
2525        // but the original_value from DB was 552756, the filter should keep it.
2526        // BUT: what if drain writes backlog=552756 (no change from DB) and the
2527        // transition records original_value=552756? Then grow writes 910507 with
2528        // original_value=552756. This should still work. Let's verify.
2529        eprintln!("\n===== VARIANT F: Pre-existing DB + StartBlock drain + grow =====");
2530        {
2531            struct PrePopulatedDb2 {
2532                gas_backlog_slot: U256,
2533                pre_existing_backlog: U256,
2534                speed_limit_slot: U256,
2535                speed_limit_value: U256,
2536            }
2537
2538            impl Database for PrePopulatedDb2 {
2539                type Error = std::convert::Infallible;
2540                fn basic(
2541                    &mut self,
2542                    _address: Address,
2543                ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
2544                    Ok(Some(revm::state::AccountInfo {
2545                        balance: U256::ZERO,
2546                        nonce: 1,
2547                        code_hash: keccak256([]),
2548                        code: None,
2549                        account_id: None,
2550                    }))
2551                }
2552                fn code_by_hash(
2553                    &mut self,
2554                    _code_hash: B256,
2555                ) -> Result<revm::state::Bytecode, Self::Error> {
2556                    Ok(revm::state::Bytecode::default())
2557                }
2558                fn storage(&mut self, _address: Address, index: U256) -> Result<U256, Self::Error> {
2559                    if index == self.gas_backlog_slot {
2560                        Ok(self.pre_existing_backlog)
2561                    } else if index == self.speed_limit_slot {
2562                        Ok(self.speed_limit_value)
2563                    } else {
2564                        Ok(U256::ZERO)
2565                    }
2566                }
2567                fn block_hash(&mut self, _number: u64) -> Result<B256, Self::Error> {
2568                    Ok(B256::ZERO)
2569                }
2570            }
2571
2572            // Compute speed_limit slot
2573            let speed_limit_slot = arb_storage::storage_key_map(l2_base.as_slice(), 0); // offset 0
2574            let pre_existing_backlog = U256::from(552756u64);
2575
2576            let mut state = StateBuilder::new()
2577                .with_database(PrePopulatedDb2 {
2578                    gas_backlog_slot,
2579                    pre_existing_backlog,
2580                    speed_limit_slot,
2581                    speed_limit_value: U256::from(7_000_000u64), // 7M gas/sec
2582                })
2583                .with_bundle_update()
2584                .build();
2585
2586            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
2587            let state_ptr: *mut revm::database::State<PrePopulatedDb2> = &mut state;
2588
2589            let backing = Storage::new(state_ptr, B256::ZERO);
2590            let l2_pricing =
2591                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2592
2593            let initial = l2_pricing.gas_backlog().unwrap();
2594            eprintln!("[F] Initial gas_backlog: {}", initial);
2595
2596            // StartBlock with time_passed=1 → drain = 1 * 7_000_000 = 7M
2597            // 552756 - 7M = 0 (saturating sub)
2598            l2_pricing.update_pricing_model(1, 10).unwrap();
2599            let after_drain = l2_pricing.gas_backlog().unwrap();
2600            eprintln!(
2601                "[F] After drain (time_passed=1, speed=7M): gas_backlog={} (was {})",
2602                after_drain, initial
2603            );
2604
2605            // EVM commit
2606            {
2607                use revm::DatabaseCommit;
2608                state.commit(Default::default());
2609            }
2610
2611            // grow_backlog
2612            let l2_pricing2 =
2613                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2614            l2_pricing2
2615                .grow_backlog(357751, MultiGas::default())
2616                .unwrap();
2617            let after_grow = l2_pricing2.gas_backlog().unwrap();
2618            eprintln!("[F] After grow(357751): gas_backlog={}", after_grow);
2619
2620            // merge + take_bundle
2621            state.merge_transitions(BundleRetention::Reverts);
2622            let mut bundle = state.take_bundle();
2623
2624            let bundle_pre = bundle
2625                .state
2626                .get(&ARBOS_STATE_ADDRESS)
2627                .and_then(|a| a.storage.get(&gas_backlog_slot))
2628                .map(|s| (s.present_value, s.previous_or_original_value));
2629            eprintln!("[F] Bundle pre-filter: gas_backlog = {:?}", bundle_pre);
2630
2631            // Note: In this variant we skip augment since all writes go through
2632            // write_storage_at which creates transitions. The bundle should
2633            // already have the slot.
2634
2635            // filter
2636            for (_addr, account) in bundle.state.iter_mut() {
2637                account
2638                    .storage
2639                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2640            }
2641
2642            let final_slot = bundle
2643                .state
2644                .get(&ARBOS_STATE_ADDRESS)
2645                .and_then(|a| a.storage.get(&gas_backlog_slot))
2646                .map(|s| (s.present_value, s.previous_or_original_value));
2647            eprintln!("[F] FINAL: gas_backlog = {:?}", final_slot);
2648
2649            // Drain brought it to 0, grow added 357751 → final = 357751
2650            // original from DB = 552756
2651            // present=357751, original=552756 → different → should survive filter
2652            assert!(
2653                final_slot.is_some(),
2654                "VARIANT F FAILED: gas_backlog slot MISSING from bundle (drain+grow)"
2655            );
2656            assert_eq!(
2657                final_slot.unwrap().0,
2658                U256::from(357751u64),
2659                "VARIANT F: gas_backlog should be 357751"
2660            );
2661        }
2662
2663        // ===== VARIANT G: Pre-existing DB + drain to 0 + NO grow =====
2664        // Edge case: if backlog drains to 0 and no user tx grows it,
2665        // the write is 0 and original from DB is 552756.
2666        // write_storage_at should NOT skip this (0 != 552756).
2667        // But wait — what if update_pricing_model drains to 0, and
2668        // write_storage_at's no-op check sees value=0 and prev_value=0?
2669        // This would happen if the cache already has backlog=0 from a previous
2670        // write... Let's check.
2671        eprintln!("\n===== VARIANT G: Drain to 0 (no grow) - write not lost? =====");
2672        {
2673            struct PrePopDb3 {
2674                gas_backlog_slot: U256,
2675                speed_limit_slot: U256,
2676            }
2677
2678            impl Database for PrePopDb3 {
2679                type Error = std::convert::Infallible;
2680                fn basic(
2681                    &mut self,
2682                    _address: Address,
2683                ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
2684                    Ok(Some(revm::state::AccountInfo {
2685                        balance: U256::ZERO,
2686                        nonce: 1,
2687                        code_hash: keccak256([]),
2688                        code: None,
2689                        account_id: None,
2690                    }))
2691                }
2692                fn code_by_hash(
2693                    &mut self,
2694                    _code_hash: B256,
2695                ) -> Result<revm::state::Bytecode, Self::Error> {
2696                    Ok(revm::state::Bytecode::default())
2697                }
2698                fn storage(&mut self, _address: Address, index: U256) -> Result<U256, Self::Error> {
2699                    if index == self.gas_backlog_slot {
2700                        Ok(U256::from(552756u64))
2701                    } else if index == self.speed_limit_slot {
2702                        Ok(U256::from(7_000_000u64))
2703                    } else {
2704                        Ok(U256::ZERO)
2705                    }
2706                }
2707                fn block_hash(&mut self, _number: u64) -> Result<B256, Self::Error> {
2708                    Ok(B256::ZERO)
2709                }
2710            }
2711
2712            let speed_limit_slot = arb_storage::storage_key_map(l2_base.as_slice(), 0);
2713
2714            let mut state = StateBuilder::new()
2715                .with_database(PrePopDb3 {
2716                    gas_backlog_slot,
2717                    speed_limit_slot,
2718                })
2719                .with_bundle_update()
2720                .build();
2721
2722            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
2723            let state_ptr: *mut revm::database::State<PrePopDb3> = &mut state;
2724
2725            let backing = Storage::new(state_ptr, B256::ZERO);
2726            let l2_pricing =
2727                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2728
2729            let initial = l2_pricing.gas_backlog().unwrap();
2730            eprintln!("[G] Initial gas_backlog: {}", initial);
2731
2732            // Drain with time_passed=1 → 552756 - 7M = 0
2733            l2_pricing.update_pricing_model(1, 10).unwrap();
2734            let after_drain = l2_pricing.gas_backlog().unwrap();
2735            eprintln!("[G] After drain: gas_backlog={}", after_drain);
2736            assert_eq!(after_drain, 0);
2737
2738            // merge + take_bundle
2739            state.merge_transitions(BundleRetention::Reverts);
2740            let mut bundle = state.take_bundle();
2741
2742            let pre_filter = bundle
2743                .state
2744                .get(&ARBOS_STATE_ADDRESS)
2745                .and_then(|a| a.storage.get(&gas_backlog_slot))
2746                .map(|s| (s.present_value, s.previous_or_original_value));
2747            eprintln!("[G] Bundle pre-filter: gas_backlog = {:?}", pre_filter);
2748
2749            // filter
2750            for (_addr, account) in bundle.state.iter_mut() {
2751                account
2752                    .storage
2753                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2754            }
2755
2756            let final_slot = bundle
2757                .state
2758                .get(&ARBOS_STATE_ADDRESS)
2759                .and_then(|a| a.storage.get(&gas_backlog_slot))
2760                .map(|s| (s.present_value, s.previous_or_original_value));
2761            eprintln!("[G] FINAL after filter: gas_backlog = {:?}", final_slot);
2762
2763            // present=0, original=552756 → different → should survive
2764            assert!(
2765                final_slot.is_some(),
2766                "VARIANT G FAILED: drain-to-0 write was lost!"
2767            );
2768        }
2769
2770        eprintln!("\n===== ALL VARIANTS PASSED =====");
2771    }
2772}