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