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