arbos/l1_pricing/
mod.rs

1mod batch_poster;
2
3pub use batch_poster::*;
4
5use alloy_primitives::{Address, U256};
6use revm::Database;
7
8use arb_storage::{
9    Storage, StorageBackedAddress, StorageBackedBigInt, StorageBackedBigUint, StorageBackedInt64,
10    StorageBackedUint64,
11};
12
13// Storage offsets for L1 pricing state.
14const PAY_REWARDS_TO_OFFSET: u64 = 0;
15const EQUILIBRATION_UNITS_OFFSET: u64 = 1;
16const INERTIA_OFFSET: u64 = 2;
17const PER_UNIT_REWARD_OFFSET: u64 = 3;
18const LAST_UPDATE_TIME_OFFSET: u64 = 4;
19const FUNDS_DUE_FOR_REWARDS_OFFSET: u64 = 5;
20const UNITS_SINCE_OFFSET: u64 = 6;
21const PRICE_PER_UNIT_OFFSET: u64 = 7;
22const LAST_SURPLUS_OFFSET: u64 = 8;
23const PER_BATCH_GAS_COST_OFFSET: u64 = 9;
24const AMORTIZED_COST_CAP_BIPS_OFFSET: u64 = 10;
25const L1_FEES_AVAILABLE_OFFSET: u64 = 11;
26const GAS_FLOOR_PER_TOKEN_OFFSET: u64 = 12;
27
28// Well-known addresses.
29pub const BATCH_POSTER_ADDRESS: Address = Address::new([
30    0xa4, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x65, 0x71, 0x75, 0x65,
31    0x6e, 0x63, 0x65, 0x72,
32]);
33pub const BATCH_POSTER_PAY_TO_ADDRESS: Address = BATCH_POSTER_ADDRESS;
34
35pub const L1_PRICER_FUNDS_POOL_ADDRESS: Address = Address::new([
36    0xa4, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
37    0x00, 0x00, 0x00, 0xf6,
38]);
39
40// Initial values.
41pub const INITIAL_INERTIA: u64 = 10;
42pub const INITIAL_PER_UNIT_REWARD: u64 = 10;
43pub const INITIAL_EQUILIBRATION_UNITS_V0: u64 = 60 * 16 * 100_000;
44pub const INITIAL_EQUILIBRATION_UNITS_V6: u64 = 16 * 10_000_000;
45pub const INITIAL_PER_BATCH_GAS_COST_V6: i64 = 100_000;
46pub const INITIAL_PER_BATCH_GAS_COST_V12: i64 = 210_000;
47
48// EIP-2028 gas cost per non-zero byte of calldata.
49pub const TX_DATA_NON_ZERO_GAS_EIP2028: u64 = 16;
50
51// Estimation padding constants.
52pub const ESTIMATION_PADDING_UNITS: u64 = TX_DATA_NON_ZERO_GAS_EIP2028 * 16;
53pub const ESTIMATION_PADDING_BASIS_POINTS: u64 = 100;
54const ONE_IN_BIPS: u64 = 10000;
55
56/// L1 pricing state manages the cost model for L1 data posting.
57pub struct L1PricingState<D> {
58    pub backing_storage: Storage<D>,
59    pay_rewards_to: StorageBackedAddress<D>,
60    equilibration_units: StorageBackedBigUint<D>,
61    inertia: StorageBackedUint64<D>,
62    per_unit_reward: StorageBackedUint64<D>,
63    last_update_time: StorageBackedUint64<D>,
64    funds_due_for_rewards: StorageBackedBigInt<D>,
65    units_since_update: StorageBackedUint64<D>,
66    price_per_unit: StorageBackedBigUint<D>,
67    last_surplus: StorageBackedBigInt<D>,
68    per_batch_gas_cost: StorageBackedInt64<D>,
69    amortized_cost_cap_bips: StorageBackedUint64<D>,
70    l1_fees_available: StorageBackedBigUint<D>,
71    gas_floor_per_token: StorageBackedUint64<D>,
72    pub arbos_version: u64,
73}
74
75pub fn initialize_l1_pricing_state<D: Database>(
76    sto: &Storage<D>,
77    rewards_recipient: Address,
78    initial_l1_base_fee: U256,
79) {
80    let state = sto.state_ptr();
81    let base_key = sto.base_key();
82
83    let _ =
84        StorageBackedAddress::new(state, base_key, PAY_REWARDS_TO_OFFSET).set(rewards_recipient);
85    let _ = StorageBackedBigUint::new(state, base_key, EQUILIBRATION_UNITS_OFFSET)
86        .set(U256::from(INITIAL_EQUILIBRATION_UNITS_V6));
87    let _ = StorageBackedUint64::new(state, base_key, INERTIA_OFFSET).set(INITIAL_INERTIA);
88    let _ = StorageBackedUint64::new(state, base_key, PER_UNIT_REWARD_OFFSET)
89        .set(INITIAL_PER_UNIT_REWARD);
90    let _ = StorageBackedUint64::new(state, base_key, LAST_UPDATE_TIME_OFFSET).set(0);
91    let _ = StorageBackedBigInt::new(state, base_key, FUNDS_DUE_FOR_REWARDS_OFFSET).set(U256::ZERO);
92    let _ = StorageBackedUint64::new(state, base_key, UNITS_SINCE_OFFSET).set(0);
93    let _ =
94        StorageBackedBigUint::new(state, base_key, PRICE_PER_UNIT_OFFSET).set(initial_l1_base_fee);
95    let _ = StorageBackedBigInt::new(state, base_key, LAST_SURPLUS_OFFSET).set(U256::ZERO);
96    let _ = StorageBackedInt64::new(state, base_key, PER_BATCH_GAS_COST_OFFSET)
97        .set(INITIAL_PER_BATCH_GAS_COST_V6);
98    let _ = StorageBackedUint64::new(state, base_key, AMORTIZED_COST_CAP_BIPS_OFFSET).set(0);
99    let _ = StorageBackedBigUint::new(state, base_key, L1_FEES_AVAILABLE_OFFSET).set(U256::ZERO);
100    let _ = StorageBackedUint64::new(state, base_key, GAS_FLOOR_PER_TOKEN_OFFSET).set(0);
101
102    initialize_batch_posters_table(sto, BATCH_POSTER_ADDRESS);
103}
104
105pub fn open_l1_pricing_state<D: Database>(
106    sto: Storage<D>,
107    arbos_version: u64,
108) -> L1PricingState<D> {
109    let state = sto.state_ptr();
110    let base_key = sto.base_key();
111
112    L1PricingState {
113        pay_rewards_to: StorageBackedAddress::new(state, base_key, PAY_REWARDS_TO_OFFSET),
114        equilibration_units: StorageBackedBigUint::new(state, base_key, EQUILIBRATION_UNITS_OFFSET),
115        inertia: StorageBackedUint64::new(state, base_key, INERTIA_OFFSET),
116        per_unit_reward: StorageBackedUint64::new(state, base_key, PER_UNIT_REWARD_OFFSET),
117        last_update_time: StorageBackedUint64::new(state, base_key, LAST_UPDATE_TIME_OFFSET),
118        funds_due_for_rewards: StorageBackedBigInt::new(
119            state,
120            base_key,
121            FUNDS_DUE_FOR_REWARDS_OFFSET,
122        ),
123        units_since_update: StorageBackedUint64::new(state, base_key, UNITS_SINCE_OFFSET),
124        price_per_unit: StorageBackedBigUint::new(state, base_key, PRICE_PER_UNIT_OFFSET),
125        last_surplus: StorageBackedBigInt::new(state, base_key, LAST_SURPLUS_OFFSET),
126        per_batch_gas_cost: StorageBackedInt64::new(state, base_key, PER_BATCH_GAS_COST_OFFSET),
127        amortized_cost_cap_bips: StorageBackedUint64::new(
128            state,
129            base_key,
130            AMORTIZED_COST_CAP_BIPS_OFFSET,
131        ),
132        l1_fees_available: StorageBackedBigUint::new(state, base_key, L1_FEES_AVAILABLE_OFFSET),
133        gas_floor_per_token: StorageBackedUint64::new(state, base_key, GAS_FLOOR_PER_TOKEN_OFFSET),
134        backing_storage: sto,
135        arbos_version,
136    }
137}
138
139impl<D: Database> L1PricingState<D> {
140    pub fn open(sto: Storage<D>, arbos_version: u64) -> Self {
141        open_l1_pricing_state(sto, arbos_version)
142    }
143
144    pub fn initialize(sto: &Storage<D>, rewards_recipient: Address, initial_l1_base_fee: U256) {
145        initialize_l1_pricing_state(sto, rewards_recipient, initial_l1_base_fee);
146    }
147
148    pub fn batch_poster_table(&self) -> BatchPostersTable<D> {
149        BatchPostersTable::open(&self.backing_storage)
150    }
151
152    // --- Getters/Setters ---
153
154    pub fn pay_rewards_to(&self) -> Result<Address, ()> {
155        self.pay_rewards_to.get()
156    }
157
158    pub fn set_pay_rewards_to(&self, addr: Address) -> Result<(), ()> {
159        self.pay_rewards_to.set(addr)
160    }
161
162    pub fn equilibration_units(&self) -> Result<U256, ()> {
163        self.equilibration_units.get()
164    }
165
166    pub fn set_equilibration_units(&self, units: U256) -> Result<(), ()> {
167        self.equilibration_units.set(units)
168    }
169
170    pub fn inertia(&self) -> Result<u64, ()> {
171        self.inertia.get()
172    }
173
174    pub fn set_inertia(&self, val: u64) -> Result<(), ()> {
175        self.inertia.set(val)
176    }
177
178    pub fn per_unit_reward(&self) -> Result<u64, ()> {
179        self.per_unit_reward.get()
180    }
181
182    pub fn set_per_unit_reward(&self, val: u64) -> Result<(), ()> {
183        self.per_unit_reward.set(val)
184    }
185
186    pub fn last_update_time(&self) -> Result<u64, ()> {
187        self.last_update_time.get()
188    }
189
190    pub fn set_last_update_time(&self, time: u64) -> Result<(), ()> {
191        self.last_update_time.set(time)
192    }
193
194    pub fn funds_due_for_rewards(&self) -> Result<U256, ()> {
195        self.funds_due_for_rewards.get_raw()
196    }
197
198    pub fn set_funds_due_for_rewards(&self, val: U256) -> Result<(), ()> {
199        self.funds_due_for_rewards.set(val)
200    }
201
202    pub fn units_since_update(&self) -> Result<u64, ()> {
203        self.units_since_update.get()
204    }
205
206    pub fn set_units_since_update(&self, val: u64) -> Result<(), ()> {
207        self.units_since_update.set(val)
208    }
209
210    pub fn add_to_units_since_update(&self, units: u64) -> Result<(), ()> {
211        let current = self.units_since_update.get().unwrap_or(0);
212        self.units_since_update.set(current.saturating_add(units))
213    }
214
215    pub fn subtract_from_units_since_update(&self, units: u64) -> Result<(), ()> {
216        let current = self.units_since_update.get().unwrap_or(0);
217        self.units_since_update.set(current.saturating_sub(units))
218    }
219
220    pub fn price_per_unit(&self) -> Result<U256, ()> {
221        self.price_per_unit.get()
222    }
223
224    pub fn set_price_per_unit(&self, val: U256) -> Result<(), ()> {
225        self.price_per_unit.set(val)
226    }
227
228    pub fn last_surplus(&self) -> Result<(U256, bool), ()> {
229        self.last_surplus.get_signed()
230    }
231
232    pub fn set_last_surplus(&self, magnitude: U256, negative: bool) -> Result<(), ()> {
233        // Pre-v7 doesn't store surplus.
234        if self.arbos_version < 7 {
235            return Ok(());
236        }
237        if negative {
238            self.last_surplus.set_negative(magnitude)
239        } else {
240            self.last_surplus.set(magnitude)
241        }
242    }
243
244    pub fn per_batch_gas_cost(&self) -> Result<i64, ()> {
245        self.per_batch_gas_cost.get()
246    }
247
248    pub fn set_per_batch_gas_cost(&self, val: i64) -> Result<(), ()> {
249        self.per_batch_gas_cost.set(val)
250    }
251
252    pub fn amortized_cost_cap_bips(&self) -> Result<u64, ()> {
253        self.amortized_cost_cap_bips.get()
254    }
255
256    pub fn set_amortized_cost_cap_bips(&self, val: u64) -> Result<(), ()> {
257        self.amortized_cost_cap_bips.set(val)
258    }
259
260    pub fn l1_fees_available(&self) -> Result<U256, ()> {
261        self.l1_fees_available.get()
262    }
263
264    pub fn set_l1_fees_available(&self, val: U256) -> Result<(), ()> {
265        self.l1_fees_available.set(val)
266    }
267
268    pub fn add_to_l1_fees_available(&self, amount: U256) -> Result<(), ()> {
269        let current = self.l1_fees_available.get().unwrap_or(U256::ZERO);
270        self.l1_fees_available.set(current.saturating_add(amount))
271    }
272
273    pub fn transfer_from_l1_fees_available(&self, amount: U256) -> Result<U256, ()> {
274        let available = self.l1_fees_available.get().unwrap_or(U256::ZERO);
275        let transfer = amount.min(available);
276        self.l1_fees_available
277            .set(available.saturating_sub(transfer))?;
278        Ok(transfer)
279    }
280
281    pub fn parent_gas_floor_per_token(&self) -> Result<u64, ()> {
282        if self.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50 {
283            return Ok(0);
284        }
285        self.gas_floor_per_token.get()
286    }
287
288    pub fn set_parent_gas_floor_per_token(&self, val: u64) -> Result<(), ()> {
289        if self.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50 {
290            return Err(());
291        }
292        self.gas_floor_per_token.set(val)
293    }
294
295    // --- Pricing logic ---
296
297    pub fn get_l1_pricing_surplus(&self) -> Result<(U256, bool), ()> {
298        let l1_fees_available = self.l1_fees_available.get().unwrap_or(U256::ZERO);
299        let bpt = self.batch_poster_table();
300        let total_funds_due = bpt.total_funds_due().unwrap_or(U256::ZERO);
301        let funds_due_for_rewards = self.funds_due_for_rewards().unwrap_or(U256::ZERO);
302
303        let need = total_funds_due.saturating_add(funds_due_for_rewards);
304        if l1_fees_available >= need {
305            Ok((l1_fees_available.saturating_sub(need), false))
306        } else {
307            Ok((need.saturating_sub(l1_fees_available), true))
308        }
309    }
310
311    pub fn get_poster_info(&self, poster: Address) -> Result<(U256, Address), ()> {
312        let bpt = self.batch_poster_table();
313        let state = bpt.open_poster(poster, false)?;
314        let due = state.funds_due()?;
315        let pay_to = state.pay_to()?;
316        Ok((due, pay_to))
317    }
318
319    pub fn poster_data_cost(&self, calldata_units: u64) -> Result<U256, ()> {
320        let price = self.price_per_unit()?;
321        let batch_cost = self.per_batch_gas_cost()?;
322
323        let calldata_cost = price.saturating_mul(U256::from(calldata_units));
324        if batch_cost >= 0 {
325            Ok(calldata_cost.saturating_add(U256::from(batch_cost as u64)))
326        } else {
327            Ok(calldata_cost.saturating_sub(U256::from((-batch_cost) as u64)))
328        }
329    }
330
331    /// Compute poster cost and units for a transaction on-chain.
332    ///
333    /// Returns `(l1_fee, units)` where `l1_fee = price_per_unit * units`.
334    pub fn compute_poster_cost(
335        &self,
336        poster: Address,
337        tx_bytes: &[u8],
338        brotli_compression_level: u64,
339    ) -> Result<(U256, u64), ()> {
340        if poster != BATCH_POSTER_ADDRESS {
341            return Ok((U256::ZERO, 0));
342        }
343        let units = self.get_poster_units_without_cache(tx_bytes, brotli_compression_level);
344        let price = self.price_per_unit()?;
345        Ok((price.saturating_mul(U256::from(units)), units))
346    }
347
348    /// Compute poster data cost for gas estimation (with padding).
349    ///
350    /// Used when we don't have an actual signed transaction, e.g. during
351    /// `eth_estimateGas`. Applies padding to account for tx encoding overhead.
352    pub fn poster_data_cost_for_estimation(
353        &self,
354        tx_bytes: &[u8],
355        brotli_compression_level: u64,
356    ) -> Result<(U256, u64), ()> {
357        let raw_units = self.get_poster_units_without_cache(tx_bytes, brotli_compression_level);
358        let padded = (raw_units.saturating_add(ESTIMATION_PADDING_UNITS))
359            .saturating_mul(ONE_IN_BIPS + ESTIMATION_PADDING_BASIS_POINTS)
360            / ONE_IN_BIPS;
361        let price = self.price_per_unit()?;
362        Ok((price.saturating_mul(U256::from(padded)), padded))
363    }
364
365    /// Compute the L1 calldata units for a transaction.
366    ///
367    /// Compresses the tx bytes with brotli and multiplies by the EIP-2028
368    /// non-zero gas cost (16) to get the unit count.
369    pub fn get_poster_units_without_cache(
370        &self,
371        tx_bytes: &[u8],
372        brotli_compression_level: u64,
373    ) -> u64 {
374        let l1_bytes = byte_count_after_brotli_level(tx_bytes, brotli_compression_level);
375        TX_DATA_NON_ZERO_GAS_EIP2028.saturating_mul(l1_bytes)
376    }
377
378    /// Update pricing based on a batch poster spending report.
379    pub fn update_for_batch_poster_spending<F>(
380        &self,
381        update_time: u64,
382        current_time: u64,
383        batch_poster: Address,
384        wei_spent: U256,
385        l1_basefee: U256,
386        mut transfer_fn: F,
387    ) -> Result<(), ()>
388    where
389        F: FnMut(Address, Address, U256) -> Result<(), ()>,
390    {
391        if self.arbos_version < 10 {
392            return self._preversion10_update(update_time, current_time, wei_spent, l1_basefee);
393        }
394
395        let bpt = self.batch_poster_table();
396        let poster_state = bpt.open_poster(batch_poster, true)?;
397
398        let funds_due_for_rewards = self.funds_due_for_rewards().unwrap_or(U256::ZERO);
399        let l1_fees_available = self.l1_fees_available.get().unwrap_or(U256::ZERO);
400
401        let mut last_update_time = self.last_update_time().unwrap_or(0);
402        if last_update_time == 0 && update_time > 0 {
403            last_update_time = update_time.saturating_sub(1);
404        }
405
406        if update_time > current_time || update_time < last_update_time {
407            return Err(());
408        }
409
410        let alloc_num = update_time.saturating_sub(last_update_time);
411        let alloc_denom = current_time.saturating_sub(last_update_time);
412        let (alloc_num, alloc_denom) = if alloc_denom == 0 {
413            (1u64, 1u64)
414        } else {
415            (alloc_num, alloc_denom)
416        };
417
418        let units_since = self.units_since_update().unwrap_or(0);
419        let units_allocated = units_since
420            .saturating_mul(alloc_num)
421            .checked_div(alloc_denom)
422            .unwrap_or(0);
423        let _ = self.set_units_since_update(units_since.saturating_sub(units_allocated));
424
425        let mut wei_spent = wei_spent;
426        if self.arbos_version >= 3 {
427            let cap_bips = self.amortized_cost_cap_bips().unwrap_or(0);
428            if cap_bips != 0 {
429                let cap = l1_basefee
430                    .saturating_mul(U256::from(units_allocated))
431                    .saturating_mul(U256::from(cap_bips))
432                    .checked_div(U256::from(10000u64))
433                    .unwrap_or(U256::MAX);
434                if cap < wei_spent {
435                    wei_spent = cap;
436                }
437            }
438        }
439
440        let due = poster_state.funds_due().unwrap_or(U256::ZERO);
441        let _ = poster_state.set_funds_due(due.saturating_add(wei_spent), &bpt.total_funds_due);
442
443        let per_unit_reward = self.per_unit_reward().unwrap_or(0);
444        let reward_amount = U256::from(units_allocated).saturating_mul(U256::from(per_unit_reward));
445        let _ = self.set_funds_due_for_rewards(funds_due_for_rewards.saturating_add(reward_amount));
446
447        let mut l1_fees = l1_fees_available;
448        let mut payment_for_rewards = reward_amount;
449        if l1_fees < payment_for_rewards {
450            payment_for_rewards = l1_fees;
451        }
452        let _ = self.set_funds_due_for_rewards(
453            self.funds_due_for_rewards()
454                .unwrap_or(U256::ZERO)
455                .saturating_sub(payment_for_rewards),
456        );
457
458        let pay_rewards_to = self.pay_rewards_to().unwrap_or(Address::ZERO);
459        if payment_for_rewards > U256::ZERO {
460            let _ = transfer_fn(
461                L1_PRICER_FUNDS_POOL_ADDRESS,
462                pay_rewards_to,
463                payment_for_rewards,
464            );
465            l1_fees = l1_fees.saturating_sub(payment_for_rewards);
466            let _ = self.set_l1_fees_available(l1_fees);
467        }
468
469        let balance_due = poster_state.funds_due().unwrap_or(U256::ZERO);
470        let mut transfer_amount = balance_due;
471        if l1_fees < transfer_amount {
472            transfer_amount = l1_fees;
473        }
474        if transfer_amount > U256::ZERO {
475            let addr_to_pay = poster_state.pay_to().unwrap_or(batch_poster);
476            let _ = transfer_fn(L1_PRICER_FUNDS_POOL_ADDRESS, addr_to_pay, transfer_amount);
477            l1_fees = l1_fees.saturating_sub(transfer_amount);
478            let _ = self.set_l1_fees_available(l1_fees);
479            let _ = poster_state.set_funds_due(
480                balance_due.saturating_sub(transfer_amount),
481                &bpt.total_funds_due,
482            );
483        }
484
485        let _ = self.set_last_update_time(update_time);
486
487        if units_allocated > 0 {
488            let total_funds_due = bpt.total_funds_due().unwrap_or(U256::ZERO);
489            let fdr = self.funds_due_for_rewards().unwrap_or(U256::ZERO);
490
491            let need_funds = total_funds_due.saturating_add(fdr);
492            let (surplus_mag, surplus_positive) = if l1_fees >= need_funds {
493                (l1_fees.saturating_sub(need_funds), true)
494            } else {
495                (need_funds.saturating_sub(l1_fees), false)
496            };
497
498            let inertia = self.inertia().unwrap_or(INITIAL_INERTIA);
499            let equil_units = self
500                .equilibration_units()
501                .unwrap_or(U256::from(INITIAL_EQUILIBRATION_UNITS_V6));
502            let inertia_units = equil_units
503                .checked_div(U256::from(inertia))
504                .unwrap_or(U256::ZERO);
505            let price = self.price_per_unit().unwrap_or(U256::ZERO);
506
507            let alloc_plus_inert = inertia_units.saturating_add(U256::from(units_allocated));
508            let (old_surplus_mag, old_surplus_neg) = self
509                .last_surplus
510                .get_signed()
511                .unwrap_or((U256::ZERO, false));
512
513            let units_u256 = U256::from(units_allocated);
514
515            // desiredDerivative = -surplus / equilUnits
516            let (desired_mag, desired_pos) =
517                signed_div(surplus_mag, !surplus_positive, equil_units);
518
519            // actualDerivative = (surplus - oldSurplus) / unitsAllocated
520            let (diff_mag, diff_pos) = signed_sub(
521                surplus_mag,
522                surplus_positive,
523                old_surplus_mag,
524                !old_surplus_neg,
525            );
526            let (actual_mag, actual_pos) = signed_div(diff_mag, diff_pos, units_u256);
527
528            // changeDerivativeBy = desired - actual
529            let (change_mag, change_pos) =
530                signed_sub(desired_mag, desired_pos, actual_mag, actual_pos);
531
532            // priceChange = changeDerivativeBy * unitsAllocated / allocPlusInert
533            let change_times_units = change_mag.saturating_mul(units_u256);
534            let (price_change, price_change_pos) =
535                signed_div(change_times_units, change_pos, alloc_plus_inert);
536
537            let new_price = if price_change_pos {
538                price.saturating_add(price_change)
539            } else {
540                price.saturating_sub(price_change)
541            };
542
543            let _ = self.set_last_surplus(surplus_mag, !surplus_positive);
544            let _ = self.set_price_per_unit(new_price);
545        }
546
547        Ok(())
548    }
549
550    fn _preversion10_update(
551        &self,
552        _update_time: u64,
553        _current_time: u64,
554        _wei_spent: U256,
555        _l1_basefee: U256,
556    ) -> Result<(), ()> {
557        // Simplified legacy pricing update for ArbOS < 10
558        Ok(())
559    }
560
561    fn _preversion2_update(
562        &self,
563        _update_time: u64,
564        _current_time: u64,
565        _wei_spent: U256,
566        _l1_basefee: U256,
567    ) -> Result<(), ()> {
568        // Simplified legacy pricing update for ArbOS < 2
569        Ok(())
570    }
571}
572
573/// Euclidean division (remainder is always non-negative).
574///
575/// For a negative dividend with a positive divisor, this rounds toward negative
576/// infinity rather than toward zero: -7 / 2 = -4 (not -3), -3 / 10 = -1 (not 0).
577fn signed_div(mag: U256, positive: bool, divisor: U256) -> (U256, bool) {
578    if divisor.is_zero() {
579        return (U256::ZERO, true);
580    }
581
582    if positive {
583        // Positive / positive: truncation and Euclidean are the same.
584        return (mag / divisor, true);
585    }
586
587    // Negative dividend: Euclidean division (matching Go's big.Int.Div).
588    // Go's big.Int.Div rounds toward negative infinity with non-negative remainder.
589    // -7 / 2 = -4 (since -7 = 2*(-4) + 1, remainder 1 >= 0)
590    let quotient = mag / divisor;
591    let remainder = mag % divisor;
592    if remainder.is_zero() {
593        if quotient.is_zero() {
594            (U256::ZERO, true) // -0 = +0
595        } else {
596            (quotient, false)
597        }
598    } else {
599        // Non-zero remainder: round toward negative infinity.
600        (quotient + U256::from(1), false)
601    }
602}
603
604/// Signed subtraction: (a_mag, a_pos) - (b_mag, b_pos)
605fn signed_sub(a_mag: U256, a_pos: bool, b_mag: U256, b_pos: bool) -> (U256, bool) {
606    // a - b = a + (-b)
607    let (neg_b_mag, neg_b_pos) = (b_mag, !b_pos);
608    signed_add(a_mag, a_pos, neg_b_mag, neg_b_pos)
609}
610
611/// Signed addition: (a_mag, a_pos) + (b_mag, b_pos)
612fn signed_add(a_mag: U256, a_pos: bool, b_mag: U256, b_pos: bool) -> (U256, bool) {
613    if a_pos == b_pos {
614        (a_mag.saturating_add(b_mag), a_pos)
615    } else if a_mag >= b_mag {
616        (a_mag.saturating_sub(b_mag), a_pos)
617    } else {
618        (b_mag.saturating_sub(a_mag), b_pos)
619    }
620}
621
622/// Compute poster cost and calldata units from pre-loaded pricing parameters.
623///
624/// This is the standalone version used by the block executor which has already
625/// extracted L1 pricing state values into the execution context.
626pub fn compute_poster_cost_standalone(
627    tx_bytes: &[u8],
628    poster: Address,
629    price_per_unit: U256,
630    brotli_compression_level: u64,
631) -> (U256, u64) {
632    if poster != BATCH_POSTER_ADDRESS {
633        return (U256::ZERO, 0);
634    }
635    let units = poster_units_from_bytes(tx_bytes, brotli_compression_level);
636    (price_per_unit.saturating_mul(U256::from(units)), units)
637}
638
639/// Compute calldata units from tx bytes using brotli compression.
640pub fn poster_units_from_bytes(tx_bytes: &[u8], brotli_compression_level: u64) -> u64 {
641    let l1_bytes = byte_count_after_brotli_level(tx_bytes, brotli_compression_level);
642    TX_DATA_NON_ZERO_GAS_EIP2028.saturating_mul(l1_bytes)
643}
644
645/// Brotli window size matching the reference C implementation.
646const BROTLI_DEFAULT_WINDOW_SIZE: i32 = 22;
647
648/// Computes the brotli-compressed size at a given compression level.
649///
650/// Uses `BrotliCompressCustomAlloc` with a full-size input buffer to process
651/// the entire input in a single shot. The standard `BrotliCompress` uses a
652/// 4096-byte chunked input buffer which produces different output for inputs
653/// exceeding that size.
654pub fn byte_count_after_brotli_level(data: &[u8], level: u64) -> u64 {
655    let quality = level.min(11) as i32;
656    let params = brotli::enc::BrotliEncoderParams {
657        quality,
658        lgwin: BROTLI_DEFAULT_WINDOW_SIZE,
659        ..Default::default()
660    };
661
662    let mut compressed = Vec::new();
663    let mut input_buffer = data.to_vec();
664    let mut output_buffer = vec![0u8; data.len() + 1024];
665
666    match brotli::BrotliCompressCustomAlloc(
667        &mut std::io::Cursor::new(data),
668        &mut compressed,
669        &mut input_buffer[..],
670        &mut output_buffer[..],
671        &params,
672        brotli::enc::StandardAlloc::default(),
673    ) {
674        Ok(_) => compressed.len() as u64,
675        Err(_) => data.len() as u64,
676    }
677}