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: revm::state::EvmState = 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: revm::state::EvmState = 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: revm::state::EvmState = 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: revm::state::EvmState = 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: revm::database::StorageWithOriginalValues =
1475                        Default::default();
1476                    for (key, value) in &plain.storage {
1477                        let original = state.database.storage(arbos, *key).unwrap_or(U256::ZERO);
1478                        if *value != original {
1479                            storage_changes.insert(
1480                                *key,
1481                                StorageSlot {
1482                                    previous_or_original_value: original,
1483                                    present_value: *value,
1484                                },
1485                            );
1486                        }
1487                    }
1488                    if !storage_changes.is_empty() {
1489                        bundle.state.insert(
1490                            arbos,
1491                            revm::database::BundleAccount {
1492                                info: Some(plain.info.clone()),
1493                                original_info: None,
1494                                storage: storage_changes,
1495                                status: revm::database::AccountStatus::Changed,
1496                            },
1497                        );
1498                    }
1499                }
1500            }
1501        }
1502
1503        let _after_augment = bundle
1504            .state
1505            .get(&arbos)
1506            .and_then(|a| a.storage.get(&gas_backlog_slot))
1507            .map(|s| s.present_value);
1508
1509        // filter_unchanged_storage
1510        for (_addr, account) in bundle.state.iter_mut() {
1511            account
1512                .storage
1513                .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
1514        }
1515
1516        let after_filter = bundle
1517            .state
1518            .get(&arbos)
1519            .and_then(|a| a.storage.get(&gas_backlog_slot))
1520            .map(|s| s.present_value);
1521
1522        assert_eq!(
1523            after_filter,
1524            Some(U256::from(910_507u64)),
1525            "gasBacklog MUST survive even when transition_state was consumed mid-block"
1526        );
1527    }
1528
1529    /// Simulates the full production flow step-by-step to find why
1530    /// gas_backlog writes are lost. Tests the interaction between:
1531    /// - ArbOS storage writes (via Storage/write_storage_at)
1532    /// - EVM state commits (state.commit)
1533    /// - Bundle construction (merge_transitions + take_bundle)
1534    /// - Post-bundle augmentation (augment_bundle_from_cache logic)
1535    /// - Storage filtering (filter_unchanged_storage logic)
1536    #[test]
1537    fn test_grow_backlog_survives_evm_commit_and_augment() {
1538        use revm::{
1539            DatabaseCommit,
1540            database::states::{bundle_state::BundleRetention, plain_account::StorageSlot},
1541        };
1542
1543        // Compute the actual gasBacklog slot for assertions
1544        let l2_base = keccak256([1u8]); // open_sub_storage([1]) from root
1545        let gas_backlog_offset: u64 = 4;
1546        let gas_backlog_slot = arb_storage::storage_key_map(l2_base.as_slice(), gas_backlog_offset);
1547
1548        // ===== VARIANT A: EVM commit with EMPTY HashMap (no ArbOS account touched) =====
1549        {
1550            let mut state = StateBuilder::new()
1551                .with_database(EmptyDb)
1552                .with_bundle_update()
1553                .build();
1554
1555            // Step 1: Ensure ArbOS account exists with nonce=1
1556            ensure_cache_account(&mut state, ARBOS_STATE_ADDRESS);
1557            arb_storage::set_account_nonce(&mut state, ARBOS_STATE_ADDRESS, 1);
1558
1559            let state_ptr: *mut revm::database::State<EmptyDb> = &mut state;
1560
1561            // Step 2: Initialize L2 pricing state
1562            let backing = Storage::new(unsafe { &mut *state_ptr }, B256::ZERO);
1563            let l2_sto = backing.open_sub_storage(&[1]);
1564            super::super::initialize_l2_pricing_state(&l2_sto, unsafe { &mut *state_ptr }).unwrap();
1565
1566            // Step 3: Set gas_backlog to 552756 (simulate pre-existing backlog)
1567            let l2_pricing =
1568                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1569            l2_pricing
1570                .set_gas_backlog(unsafe { &mut *state_ptr }, 552756)
1571                .unwrap();
1572            let pre_start = l2_pricing.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
1573            assert_eq!(pre_start, 552756, "Pre-existing backlog should be 552756");
1574
1575            // Step 4: Simulate StartBlock: update_pricing_model(time_passed=0)
1576            l2_pricing
1577                .update_pricing_model(unsafe { &mut *state_ptr }, 0, 10)
1578                .unwrap();
1579            let after_start = l2_pricing.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
1580            assert_eq!(
1581                after_start, 552756,
1582                "time_passed=0 should not change backlog"
1583            );
1584
1585            // Step 5: EVM commit with empty HashMap
1586            let empty_state: revm::state::EvmState = 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: revm::database::StorageWithOriginalValues =
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: revm::state::EvmState = Default::default();
1765            evm_changes.insert(ARBOS_STATE_ADDRESS, arbos_evm_account);
1766            state.commit(evm_changes);
1767
1768            // Now grow_backlog AFTER the EVM commit
1769            let l2_pricing2 =
1770                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1771            let _read_before_grow = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
1772
1773            l2_pricing2
1774                .grow_backlog(unsafe { &mut *state_ptr }, 357751, MultiGas::default())
1775                .unwrap();
1776            let _after_grow = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
1777
1778            // Check cache after grow
1779            let _cache_val2 = state
1780                .cache
1781                .accounts
1782                .get(&ARBOS_STATE_ADDRESS)
1783                .and_then(|ca| ca.account.as_ref())
1784                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
1785
1786            // merge + take_bundle
1787            state.merge_transitions(BundleRetention::Reverts);
1788            let mut bundle = state.take_bundle();
1789
1790            let _bundle_pre = bundle
1791                .state
1792                .get(&ARBOS_STATE_ADDRESS)
1793                .and_then(|a| a.storage.get(&gas_backlog_slot))
1794                .map(|s| (s.present_value, s.previous_or_original_value));
1795
1796            // augment_bundle_from_cache (inline)
1797            for (addr, cache_acct) in &state.cache.accounts {
1798                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
1799                let current_storage = cache_acct
1800                    .account
1801                    .as_ref()
1802                    .map(|a| &a.storage)
1803                    .cloned()
1804                    .unwrap_or_default();
1805
1806                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
1807                    bundle_acct.info = current_info;
1808                    for (key, value) in &current_storage {
1809                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
1810                            slot.present_value = *value;
1811                        } else {
1812                            let original_value = U256::ZERO;
1813                            if *value != original_value {
1814                                bundle_acct.storage.insert(
1815                                    *key,
1816                                    StorageSlot {
1817                                        previous_or_original_value: original_value,
1818                                        present_value: *value,
1819                                    },
1820                                );
1821                            }
1822                        }
1823                    }
1824                }
1825            }
1826
1827            let _bundle_post = bundle
1828                .state
1829                .get(&ARBOS_STATE_ADDRESS)
1830                .and_then(|a| a.storage.get(&gas_backlog_slot))
1831                .map(|s| (s.present_value, s.previous_or_original_value));
1832
1833            // filter_unchanged_storage
1834            for (_addr, account) in bundle.state.iter_mut() {
1835                account
1836                    .storage
1837                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
1838            }
1839
1840            let final_slot = bundle
1841                .state
1842                .get(&ARBOS_STATE_ADDRESS)
1843                .and_then(|a| a.storage.get(&gas_backlog_slot))
1844                .map(|s| s.present_value);
1845            assert!(
1846                final_slot.is_some(),
1847                "VARIANT B FAILED: gas_backlog slot MISSING from bundle after EVM commit with ArbOS touched"
1848            );
1849            assert_eq!(
1850                final_slot.unwrap(),
1851                U256::from(552756u64 + 357751u64),
1852                "VARIANT B: gas_backlog should be 910507"
1853            );
1854        }
1855
1856        // ===== VARIANT C: EVM commit WITH ArbOS account AND storage slot that was read =====
1857        // This simulates the most realistic case: EVM reads gasBacklog slot during
1858        // execution (e.g., GetPricesInWei precompile reads ArbOS state), and the
1859        // slot appears in EVM output with is_changed()=false but present in Account.storage
1860        {
1861            let mut state = StateBuilder::new()
1862                .with_database(EmptyDb)
1863                .with_bundle_update()
1864                .build();
1865
1866            ensure_cache_account(&mut state, ARBOS_STATE_ADDRESS);
1867            arb_storage::set_account_nonce(&mut state, ARBOS_STATE_ADDRESS, 1);
1868
1869            let state_ptr: *mut revm::database::State<EmptyDb> = &mut state;
1870
1871            let backing = Storage::new(unsafe { &mut *state_ptr }, B256::ZERO);
1872            let l2_sto = backing.open_sub_storage(&[1]);
1873            super::super::initialize_l2_pricing_state(&l2_sto, unsafe { &mut *state_ptr }).unwrap();
1874
1875            let l2_pricing =
1876                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1877            l2_pricing
1878                .set_gas_backlog(unsafe { &mut *state_ptr }, 552756)
1879                .unwrap();
1880            l2_pricing
1881                .update_pricing_model(unsafe { &mut *state_ptr }, 0, 10)
1882                .unwrap();
1883
1884            // EVM commit with ArbOS account touched AND a storage slot that was
1885            // read but not written (EvmStorageSlot with original_value == present_value).
1886            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
1887            let mut arbos_evm_account = revm::state::Account {
1888                info: revm::state::AccountInfo {
1889                    balance: U256::ZERO,
1890                    nonce: 1,
1891                    code_hash: keccak256([]),
1892                    code: None,
1893                    account_id: None,
1894                },
1895                ..Default::default()
1896            };
1897            arbos_evm_account.mark_touch();
1898
1899            // Add gas_backlog slot as READ-ONLY (original == present, is_changed()=false)
1900            // This is what happens when the EVM loads a storage slot via SLOAD
1901            arbos_evm_account.storage.insert(
1902                gas_backlog_slot,
1903                revm::state::EvmStorageSlot::new(U256::from(552756u64), 0),
1904                // new() sets original_value = present_value, so is_changed() = false
1905            );
1906
1907            let mut evm_changes: revm::state::EvmState = Default::default();
1908            evm_changes.insert(ARBOS_STATE_ADDRESS, arbos_evm_account);
1909            state.commit(evm_changes);
1910
1911            // grow_backlog after commit
1912            let l2_pricing2 =
1913                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
1914            let _read_before_grow = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
1915
1916            l2_pricing2
1917                .grow_backlog(unsafe { &mut *state_ptr }, 357751, MultiGas::default())
1918                .unwrap();
1919            let _after_grow = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
1920
1921            // merge + take_bundle
1922            state.merge_transitions(BundleRetention::Reverts);
1923            let mut bundle = state.take_bundle();
1924
1925            let _bundle_pre = bundle
1926                .state
1927                .get(&ARBOS_STATE_ADDRESS)
1928                .and_then(|a| a.storage.get(&gas_backlog_slot))
1929                .map(|s| (s.present_value, s.previous_or_original_value));
1930
1931            // augment (inline)
1932            for (addr, cache_acct) in &state.cache.accounts {
1933                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
1934                let current_storage = cache_acct
1935                    .account
1936                    .as_ref()
1937                    .map(|a| &a.storage)
1938                    .cloned()
1939                    .unwrap_or_default();
1940
1941                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
1942                    bundle_acct.info = current_info;
1943                    for (key, value) in &current_storage {
1944                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
1945                            slot.present_value = *value;
1946                        } else {
1947                            let original_value = U256::ZERO;
1948                            if *value != original_value {
1949                                bundle_acct.storage.insert(
1950                                    *key,
1951                                    StorageSlot {
1952                                        previous_or_original_value: original_value,
1953                                        present_value: *value,
1954                                    },
1955                                );
1956                            }
1957                        }
1958                    }
1959                }
1960            }
1961
1962            let _bundle_post = bundle
1963                .state
1964                .get(&ARBOS_STATE_ADDRESS)
1965                .and_then(|a| a.storage.get(&gas_backlog_slot))
1966                .map(|s| (s.present_value, s.previous_or_original_value));
1967
1968            // filter_unchanged_storage
1969            for (_addr, account) in bundle.state.iter_mut() {
1970                account
1971                    .storage
1972                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
1973            }
1974
1975            let final_slot = bundle
1976                .state
1977                .get(&ARBOS_STATE_ADDRESS)
1978                .and_then(|a| a.storage.get(&gas_backlog_slot))
1979                .map(|s| s.present_value);
1980            assert!(
1981                final_slot.is_some(),
1982                "VARIANT C FAILED: gas_backlog slot MISSING from bundle after EVM commit with ArbOS storage read"
1983            );
1984            assert_eq!(
1985                final_slot.unwrap(),
1986                U256::from(552756u64 + 357751u64),
1987                "VARIANT C: gas_backlog should be 910507"
1988            );
1989        }
1990
1991        // ===== VARIANT D: Two EVM commits (StartBlock + user tx) then grow_backlog =====
1992        // Most realistic production sequence
1993        {
1994            let mut state = StateBuilder::new()
1995                .with_database(EmptyDb)
1996                .with_bundle_update()
1997                .build();
1998
1999            ensure_cache_account(&mut state, ARBOS_STATE_ADDRESS);
2000            arb_storage::set_account_nonce(&mut state, ARBOS_STATE_ADDRESS, 1);
2001
2002            let state_ptr: *mut revm::database::State<EmptyDb> = &mut state;
2003
2004            let backing = Storage::new(unsafe { &mut *state_ptr }, B256::ZERO);
2005            let l2_sto = backing.open_sub_storage(&[1]);
2006            super::super::initialize_l2_pricing_state(&l2_sto, unsafe { &mut *state_ptr }).unwrap();
2007
2008            // Set initial backlog
2009            let l2_pricing =
2010                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2011            l2_pricing
2012                .set_gas_backlog(unsafe { &mut *state_ptr }, 552756)
2013                .unwrap();
2014
2015            // Simulate StartBlock: update_pricing_model writes base_fee
2016            l2_pricing
2017                .update_pricing_model(unsafe { &mut *state_ptr }, 0, 10)
2018                .unwrap();
2019
2020            // First EVM commit (StartBlock internal tx - empty output)
2021            state.commit(Default::default());
2022
2023            // Second EVM commit (user tx - touches sender + receiver, NOT ArbOS)
2024            let sender = address!("1111111111111111111111111111111111111111");
2025            let receiver = address!("2222222222222222222222222222222222222222");
2026            let _ = state.load_cache_account(sender);
2027            let _ = state.load_cache_account(receiver);
2028
2029            let mut user_changes: revm::state::EvmState = Default::default();
2030            let mut sender_acct = revm::state::Account::default();
2031            sender_acct.info.balance = U256::from(999_000u64);
2032            sender_acct.info.nonce = 1;
2033            sender_acct.mark_touch();
2034            user_changes.insert(sender, sender_acct);
2035
2036            let mut receiver_acct = revm::state::Account::default();
2037            receiver_acct.info.balance = U256::from(1_000u64);
2038            receiver_acct.mark_touch();
2039            user_changes.insert(receiver, receiver_acct);
2040
2041            state.commit(user_changes);
2042
2043            // Check cache
2044            let _cache_val_after_user = state
2045                .cache
2046                .accounts
2047                .get(&ARBOS_STATE_ADDRESS)
2048                .and_then(|ca| ca.account.as_ref())
2049                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
2050
2051            // Post-commit: grow_backlog (this is what happens in production after
2052            // commit_transaction)
2053            let l2_pricing2 =
2054                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2055            let _read_val = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2056            l2_pricing2
2057                .grow_backlog(unsafe { &mut *state_ptr }, 357751, MultiGas::default())
2058                .unwrap();
2059            let after_grow = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2060            assert_eq!(after_grow, 552756 + 357751, "backlog should be sum");
2061
2062            // merge + take_bundle
2063            state.merge_transitions(BundleRetention::Reverts);
2064            let mut bundle = state.take_bundle();
2065
2066            let _bundle_pre = bundle
2067                .state
2068                .get(&ARBOS_STATE_ADDRESS)
2069                .and_then(|a| a.storage.get(&gas_backlog_slot))
2070                .map(|s| (s.present_value, s.previous_or_original_value));
2071
2072            // augment (inline)
2073            for (addr, cache_acct) in &state.cache.accounts {
2074                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
2075                let current_storage = cache_acct
2076                    .account
2077                    .as_ref()
2078                    .map(|a| &a.storage)
2079                    .cloned()
2080                    .unwrap_or_default();
2081
2082                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
2083                    bundle_acct.info = current_info;
2084                    for (key, value) in &current_storage {
2085                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
2086                            slot.present_value = *value;
2087                        } else {
2088                            let original_value = U256::ZERO;
2089                            if *value != original_value {
2090                                bundle_acct.storage.insert(
2091                                    *key,
2092                                    StorageSlot {
2093                                        previous_or_original_value: original_value,
2094                                        present_value: *value,
2095                                    },
2096                                );
2097                            }
2098                        }
2099                    }
2100                } else {
2101                    let storage_changes: revm::database::StorageWithOriginalValues =
2102                        current_storage
2103                            .iter()
2104                            .filter_map(|(key, value)| {
2105                                let original_value = U256::ZERO;
2106                                if original_value != *value {
2107                                    Some((
2108                                        *key,
2109                                        StorageSlot {
2110                                            previous_or_original_value: original_value,
2111                                            present_value: *value,
2112                                        },
2113                                    ))
2114                                } else {
2115                                    None
2116                                }
2117                            })
2118                            .collect();
2119                    let info_changed = current_info.is_some();
2120                    if info_changed || !storage_changes.is_empty() {
2121                        bundle.state.insert(
2122                            *addr,
2123                            revm::database::BundleAccount {
2124                                info: current_info,
2125                                original_info: None,
2126                                storage: storage_changes,
2127                                status: revm::database::AccountStatus::InMemoryChange,
2128                            },
2129                        );
2130                    }
2131                }
2132            }
2133
2134            let _bundle_post = bundle
2135                .state
2136                .get(&ARBOS_STATE_ADDRESS)
2137                .and_then(|a| a.storage.get(&gas_backlog_slot))
2138                .map(|s| (s.present_value, s.previous_or_original_value));
2139
2140            // filter
2141            for (_addr, account) in bundle.state.iter_mut() {
2142                account
2143                    .storage
2144                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2145            }
2146
2147            let final_slot = bundle
2148                .state
2149                .get(&ARBOS_STATE_ADDRESS)
2150                .and_then(|a| a.storage.get(&gas_backlog_slot))
2151                .map(|s| s.present_value);
2152            assert!(
2153                final_slot.is_some(),
2154                "VARIANT D FAILED: gas_backlog slot MISSING from bundle"
2155            );
2156            assert_eq!(
2157                final_slot.unwrap(),
2158                U256::from(552756u64 + 357751u64),
2159                "VARIANT D: gas_backlog should be 910507"
2160            );
2161        }
2162
2163        // ===== VARIANT E: Database with PRE-EXISTING gas_backlog (production scenario) =====
2164        // In production, the state provider has the previous block's gas_backlog.
2165        // write_storage_at reads original_value from DB. If the DB already has the
2166        // value, the transition's original_value matches, and filter_unchanged_storage
2167        // may remove it.
2168        {
2169            // Create a DB that returns the pre-existing backlog value
2170            struct PrePopulatedDb {
2171                gas_backlog_slot: U256,
2172                pre_existing_backlog: U256,
2173            }
2174
2175            impl Database for PrePopulatedDb {
2176                type Error = std::convert::Infallible;
2177                fn basic(
2178                    &mut self,
2179                    _address: Address,
2180                ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
2181                    // ArbOS account exists with nonce=1
2182                    Ok(Some(revm::state::AccountInfo {
2183                        balance: U256::ZERO,
2184                        nonce: 1,
2185                        code_hash: keccak256([]),
2186                        code: None,
2187                        account_id: None,
2188                    }))
2189                }
2190                fn code_by_hash(
2191                    &mut self,
2192                    _code_hash: B256,
2193                ) -> Result<revm::state::Bytecode, Self::Error> {
2194                    Ok(revm::state::Bytecode::default())
2195                }
2196                fn storage(&mut self, _address: Address, index: U256) -> Result<U256, Self::Error> {
2197                    // Return pre-existing backlog for the gas_backlog slot
2198                    if index == self.gas_backlog_slot {
2199                        Ok(self.pre_existing_backlog)
2200                    } else {
2201                        Ok(U256::ZERO)
2202                    }
2203                }
2204                fn block_hash(&mut self, _number: u64) -> Result<B256, Self::Error> {
2205                    Ok(B256::ZERO)
2206                }
2207            }
2208
2209            let pre_existing_backlog = U256::from(552756u64);
2210            let mut state = StateBuilder::new()
2211                .with_database(PrePopulatedDb {
2212                    gas_backlog_slot,
2213                    pre_existing_backlog,
2214                })
2215                .with_bundle_update()
2216                .build();
2217
2218            // Load ArbOS account from DB (nonce=1 already in DB)
2219            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
2220
2221            let state_ptr: *mut revm::database::State<PrePopulatedDb> = &mut state;
2222
2223            // Open L2 pricing state — gas_backlog already in DB as 552756
2224            let backing = Storage::new(unsafe { &mut *state_ptr }, B256::ZERO);
2225            let l2_pricing =
2226                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2227
2228            // Read current backlog — should come from DB
2229            let current = l2_pricing.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2230            assert_eq!(current, 552756, "Should read from DB");
2231
2232            // Simulate StartBlock: update_pricing_model(time_passed=0)
2233            l2_pricing
2234                .update_pricing_model(unsafe { &mut *state_ptr }, 0, 10)
2235                .unwrap();
2236            let _after_start = l2_pricing.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2237
2238            // EVM commit: empty (StartBlock internal tx)
2239            {
2240                use revm::DatabaseCommit;
2241                let empty: revm::state::EvmState = Default::default();
2242                state.commit(empty);
2243            }
2244
2245            // EVM commit: user tx touching only sender/receiver (NOT ArbOS)
2246            {
2247                use revm::DatabaseCommit;
2248                let sender = address!("1111111111111111111111111111111111111111");
2249                let _ = state.load_cache_account(sender);
2250                let mut user_changes: revm::state::EvmState = Default::default();
2251                let mut sender_acct = revm::state::Account::default();
2252                sender_acct.info.balance = U256::from(999_000u64);
2253                sender_acct.info.nonce = 1;
2254                sender_acct.mark_touch();
2255                user_changes.insert(sender, sender_acct);
2256                state.commit(user_changes);
2257            }
2258
2259            // Post-commit: grow_backlog
2260            let l2_pricing2 =
2261                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2262            let _read_before = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2263
2264            l2_pricing2
2265                .grow_backlog(unsafe { &mut *state_ptr }, 357751, MultiGas::default())
2266                .unwrap();
2267            let _after_grow = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2268
2269            // Check cache
2270            let _cache_val = state
2271                .cache
2272                .accounts
2273                .get(&ARBOS_STATE_ADDRESS)
2274                .and_then(|ca| ca.account.as_ref())
2275                .and_then(|a| a.storage.get(&gas_backlog_slot).copied());
2276
2277            // merge + take_bundle
2278            state.merge_transitions(BundleRetention::Reverts);
2279            let mut bundle = state.take_bundle();
2280
2281            let _bundle_pre = bundle
2282                .state
2283                .get(&ARBOS_STATE_ADDRESS)
2284                .and_then(|a| a.storage.get(&gas_backlog_slot))
2285                .map(|s| (s.present_value, s.previous_or_original_value));
2286
2287            // augment (inline) — for PrePopulatedDb, original values come from DB
2288            for (addr, cache_acct) in &state.cache.accounts {
2289                let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
2290                let current_storage = cache_acct
2291                    .account
2292                    .as_ref()
2293                    .map(|a| &a.storage)
2294                    .cloned()
2295                    .unwrap_or_default();
2296
2297                if let Some(bundle_acct) = bundle.state.get_mut(addr) {
2298                    bundle_acct.info = current_info;
2299                    for (key, value) in &current_storage {
2300                        if let Some(slot) = bundle_acct.storage.get_mut(key) {
2301                            slot.present_value = *value;
2302                        } else {
2303                            // Use pre_existing_backlog for DB lookup simulation
2304                            let original_value =
2305                                if *addr == ARBOS_STATE_ADDRESS && *key == gas_backlog_slot {
2306                                    pre_existing_backlog
2307                                } else {
2308                                    U256::ZERO
2309                                };
2310                            if *value != original_value {
2311                                bundle_acct.storage.insert(
2312                                    *key,
2313                                    StorageSlot {
2314                                        previous_or_original_value: original_value,
2315                                        present_value: *value,
2316                                    },
2317                                );
2318                            }
2319                        }
2320                    }
2321                } else {
2322                    // Account not in bundle
2323                    let storage_changes: revm::database::StorageWithOriginalValues =
2324                        current_storage
2325                            .iter()
2326                            .filter_map(|(key, value)| {
2327                                let original_value =
2328                                    if *addr == ARBOS_STATE_ADDRESS && *key == gas_backlog_slot {
2329                                        pre_existing_backlog
2330                                    } else {
2331                                        U256::ZERO
2332                                    };
2333                                if original_value != *value {
2334                                    Some((
2335                                        *key,
2336                                        StorageSlot {
2337                                            previous_or_original_value: original_value,
2338                                            present_value: *value,
2339                                        },
2340                                    ))
2341                                } else {
2342                                    None
2343                                }
2344                            })
2345                            .collect();
2346                    let info_changed = false; // account existed in DB
2347                    if info_changed || !storage_changes.is_empty() {
2348                        bundle.state.insert(
2349                            *addr,
2350                            revm::database::BundleAccount {
2351                                info: current_info,
2352                                original_info: None,
2353                                storage: storage_changes,
2354                                status: revm::database::AccountStatus::Changed,
2355                            },
2356                        );
2357                    }
2358                }
2359            }
2360
2361            let _bundle_post = bundle
2362                .state
2363                .get(&ARBOS_STATE_ADDRESS)
2364                .and_then(|a| a.storage.get(&gas_backlog_slot))
2365                .map(|s| (s.present_value, s.previous_or_original_value));
2366
2367            // filter_unchanged_storage
2368            for (_addr, account) in bundle.state.iter_mut() {
2369                account
2370                    .storage
2371                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2372            }
2373
2374            let final_slot = bundle
2375                .state
2376                .get(&ARBOS_STATE_ADDRESS)
2377                .and_then(|a| a.storage.get(&gas_backlog_slot))
2378                .map(|s| s.present_value);
2379            assert!(
2380                final_slot.is_some(),
2381                "VARIANT E FAILED: gas_backlog slot MISSING from bundle (pre-populated DB)"
2382            );
2383            assert_eq!(
2384                final_slot.unwrap(),
2385                U256::from(552756u64 + 357751u64),
2386                "VARIANT E: gas_backlog should be 910507"
2387            );
2388        }
2389
2390        // ===== VARIANT F: Pre-existing DB + StartBlock DRAIN (time_passed > 0) =====
2391        // The most realistic production scenario: gas_backlog exists in DB,
2392        // StartBlock drains some, then user tx grows it back.
2393        // The drain writes the same slot, and the grow writes it again.
2394        // If drain writes backlog=0 and grow writes backlog=357751,
2395        // but the original_value from DB was 552756, the filter should keep it.
2396        // BUT: what if drain writes backlog=552756 (no change from DB) and the
2397        // transition records original_value=552756? Then grow writes 910507 with
2398        // original_value=552756. This should still work. Let's verify.
2399        {
2400            struct PrePopulatedDb2 {
2401                gas_backlog_slot: U256,
2402                pre_existing_backlog: U256,
2403                speed_limit_slot: U256,
2404                speed_limit_value: U256,
2405            }
2406
2407            impl Database for PrePopulatedDb2 {
2408                type Error = std::convert::Infallible;
2409                fn basic(
2410                    &mut self,
2411                    _address: Address,
2412                ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
2413                    Ok(Some(revm::state::AccountInfo {
2414                        balance: U256::ZERO,
2415                        nonce: 1,
2416                        code_hash: keccak256([]),
2417                        code: None,
2418                        account_id: None,
2419                    }))
2420                }
2421                fn code_by_hash(
2422                    &mut self,
2423                    _code_hash: B256,
2424                ) -> Result<revm::state::Bytecode, Self::Error> {
2425                    Ok(revm::state::Bytecode::default())
2426                }
2427                fn storage(&mut self, _address: Address, index: U256) -> Result<U256, Self::Error> {
2428                    if index == self.gas_backlog_slot {
2429                        Ok(self.pre_existing_backlog)
2430                    } else if index == self.speed_limit_slot {
2431                        Ok(self.speed_limit_value)
2432                    } else {
2433                        Ok(U256::ZERO)
2434                    }
2435                }
2436                fn block_hash(&mut self, _number: u64) -> Result<B256, Self::Error> {
2437                    Ok(B256::ZERO)
2438                }
2439            }
2440
2441            // Compute speed_limit slot
2442            let speed_limit_slot = arb_storage::storage_key_map(l2_base.as_slice(), 0); // offset 0
2443            let pre_existing_backlog = U256::from(552756u64);
2444
2445            let mut state = StateBuilder::new()
2446                .with_database(PrePopulatedDb2 {
2447                    gas_backlog_slot,
2448                    pre_existing_backlog,
2449                    speed_limit_slot,
2450                    speed_limit_value: U256::from(7_000_000u64), // 7M gas/sec
2451                })
2452                .with_bundle_update()
2453                .build();
2454
2455            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
2456            let state_ptr: *mut revm::database::State<PrePopulatedDb2> = &mut state;
2457
2458            let backing = Storage::new(unsafe { &mut *state_ptr }, B256::ZERO);
2459            let l2_pricing =
2460                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2461
2462            let _initial = l2_pricing.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2463
2464            // StartBlock with time_passed=1 → drain = 1 * 7_000_000 = 7M
2465            // 552756 - 7M = 0 (saturating sub)
2466            l2_pricing
2467                .update_pricing_model(unsafe { &mut *state_ptr }, 1, 10)
2468                .unwrap();
2469            let _after_drain = l2_pricing.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2470
2471            // EVM commit
2472            {
2473                use revm::DatabaseCommit;
2474                state.commit(Default::default());
2475            }
2476
2477            // grow_backlog
2478            let l2_pricing2 =
2479                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2480            l2_pricing2
2481                .grow_backlog(unsafe { &mut *state_ptr }, 357751, MultiGas::default())
2482                .unwrap();
2483            let _after_grow = l2_pricing2.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2484
2485            // merge + take_bundle
2486            state.merge_transitions(BundleRetention::Reverts);
2487            let mut bundle = state.take_bundle();
2488
2489            let _bundle_pre = bundle
2490                .state
2491                .get(&ARBOS_STATE_ADDRESS)
2492                .and_then(|a| a.storage.get(&gas_backlog_slot))
2493                .map(|s| (s.present_value, s.previous_or_original_value));
2494
2495            // Note: In this variant we skip augment since all writes go through
2496            // write_storage_at which creates transitions. The bundle should
2497            // already have the slot.
2498
2499            // filter
2500            for (_addr, account) in bundle.state.iter_mut() {
2501                account
2502                    .storage
2503                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2504            }
2505
2506            let final_slot = bundle
2507                .state
2508                .get(&ARBOS_STATE_ADDRESS)
2509                .and_then(|a| a.storage.get(&gas_backlog_slot))
2510                .map(|s| (s.present_value, s.previous_or_original_value));
2511
2512            // Drain brought it to 0, grow added 357751 → final = 357751
2513            // original from DB = 552756
2514            // present=357751, original=552756 → different → should survive filter
2515            assert!(
2516                final_slot.is_some(),
2517                "VARIANT F FAILED: gas_backlog slot MISSING from bundle (drain+grow)"
2518            );
2519            assert_eq!(
2520                final_slot.unwrap().0,
2521                U256::from(357751u64),
2522                "VARIANT F: gas_backlog should be 357751"
2523            );
2524        }
2525
2526        // ===== VARIANT G: Pre-existing DB + drain to 0 + NO grow =====
2527        // Edge case: if backlog drains to 0 and no user tx grows it,
2528        // the write is 0 and original from DB is 552756.
2529        // write_storage_at should NOT skip this (0 != 552756).
2530        // But wait — what if update_pricing_model drains to 0, and
2531        // write_storage_at's no-op check sees value=0 and prev_value=0?
2532        // This would happen if the cache already has backlog=0 from a previous
2533        // write... Let's check.
2534        {
2535            struct PrePopDb3 {
2536                gas_backlog_slot: U256,
2537                speed_limit_slot: U256,
2538            }
2539
2540            impl Database for PrePopDb3 {
2541                type Error = std::convert::Infallible;
2542                fn basic(
2543                    &mut self,
2544                    _address: Address,
2545                ) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
2546                    Ok(Some(revm::state::AccountInfo {
2547                        balance: U256::ZERO,
2548                        nonce: 1,
2549                        code_hash: keccak256([]),
2550                        code: None,
2551                        account_id: None,
2552                    }))
2553                }
2554                fn code_by_hash(
2555                    &mut self,
2556                    _code_hash: B256,
2557                ) -> Result<revm::state::Bytecode, Self::Error> {
2558                    Ok(revm::state::Bytecode::default())
2559                }
2560                fn storage(&mut self, _address: Address, index: U256) -> Result<U256, Self::Error> {
2561                    if index == self.gas_backlog_slot {
2562                        Ok(U256::from(552756u64))
2563                    } else if index == self.speed_limit_slot {
2564                        Ok(U256::from(7_000_000u64))
2565                    } else {
2566                        Ok(U256::ZERO)
2567                    }
2568                }
2569                fn block_hash(&mut self, _number: u64) -> Result<B256, Self::Error> {
2570                    Ok(B256::ZERO)
2571                }
2572            }
2573
2574            let speed_limit_slot = arb_storage::storage_key_map(l2_base.as_slice(), 0);
2575
2576            let mut state = StateBuilder::new()
2577                .with_database(PrePopDb3 {
2578                    gas_backlog_slot,
2579                    speed_limit_slot,
2580                })
2581                .with_bundle_update()
2582                .build();
2583
2584            let _ = state.load_cache_account(ARBOS_STATE_ADDRESS);
2585            let state_ptr: *mut revm::database::State<PrePopDb3> = &mut state;
2586
2587            let backing = Storage::new(unsafe { &mut *state_ptr }, B256::ZERO);
2588            let l2_pricing =
2589                super::super::open_l2_pricing_state(backing.open_sub_storage(&[1]), 10);
2590
2591            let _initial = l2_pricing.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2592
2593            // Drain with time_passed=1 → 552756 - 7M = 0
2594            l2_pricing
2595                .update_pricing_model(unsafe { &mut *state_ptr }, 1, 10)
2596                .unwrap();
2597            let after_drain = l2_pricing.gas_backlog(unsafe { &mut *state_ptr }).unwrap();
2598            assert_eq!(after_drain, 0);
2599
2600            // merge + take_bundle
2601            state.merge_transitions(BundleRetention::Reverts);
2602            let mut bundle = state.take_bundle();
2603
2604            let _pre_filter = bundle
2605                .state
2606                .get(&ARBOS_STATE_ADDRESS)
2607                .and_then(|a| a.storage.get(&gas_backlog_slot))
2608                .map(|s| (s.present_value, s.previous_or_original_value));
2609
2610            // filter
2611            for (_addr, account) in bundle.state.iter_mut() {
2612                account
2613                    .storage
2614                    .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
2615            }
2616
2617            let final_slot = bundle
2618                .state
2619                .get(&ARBOS_STATE_ADDRESS)
2620                .and_then(|a| a.storage.get(&gas_backlog_slot))
2621                .map(|s| (s.present_value, s.previous_or_original_value));
2622
2623            // present=0, original=552756 → different → should survive
2624            assert!(
2625                final_slot.is_some(),
2626                "VARIANT G FAILED: drain-to-0 write was lost!"
2627            );
2628        }
2629    }
2630}