arbos/l2_pricing/
model.rs

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