arbos/l1_pricing/
mod.rs

1mod batch_poster;
2mod error;
3
4use alloy_primitives::{Address, U256};
5use arb_storage::{
6    Storage, StorageBackedAddress, StorageBackedBigInt, StorageBackedBigUint, StorageBackedInt64,
7    StorageBackedUint64, StorageBackend, SystemStateBackend,
8};
9pub use arbos_types::BATCH_POSTER_ADDRESS;
10pub use batch_poster::*;
11pub use error::L1PricingError;
12
13use crate::util::BalanceError;
14
15// Storage offsets for L1 pricing state.
16pub const PAY_REWARDS_TO_OFFSET: u64 = 0;
17pub const EQUILIBRATION_UNITS_OFFSET: u64 = 1;
18pub const INERTIA_OFFSET: u64 = 2;
19pub const PER_UNIT_REWARD_OFFSET: u64 = 3;
20pub const LAST_UPDATE_TIME_OFFSET: u64 = 4;
21pub const FUNDS_DUE_FOR_REWARDS_OFFSET: u64 = 5;
22pub const UNITS_SINCE_OFFSET: u64 = 6;
23pub const PRICE_PER_UNIT_OFFSET: u64 = 7;
24pub const LAST_SURPLUS_OFFSET: u64 = 8;
25pub const PER_BATCH_GAS_COST_OFFSET: u64 = 9;
26pub const AMORTIZED_COST_CAP_BIPS_OFFSET: u64 = 10;
27pub const L1_FEES_AVAILABLE_OFFSET: u64 = 11;
28pub const GAS_FLOOR_PER_TOKEN_OFFSET: u64 = 12;
29
30// Well-known addresses.
31pub const BATCH_POSTER_PAY_TO_ADDRESS: Address = BATCH_POSTER_ADDRESS;
32
33pub const L1_PRICER_FUNDS_POOL_ADDRESS: Address = Address::new([
34    0xa4, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
35    0x00, 0x00, 0x00, 0xf6,
36]);
37
38// Initial values.
39pub const INITIAL_INERTIA: u64 = 10;
40pub const INITIAL_PER_UNIT_REWARD: u64 = 10;
41pub const INITIAL_EQUILIBRATION_UNITS_V0: u64 = 60 * 16 * 100_000;
42pub const INITIAL_EQUILIBRATION_UNITS_V6: u64 = 16 * 10_000_000;
43pub const INITIAL_PER_BATCH_GAS_COST_V6: i64 = 100_000;
44pub const INITIAL_PER_BATCH_GAS_COST_V12: i64 = 210_000;
45
46// EIP-2028 gas cost per non-zero byte of calldata.
47pub const TX_DATA_NON_ZERO_GAS_EIP2028: u64 = 16;
48
49// Estimation padding constants.
50pub const ESTIMATION_PADDING_UNITS: u64 = TX_DATA_NON_ZERO_GAS_EIP2028 * 16;
51pub const ESTIMATION_PADDING_BASIS_POINTS: u64 = 100;
52const ONE_IN_BIPS: u64 = 10000;
53
54/// L1 pricing state manages the cost model for L1 data posting.
55pub struct L1PricingState<'a, D> {
56    pub backing_storage: Storage<'a, D>,
57    pay_rewards_to: StorageBackedAddress,
58    equilibration_units: StorageBackedBigUint,
59    inertia: StorageBackedUint64,
60    per_unit_reward: StorageBackedUint64,
61    last_update_time: StorageBackedUint64,
62    funds_due_for_rewards: StorageBackedBigInt,
63    units_since_update: StorageBackedUint64,
64    price_per_unit: StorageBackedBigUint,
65    last_surplus: StorageBackedBigInt,
66    per_batch_gas_cost: StorageBackedInt64,
67    amortized_cost_cap_bips: StorageBackedUint64,
68    l1_fees_available: StorageBackedBigUint,
69    gas_floor_per_token: StorageBackedUint64,
70    pub arbos_version: u64,
71}
72
73pub fn initialize_l1_pricing_state<D: revm::Database, B: StorageBackend>(
74    sto: &Storage<'_, D>,
75    backend: &mut B,
76    rewards_recipient: Address,
77    initial_l1_base_fee: U256,
78) -> Result<(), L1PricingError> {
79    let base_key = sto.base_key();
80
81    StorageBackedAddress::new(base_key, PAY_REWARDS_TO_OFFSET).set(backend, rewards_recipient)?;
82    StorageBackedBigUint::new(base_key, EQUILIBRATION_UNITS_OFFSET)
83        .set(backend, U256::from(INITIAL_EQUILIBRATION_UNITS_V0))?;
84    StorageBackedUint64::new(base_key, INERTIA_OFFSET).set(backend, INITIAL_INERTIA)?;
85    StorageBackedUint64::new(base_key, PER_UNIT_REWARD_OFFSET)
86        .set(backend, INITIAL_PER_UNIT_REWARD)?;
87    StorageBackedUint64::new(base_key, LAST_UPDATE_TIME_OFFSET).set(backend, 0)?;
88    StorageBackedBigInt::new(base_key, FUNDS_DUE_FOR_REWARDS_OFFSET).set(backend, U256::ZERO)?;
89    StorageBackedUint64::new(base_key, UNITS_SINCE_OFFSET).set(backend, 0)?;
90    StorageBackedBigUint::new(base_key, PRICE_PER_UNIT_OFFSET).set(backend, initial_l1_base_fee)?;
91
92    initialize_batch_posters_table(sto, backend, BATCH_POSTER_ADDRESS)?;
93    Ok(())
94}
95
96pub fn open_l1_pricing_state<D>(sto: Storage<'_, D>, arbos_version: u64) -> L1PricingState<'_, D> {
97    let base_key = sto.base_key();
98
99    L1PricingState {
100        pay_rewards_to: StorageBackedAddress::new(base_key, PAY_REWARDS_TO_OFFSET),
101        equilibration_units: StorageBackedBigUint::new(base_key, EQUILIBRATION_UNITS_OFFSET),
102        inertia: StorageBackedUint64::new(base_key, INERTIA_OFFSET),
103        per_unit_reward: StorageBackedUint64::new(base_key, PER_UNIT_REWARD_OFFSET),
104        last_update_time: StorageBackedUint64::new(base_key, LAST_UPDATE_TIME_OFFSET),
105        funds_due_for_rewards: StorageBackedBigInt::new(base_key, FUNDS_DUE_FOR_REWARDS_OFFSET),
106        units_since_update: StorageBackedUint64::new(base_key, UNITS_SINCE_OFFSET),
107        price_per_unit: StorageBackedBigUint::new(base_key, PRICE_PER_UNIT_OFFSET),
108        last_surplus: StorageBackedBigInt::new(base_key, LAST_SURPLUS_OFFSET),
109        per_batch_gas_cost: StorageBackedInt64::new(base_key, PER_BATCH_GAS_COST_OFFSET),
110        amortized_cost_cap_bips: StorageBackedUint64::new(base_key, AMORTIZED_COST_CAP_BIPS_OFFSET),
111        l1_fees_available: StorageBackedBigUint::new(base_key, L1_FEES_AVAILABLE_OFFSET),
112        gas_floor_per_token: StorageBackedUint64::new(base_key, GAS_FLOOR_PER_TOKEN_OFFSET),
113        backing_storage: sto,
114        arbos_version,
115    }
116}
117
118impl<'a, D> L1PricingState<'a, D> {
119    pub fn open(sto: Storage<'a, D>, arbos_version: u64) -> Self {
120        open_l1_pricing_state(sto, arbos_version)
121    }
122
123    pub fn batch_poster_table(&self) -> BatchPostersTable<'a, D> {
124        BatchPostersTable::open(&self.backing_storage)
125    }
126
127    // --- Getters/Setters ---
128
129    pub fn pay_rewards_to<B: SystemStateBackend>(
130        &self,
131        backend: &mut B,
132    ) -> Result<Address, L1PricingError> {
133        Ok(self.pay_rewards_to.get(backend)?)
134    }
135
136    pub fn set_pay_rewards_to<B: StorageBackend>(
137        &self,
138        backend: &mut B,
139        addr: Address,
140    ) -> Result<(), L1PricingError> {
141        Ok(self.pay_rewards_to.set(backend, addr)?)
142    }
143
144    pub fn equilibration_units<B: SystemStateBackend>(
145        &self,
146        backend: &mut B,
147    ) -> Result<U256, L1PricingError> {
148        Ok(self.equilibration_units.get(backend)?)
149    }
150
151    pub fn set_equilibration_units<B: StorageBackend>(
152        &self,
153        backend: &mut B,
154        units: U256,
155    ) -> Result<(), L1PricingError> {
156        Ok(self.equilibration_units.set(backend, units)?)
157    }
158
159    pub fn inertia<B: SystemStateBackend>(&self, backend: &mut B) -> Result<u64, L1PricingError> {
160        Ok(self.inertia.get(backend)?)
161    }
162
163    pub fn set_inertia<B: StorageBackend>(
164        &self,
165        backend: &mut B,
166        val: u64,
167    ) -> Result<(), L1PricingError> {
168        Ok(self.inertia.set(backend, val)?)
169    }
170
171    pub fn per_unit_reward<B: SystemStateBackend>(
172        &self,
173        backend: &mut B,
174    ) -> Result<u64, L1PricingError> {
175        Ok(self.per_unit_reward.get(backend)?)
176    }
177
178    pub fn set_per_unit_reward<B: StorageBackend>(
179        &self,
180        backend: &mut B,
181        val: u64,
182    ) -> Result<(), L1PricingError> {
183        Ok(self.per_unit_reward.set(backend, val)?)
184    }
185
186    pub fn last_update_time<B: SystemStateBackend>(
187        &self,
188        backend: &mut B,
189    ) -> Result<u64, L1PricingError> {
190        Ok(self.last_update_time.get(backend)?)
191    }
192
193    pub fn set_last_update_time<B: StorageBackend>(
194        &self,
195        backend: &mut B,
196        time: u64,
197    ) -> Result<(), L1PricingError> {
198        Ok(self.last_update_time.set(backend, time)?)
199    }
200
201    pub fn funds_due_for_rewards<B: SystemStateBackend>(
202        &self,
203        backend: &mut B,
204    ) -> Result<U256, L1PricingError> {
205        Ok(self.funds_due_for_rewards.get_raw(backend)?)
206    }
207
208    pub fn set_funds_due_for_rewards<B: StorageBackend>(
209        &self,
210        backend: &mut B,
211        val: U256,
212    ) -> Result<(), L1PricingError> {
213        Ok(self.funds_due_for_rewards.set(backend, val)?)
214    }
215
216    pub fn units_since_update<B: SystemStateBackend>(
217        &self,
218        backend: &mut B,
219    ) -> Result<u64, L1PricingError> {
220        Ok(self.units_since_update.get(backend)?)
221    }
222
223    pub fn set_units_since_update<B: StorageBackend>(
224        &self,
225        backend: &mut B,
226        val: u64,
227    ) -> Result<(), L1PricingError> {
228        Ok(self.units_since_update.set(backend, val)?)
229    }
230
231    pub fn add_to_units_since_update<B: StorageBackend>(
232        &self,
233        backend: &mut B,
234        units: u64,
235    ) -> Result<(), L1PricingError> {
236        let current = self.units_since_update.get(backend)?;
237        Ok(self
238            .units_since_update
239            .set(backend, current.saturating_add(units))?)
240    }
241
242    pub fn subtract_from_units_since_update<B: StorageBackend>(
243        &self,
244        backend: &mut B,
245        units: u64,
246    ) -> Result<(), L1PricingError> {
247        let current = self.units_since_update.get(backend)?;
248        Ok(self
249            .units_since_update
250            .set(backend, current.saturating_sub(units))?)
251    }
252
253    pub fn price_per_unit<B: SystemStateBackend>(
254        &self,
255        backend: &mut B,
256    ) -> Result<U256, L1PricingError> {
257        Ok(self.price_per_unit.get(backend)?)
258    }
259
260    pub fn set_price_per_unit<B: StorageBackend>(
261        &self,
262        backend: &mut B,
263        val: U256,
264    ) -> Result<(), L1PricingError> {
265        Ok(self.price_per_unit.set(backend, val)?)
266    }
267
268    pub fn last_surplus<B: SystemStateBackend>(
269        &self,
270        backend: &mut B,
271    ) -> Result<(U256, bool), L1PricingError> {
272        Ok(self.last_surplus.get_signed(backend)?)
273    }
274
275    pub fn set_last_surplus<B: StorageBackend>(
276        &self,
277        backend: &mut B,
278        magnitude: U256,
279        negative: bool,
280    ) -> Result<(), L1PricingError> {
281        if self.arbos_version < 7 {
282            return Ok(());
283        }
284        if negative {
285            Ok(self.last_surplus.set_negative(backend, magnitude)?)
286        } else {
287            Ok(self.last_surplus.set(backend, magnitude)?)
288        }
289    }
290
291    pub fn per_batch_gas_cost<B: SystemStateBackend>(
292        &self,
293        backend: &mut B,
294    ) -> Result<i64, L1PricingError> {
295        Ok(self.per_batch_gas_cost.get(backend)?)
296    }
297
298    pub fn set_per_batch_gas_cost<B: StorageBackend>(
299        &self,
300        backend: &mut B,
301        val: i64,
302    ) -> Result<(), L1PricingError> {
303        Ok(self.per_batch_gas_cost.set(backend, val)?)
304    }
305
306    pub fn amortized_cost_cap_bips<B: SystemStateBackend>(
307        &self,
308        backend: &mut B,
309    ) -> Result<u64, L1PricingError> {
310        Ok(self.amortized_cost_cap_bips.get(backend)?)
311    }
312
313    pub fn set_amortized_cost_cap_bips<B: StorageBackend>(
314        &self,
315        backend: &mut B,
316        val: u64,
317    ) -> Result<(), L1PricingError> {
318        Ok(self.amortized_cost_cap_bips.set(backend, val)?)
319    }
320
321    pub fn l1_fees_available<B: SystemStateBackend>(
322        &self,
323        backend: &mut B,
324    ) -> Result<U256, L1PricingError> {
325        Ok(self.l1_fees_available.get(backend)?)
326    }
327
328    pub fn set_l1_fees_available<B: StorageBackend>(
329        &self,
330        backend: &mut B,
331        val: U256,
332    ) -> Result<(), L1PricingError> {
333        Ok(self.l1_fees_available.set(backend, val)?)
334    }
335
336    pub fn add_to_l1_fees_available<B: StorageBackend>(
337        &self,
338        backend: &mut B,
339        amount: U256,
340    ) -> Result<(), L1PricingError> {
341        let current = self.l1_fees_available.get(backend)?;
342        Ok(self
343            .l1_fees_available
344            .set(backend, current.saturating_add(amount))?)
345    }
346
347    pub fn transfer_from_l1_fees_available<B: StorageBackend>(
348        &self,
349        backend: &mut B,
350        amount: U256,
351    ) -> Result<U256, L1PricingError> {
352        let available = self.l1_fees_available.get(backend)?;
353        let transfer = amount.min(available);
354        self.l1_fees_available
355            .set(backend, available.saturating_sub(transfer))?;
356        Ok(transfer)
357    }
358
359    pub fn parent_gas_floor_per_token<B: SystemStateBackend>(
360        &self,
361        backend: &mut B,
362    ) -> Result<u64, L1PricingError> {
363        if self.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50 {
364            return Ok(0);
365        }
366        Ok(self.gas_floor_per_token.get(backend)?)
367    }
368
369    pub fn set_parent_gas_floor_per_token<B: StorageBackend>(
370        &self,
371        backend: &mut B,
372        val: u64,
373    ) -> Result<(), L1PricingError> {
374        if self.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_50 {
375            return Err(L1PricingError::ParentGasFloorUnsupportedVersion);
376        }
377        Ok(self.gas_floor_per_token.set(backend, val)?)
378    }
379
380    // --- Pricing logic ---
381
382    pub fn get_l1_pricing_surplus<B: SystemStateBackend>(
383        &self,
384        backend: &mut B,
385    ) -> Result<(U256, bool), L1PricingError> {
386        let l1_fees_available = self.l1_fees_available.get(backend)?;
387        let bpt = self.batch_poster_table();
388        let total_funds_due = bpt.total_funds_due(backend)?;
389        let funds_due_for_rewards = self.funds_due_for_rewards(backend)?;
390
391        let need = total_funds_due.saturating_add(funds_due_for_rewards);
392        if l1_fees_available >= need {
393            Ok((l1_fees_available.saturating_sub(need), false))
394        } else {
395            Ok((need.saturating_sub(l1_fees_available), true))
396        }
397    }
398
399    pub fn poster_data_cost<B: SystemStateBackend>(
400        &self,
401        backend: &mut B,
402        calldata_units: u64,
403    ) -> Result<U256, L1PricingError> {
404        let price = self.price_per_unit(backend)?;
405        let batch_cost = self.per_batch_gas_cost(backend)?;
406
407        let calldata_cost = price.saturating_mul(U256::from(calldata_units));
408        if batch_cost >= 0 {
409            Ok(calldata_cost.saturating_add(U256::from(batch_cost as u64)))
410        } else {
411            Ok(calldata_cost.saturating_sub(U256::from((-batch_cost) as u64)))
412        }
413    }
414
415    /// Compute poster cost and units for a transaction on-chain.
416    pub fn compute_poster_cost<B: SystemStateBackend>(
417        &self,
418        backend: &mut B,
419        poster: Address,
420        tx_bytes: &[u8],
421        brotli_compression_level: u64,
422    ) -> Result<(U256, u64), L1PricingError> {
423        if poster != BATCH_POSTER_ADDRESS {
424            return Ok((U256::ZERO, 0));
425        }
426        let units = self.get_poster_units_without_cache(tx_bytes, brotli_compression_level);
427        let price = self.price_per_unit(backend)?;
428        Ok((price.saturating_mul(U256::from(units)), units))
429    }
430
431    /// Compute poster data cost for gas estimation (with padding).
432    pub fn poster_data_cost_for_estimation<B: SystemStateBackend>(
433        &self,
434        backend: &mut B,
435        tx_bytes: &[u8],
436        brotli_compression_level: u64,
437    ) -> Result<(U256, u64), L1PricingError> {
438        let raw_units = self.get_poster_units_without_cache(tx_bytes, brotli_compression_level);
439        let padded = (raw_units.saturating_add(ESTIMATION_PADDING_UNITS))
440            .saturating_mul(ONE_IN_BIPS + ESTIMATION_PADDING_BASIS_POINTS)
441            / ONE_IN_BIPS;
442        let price = self.price_per_unit(backend)?;
443        Ok((price.saturating_mul(U256::from(padded)), padded))
444    }
445
446    /// Compute the L1 calldata units for a transaction.
447    pub fn get_poster_units_without_cache(
448        &self,
449        tx_bytes: &[u8],
450        brotli_compression_level: u64,
451    ) -> u64 {
452        let l1_bytes = byte_count_after_brotli_level(tx_bytes, brotli_compression_level);
453        TX_DATA_NON_ZERO_GAS_EIP2028.saturating_mul(l1_bytes)
454    }
455
456    fn _preversion10_update(
457        &self,
458        _update_time: u64,
459        _current_time: u64,
460        _wei_spent: U256,
461        _l1_basefee: U256,
462    ) -> Result<(), L1PricingError> {
463        Ok(())
464    }
465
466    fn _preversion2_update(
467        &self,
468        _update_time: u64,
469        _current_time: u64,
470        _wei_spent: U256,
471        _l1_basefee: U256,
472    ) -> Result<(), L1PricingError> {
473        Ok(())
474    }
475}
476
477impl<D: revm::Database> L1PricingState<'_, D> {
478    pub fn initialize<B: StorageBackend>(
479        sto: &Storage<'_, D>,
480        backend: &mut B,
481        rewards_recipient: Address,
482        initial_l1_base_fee: U256,
483    ) -> Result<(), L1PricingError> {
484        initialize_l1_pricing_state(sto, backend, rewards_recipient, initial_l1_base_fee)
485    }
486
487    pub fn get_poster_info<B: StorageBackend>(
488        &self,
489        backend: &mut B,
490        poster: Address,
491    ) -> Result<(U256, Address), L1PricingError> {
492        let bpt = self.batch_poster_table();
493        let state = bpt.open_poster(backend, poster, false)?;
494        let due = state.funds_due(backend)?;
495        let pay_to = state.pay_to(backend)?;
496        Ok((due, pay_to))
497    }
498
499    /// Update pricing based on a batch poster spending report.
500    pub fn update_for_batch_poster_spending<F, B>(
501        &self,
502        backend: &mut B,
503        update_time: u64,
504        current_time: u64,
505        batch_poster: Address,
506        wei_spent: U256,
507        l1_basefee: U256,
508        mut transfer_fn: F,
509    ) -> Result<(), L1PricingError>
510    where
511        F: FnMut(Address, Address, U256) -> Result<(), BalanceError>,
512        B: StorageBackend,
513    {
514        if self.arbos_version < 10 {
515            return self._preversion10_update(update_time, current_time, wei_spent, l1_basefee);
516        }
517
518        let bpt = self.batch_poster_table();
519        let poster_state = bpt.open_poster(backend, batch_poster, true)?;
520
521        let funds_due_for_rewards = self.funds_due_for_rewards(backend)?;
522        let l1_fees_available = self.l1_fees_available.get(backend)?;
523
524        let mut last_update_time = self.last_update_time(backend)?;
525        if last_update_time == 0 && update_time > 0 {
526            last_update_time = update_time.saturating_sub(1);
527        }
528
529        if update_time > current_time || update_time < last_update_time {
530            return Err(L1PricingError::InvalidUpdateTime);
531        }
532
533        let alloc_num = update_time.saturating_sub(last_update_time);
534        let alloc_denom = current_time.saturating_sub(last_update_time);
535        let (alloc_num, alloc_denom) = if alloc_denom == 0 {
536            (1u64, 1u64)
537        } else {
538            (alloc_num, alloc_denom)
539        };
540
541        let units_since = self.units_since_update(backend)?;
542        let units_allocated = units_since
543            .saturating_mul(alloc_num)
544            .checked_div(alloc_denom)
545            .unwrap_or(0);
546        self.set_units_since_update(backend, units_since.saturating_sub(units_allocated))?;
547
548        let mut wei_spent = wei_spent;
549        if self.arbos_version >= 3 {
550            let cap_bips = self.amortized_cost_cap_bips(backend)?;
551            if cap_bips != 0 {
552                let cap = l1_basefee
553                    .saturating_mul(U256::from(units_allocated))
554                    .saturating_mul(U256::from(cap_bips))
555                    .checked_div(U256::from(10000u64))
556                    .unwrap_or(U256::MAX);
557                if cap < wei_spent {
558                    wei_spent = cap;
559                }
560            }
561        }
562
563        let due = poster_state.funds_due(backend)?;
564        let _ = poster_state.set_funds_due(
565            backend,
566            due.saturating_add(wei_spent),
567            &bpt.total_funds_due,
568        );
569
570        let per_unit_reward = self.per_unit_reward(backend)?;
571        let reward_amount = U256::from(units_allocated).saturating_mul(U256::from(per_unit_reward));
572        self.set_funds_due_for_rewards(
573            backend,
574            funds_due_for_rewards.saturating_add(reward_amount),
575        )?;
576
577        let mut l1_fees = l1_fees_available;
578        let mut payment_for_rewards = reward_amount;
579        if l1_fees < payment_for_rewards {
580            payment_for_rewards = l1_fees;
581        }
582        let fdr_after = self
583            .funds_due_for_rewards(backend)?
584            .saturating_sub(payment_for_rewards);
585        self.set_funds_due_for_rewards(backend, fdr_after)?;
586
587        let pay_rewards_to = self.pay_rewards_to(backend)?;
588        if payment_for_rewards > U256::ZERO {
589            // payment_for_rewards was clamped to l1_fees just above, which mirrors
590            // the L1 pricer pool balance. A typed shortfall here would indicate
591            // pool/state drift and must not block the rest of the bookkeeping.
592            let _ = transfer_fn(
593                L1_PRICER_FUNDS_POOL_ADDRESS,
594                pay_rewards_to,
595                payment_for_rewards,
596            );
597            l1_fees = l1_fees.saturating_sub(payment_for_rewards);
598            self.set_l1_fees_available(backend, l1_fees)?;
599        }
600
601        let balance_due = poster_state.funds_due(backend)?;
602        let mut transfer_amount = balance_due;
603        if l1_fees < transfer_amount {
604            transfer_amount = l1_fees;
605        }
606        if transfer_amount > U256::ZERO {
607            let addr_to_pay = poster_state.pay_to(backend)?;
608            // transfer_amount is capped to the remaining pool balance above; a
609            // shortfall here would be a pool/state inconsistency rather than a
610            // user-driven error, so do not surface it as Err.
611            let _ = transfer_fn(L1_PRICER_FUNDS_POOL_ADDRESS, addr_to_pay, transfer_amount);
612            l1_fees = l1_fees.saturating_sub(transfer_amount);
613            self.set_l1_fees_available(backend, l1_fees)?;
614            let _ = poster_state.set_funds_due(
615                backend,
616                balance_due.saturating_sub(transfer_amount),
617                &bpt.total_funds_due,
618            );
619        }
620
621        self.set_last_update_time(backend, update_time)?;
622
623        if units_allocated > 0 {
624            let total_funds_due = bpt.total_funds_due(backend)?;
625            let fdr = self.funds_due_for_rewards(backend)?;
626
627            let need_funds = total_funds_due.saturating_add(fdr);
628            let (surplus_mag, surplus_positive) = if l1_fees >= need_funds {
629                (l1_fees.saturating_sub(need_funds), true)
630            } else {
631                (need_funds.saturating_sub(l1_fees), false)
632            };
633
634            let inertia = self.inertia(backend)?;
635            let equil_units = self.equilibration_units(backend)?;
636            let inertia_units = equil_units
637                .checked_div(U256::from(inertia))
638                .unwrap_or(U256::ZERO);
639            let price = self.price_per_unit(backend)?;
640
641            let alloc_plus_inert = inertia_units.saturating_add(U256::from(units_allocated));
642            let (old_surplus_mag, old_surplus_neg) = self.last_surplus.get_signed(backend)?;
643
644            let units_u256 = U256::from(units_allocated);
645
646            let (desired_mag, desired_pos) =
647                signed_div(surplus_mag, !surplus_positive, equil_units);
648
649            let (diff_mag, diff_pos) = signed_sub(
650                surplus_mag,
651                surplus_positive,
652                old_surplus_mag,
653                !old_surplus_neg,
654            );
655            let (actual_mag, actual_pos) = signed_div(diff_mag, diff_pos, units_u256);
656
657            let (change_mag, change_pos) =
658                signed_sub(desired_mag, desired_pos, actual_mag, actual_pos);
659
660            let change_times_units = change_mag.saturating_mul(units_u256);
661            let (price_change, price_change_pos) =
662                signed_div(change_times_units, change_pos, alloc_plus_inert);
663
664            let new_price = if price_change_pos {
665                price.saturating_add(price_change)
666            } else {
667                price.saturating_sub(price_change)
668            };
669
670            self.set_last_surplus(backend, surplus_mag, !surplus_positive)?;
671            self.set_price_per_unit(backend, new_price)?;
672        }
673
674        Ok(())
675    }
676}
677
678/// Euclidean division (remainder is always non-negative).
679///
680/// For a negative dividend with a positive divisor, this rounds toward negative
681/// infinity rather than toward zero: -7 / 2 = -4 (not -3), -3 / 10 = -1 (not 0).
682fn signed_div(mag: U256, positive: bool, divisor: U256) -> (U256, bool) {
683    if divisor.is_zero() {
684        return (U256::ZERO, true);
685    }
686
687    if positive {
688        // Positive / positive: truncation and Euclidean are the same.
689        return (mag / divisor, true);
690    }
691
692    // Negative dividend: Euclidean division (matching Go's big.Int.Div).
693    // Go's big.Int.Div rounds toward negative infinity with non-negative remainder.
694    // -7 / 2 = -4 (since -7 = 2*(-4) + 1, remainder 1 >= 0)
695    let quotient = mag / divisor;
696    let remainder = mag % divisor;
697    if remainder.is_zero() {
698        if quotient.is_zero() {
699            (U256::ZERO, true) // -0 = +0
700        } else {
701            (quotient, false)
702        }
703    } else {
704        // Non-zero remainder: round toward negative infinity.
705        (quotient + U256::from(1), false)
706    }
707}
708
709/// Signed subtraction: (a_mag, a_pos) - (b_mag, b_pos)
710fn signed_sub(a_mag: U256, a_pos: bool, b_mag: U256, b_pos: bool) -> (U256, bool) {
711    // a - b = a + (-b)
712    let (neg_b_mag, neg_b_pos) = (b_mag, !b_pos);
713    signed_add(a_mag, a_pos, neg_b_mag, neg_b_pos)
714}
715
716/// Signed addition: (a_mag, a_pos) + (b_mag, b_pos)
717fn signed_add(a_mag: U256, a_pos: bool, b_mag: U256, b_pos: bool) -> (U256, bool) {
718    if a_pos == b_pos {
719        (a_mag.saturating_add(b_mag), a_pos)
720    } else if a_mag >= b_mag {
721        (a_mag.saturating_sub(b_mag), a_pos)
722    } else {
723        (b_mag.saturating_sub(a_mag), b_pos)
724    }
725}
726
727/// Compute poster cost and calldata units from pre-loaded pricing parameters.
728///
729/// This is the standalone version used by the block executor which has already
730/// extracted L1 pricing state values into the execution context.
731pub fn compute_poster_cost_standalone(
732    tx_bytes: &[u8],
733    poster: Address,
734    price_per_unit: U256,
735    brotli_compression_level: u64,
736) -> (U256, u64) {
737    if poster != BATCH_POSTER_ADDRESS {
738        return (U256::ZERO, 0);
739    }
740    let units = poster_units_from_bytes(tx_bytes, brotli_compression_level);
741    (price_per_unit.saturating_mul(U256::from(units)), units)
742}
743
744/// Compute calldata units from tx bytes using brotli compression.
745pub fn poster_units_from_bytes(tx_bytes: &[u8], brotli_compression_level: u64) -> u64 {
746    let l1_bytes = byte_count_after_brotli_level(tx_bytes, brotli_compression_level);
747    TX_DATA_NON_ZERO_GAS_EIP2028.saturating_mul(l1_bytes)
748}
749
750/// Brotli window size matching the reference C implementation.
751const BROTLI_DEFAULT_WINDOW_SIZE: i32 = 22;
752
753/// Computes the brotli-compressed size at a given compression level.
754pub fn byte_count_after_brotli_level(data: &[u8], level: u64) -> u64 {
755    use std::{ffi::c_int, os::raw::c_void, ptr};
756
757    type BrotliBool = c_int;
758    const BROTLI_PARAM_QUALITY: u32 = 1;
759    const BROTLI_PARAM_LGWIN: u32 = 2;
760    const BROTLI_OPERATION_FINISH: u32 = 2;
761
762    unsafe extern "C" {
763        fn BrotliEncoderCreateInstance(
764            alloc: Option<extern "C" fn(*mut c_void, usize) -> *mut c_void>,
765            free: Option<extern "C" fn(*mut c_void, *mut c_void)>,
766            opaque: *mut c_void,
767        ) -> *mut c_void;
768        fn BrotliEncoderSetParameter(state: *mut c_void, param: u32, value: u32) -> BrotliBool;
769        fn BrotliEncoderCompressStream(
770            state: *mut c_void,
771            op: u32,
772            available_in: *mut usize,
773            next_in: *mut *const u8,
774            available_out: *mut usize,
775            next_out: *mut *mut u8,
776            total_out: *mut usize,
777        ) -> BrotliBool;
778        fn BrotliEncoderIsFinished(state: *const c_void) -> BrotliBool;
779        fn BrotliEncoderDestroyInstance(state: *mut c_void);
780        fn BrotliEncoderMaxCompressedSize(input_size: usize) -> usize;
781    }
782
783    // SAFETY: FFI into libbrotlienc. The encoder state is created,
784    // configured, fed, then unconditionally destroyed in this block;
785    // input and output buffers are stack/heap allocations whose lifetime
786    // exceeds the encoder. Null state is checked before any use.
787    unsafe {
788        let state = BrotliEncoderCreateInstance(None, None, ptr::null_mut());
789        if state.is_null() {
790            return data.len() as u64;
791        }
792
793        BrotliEncoderSetParameter(state, BROTLI_PARAM_QUALITY, level.min(11) as u32);
794        BrotliEncoderSetParameter(state, BROTLI_PARAM_LGWIN, BROTLI_DEFAULT_WINDOW_SIZE as u32);
795
796        let max_size = BrotliEncoderMaxCompressedSize(data.len());
797        let max_size = max_size.max(data.len() + (data.len() >> 10) * 8 + 64);
798        let mut output = vec![0u8; max_size];
799
800        let mut in_len = data.len();
801        let mut in_ptr = data.as_ptr();
802        let mut out_left = output.len();
803        let mut out_ptr = output.as_mut_ptr();
804        let mut out_len = 0usize;
805
806        let ok = BrotliEncoderCompressStream(
807            state,
808            BROTLI_OPERATION_FINISH,
809            &mut in_len,
810            &mut in_ptr,
811            &mut out_left,
812            &mut out_ptr,
813            &mut out_len,
814        );
815        let finished = BrotliEncoderIsFinished(state);
816        BrotliEncoderDestroyInstance(state);
817
818        if ok != 0 && finished != 0 {
819            out_len as u64
820        } else {
821            data.len() as u64
822        }
823    }
824}