arb_precompiles/
arbgasinfo.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, U256};
3use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
4
5use crate::storage_slot::{
6    derive_subspace_key, gas_constraints_vec_key, map_slot, multi_gas_base_fees_subspace,
7    multi_gas_constraints_vec_key, subspace_slot, vector_element_field, vector_element_key,
8    vector_length_slot, ARBOS_STATE_ADDRESS, L1_PRICING_SUBSPACE, L2_PRICING_SUBSPACE,
9    ROOT_STORAGE_KEY,
10};
11
12/// ArbGasInfo precompile address (0x6c).
13pub const ARBGASINFO_ADDRESS: Address = Address::new([
14    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
15    0x00, 0x00, 0x00, 0x6c,
16]);
17
18// Function selectors (keccak256 of Solidity signatures, first 4 bytes).
19const GET_L1_BASEFEE_ESTIMATE: [u8; 4] = [0xf5, 0xd6, 0xde, 0xd7]; // getL1BaseFeeEstimate()
20const GET_L1_GAS_PRICE_ESTIMATE: [u8; 4] = [0x05, 0x5f, 0x36, 0x2f]; // getL1GasPriceEstimate()
21const GET_MINIMUM_GAS_PRICE: [u8; 4] = [0xf9, 0x18, 0x37, 0x9a]; // getMinimumGasPrice()
22const GET_PRICES_IN_WEI: [u8; 4] = [0x41, 0xb2, 0x47, 0xa8]; // getPricesInWei()
23const GET_GAS_ACCOUNTING_PARAMS: [u8; 4] = [0x61, 0x2a, 0xf1, 0x78]; // getGasAccountingParams()
24const GET_CURRENT_TX_L1_FEES: [u8; 4] = [0xc6, 0xf7, 0xde, 0x0e]; // getCurrentTxL1GasFees()
25const GET_PRICES_IN_ARBGAS: [u8; 4] = [0x02, 0x19, 0x9f, 0x34]; // getPricesInArbGas()
26const GET_L1_BASEFEE_ESTIMATE_INERTIA: [u8; 4] = [0x29, 0xeb, 0x31, 0xee]; // getL1BaseFeeEstimateInertia()
27const GET_GAS_BACKLOG: [u8; 4] = [0x1d, 0x5b, 0x5c, 0x20]; // getGasBacklog()
28const GET_PRICING_INERTIA: [u8; 4] = [0x3d, 0xfb, 0x45, 0xb9]; // getPricingInertia()
29const GET_GAS_BACKLOG_TOLERANCE: [u8; 4] = [0x25, 0x75, 0x4f, 0x91]; // getGasBacklogTolerance()
30const GET_L1_PRICING_SURPLUS: [u8; 4] = [0x52, 0x0a, 0xcd, 0xd7]; // getL1PricingSurplus()
31const GET_PER_BATCH_GAS_CHARGE: [u8; 4] = [0x6e, 0xcc, 0xa4, 0x5a]; // getPerBatchGasCharge()
32const GET_AMORTIZED_COST_CAP_BIPS: [u8; 4] = [0x7a, 0x7d, 0x6b, 0xeb]; // getAmortizedCostCapBips()
33const GET_L1_FEES_AVAILABLE: [u8; 4] = [0x5b, 0x39, 0xd2, 0x3c]; // getL1FeesAvailable()
34const GET_L1_REWARD_RATE: [u8; 4] = [0x8a, 0x5b, 0x1d, 0x28]; // getL1RewardRate()
35const GET_L1_REWARD_RECIPIENT: [u8; 4] = [0x9e, 0x6d, 0x7e, 0x31]; // getL1RewardRecipient()
36const GET_L1_PRICING_EQUILIBRATION_UNITS: [u8; 4] = [0xad, 0x26, 0xce, 0x90]; // getL1PricingEquilibrationUnits()
37const GET_LAST_L1_PRICING_UPDATE_TIME: [u8; 4] = [0x13, 0x8b, 0x47, 0xb4]; // getLastL1PricingUpdateTime()
38const GET_L1_PRICING_FUNDS_DUE_FOR_REWARDS: [u8; 4] = [0x96, 0x3d, 0x60, 0x02]; // getL1PricingFundsDueForRewards()
39const GET_L1_PRICING_UNITS_SINCE_UPDATE: [u8; 4] = [0xef, 0xf0, 0x13, 0x06]; // getL1PricingUnitsSinceUpdate()
40const GET_LAST_L1_PRICING_SURPLUS: [u8; 4] = [0x29, 0x87, 0xd0, 0x27]; // getLastL1PricingSurplus()
41const GET_MAX_BLOCK_GAS_LIMIT: [u8; 4] = [0x03, 0x71, 0xfd, 0xb4]; // getMaxBlockGasLimit()
42const GET_MAX_TX_GAS_LIMIT: [u8; 4] = [0xaa, 0xe1, 0xcd, 0x4c]; // getMaxTxGasLimit()
43const GET_GAS_PRICING_CONSTRAINTS: [u8; 4] = [0x23, 0x20, 0x27, 0xd1]; // getGasPricingConstraints()
44const GET_MULTI_GAS_PRICING_CONSTRAINTS: [u8; 4] = [0xbb, 0xfc, 0x0a, 0x72]; // getMultiGasPricingConstraints()
45const GET_MULTI_GAS_BASE_FEE: [u8; 4] = [0xc0, 0xe1, 0x0b, 0xbb]; // getMultiGasBaseFee()
46                                                                  // Legacy selectors for WithAggregator variants (aggregator param is ignored post-v4).
47const GET_PRICES_IN_WEI_WITH_AGG: [u8; 4] = [0xba, 0x9c, 0x91, 0x6e]; // getPricesInWeiWithAggregator(address)
48const GET_PRICES_IN_ARBGAS_WITH_AGG: [u8; 4] = [0x7a, 0x1e, 0xa7, 0x32]; // getPricesInArbGasWithAggregator(address)
49
50// Gas costs.
51const SLOAD_GAS: u64 = 800;
52const COPY_GAS: u64 = 3;
53
54// L1 pricing field offsets (within L1 pricing subspace).
55const L1_PAY_REWARDS_TO: u64 = 0;
56const L1_INERTIA: u64 = 2;
57const L1_PER_UNIT_REWARD: u64 = 3;
58const L1_PRICE_PER_UNIT: u64 = 7;
59const L1_LAST_SURPLUS: u64 = 8;
60const L1_PER_BATCH_GAS_COST: u64 = 9;
61const L1_AMORTIZED_COST_CAP_BIPS: u64 = 10;
62const L1_EQUILIBRATION_UNITS: u64 = 1;
63const L1_LAST_UPDATE_TIME: u64 = 4;
64const L1_FUNDS_DUE_FOR_REWARDS: u64 = 5;
65const L1_UNITS_SINCE: u64 = 6;
66const L1_FEES_AVAILABLE: u64 = 11;
67
68// L2 pricing field offsets (within L2 pricing subspace).
69const L2_SPEED_LIMIT: u64 = 0;
70const L2_PER_BLOCK_GAS_LIMIT: u64 = 1;
71const L2_BASE_FEE: u64 = 2;
72const L2_MIN_BASE_FEE: u64 = 3;
73const L2_GAS_BACKLOG: u64 = 4;
74const L2_PRICING_INERTIA: u64 = 5;
75const L2_BACKLOG_TOLERANCE: u64 = 6;
76const L2_PER_TX_GAS_LIMIT: u64 = 7;
77
78const TX_DATA_NON_ZERO_GAS: u64 = 16;
79const ASSUMED_SIMPLE_TX_SIZE: u64 = 140;
80const STORAGE_WRITE_COST: u64 = 20_000;
81
82/// Batch poster table subspace key within L1 pricing.
83const BATCH_POSTER_TABLE_KEY: &[u8] = &[0];
84/// TotalFundsDue offset within batch poster table subspace.
85const TOTAL_FUNDS_DUE_OFFSET: u64 = 0;
86
87/// L1 pricer funds pool address (0xa4b05...fffffffffffffffffffffffffffffffffff).
88const L1_PRICER_FUNDS_POOL_ADDRESS: Address = Address::new([
89    0xa4, 0xb0, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
90    0xff, 0xff, 0xff, 0xff,
91]);
92
93pub fn create_arbgasinfo_precompile() -> DynPrecompile {
94    DynPrecompile::new_stateful(PrecompileId::custom("arbgasinfo"), handler)
95}
96
97fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
98    let gas_limit = input.gas;
99    let data = input.data;
100    if data.len() < 4 {
101        return Err(PrecompileError::other("input too short"));
102    }
103
104    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
105
106    let result = match selector {
107        GET_L1_BASEFEE_ESTIMATE | GET_L1_GAS_PRICE_ESTIMATE => {
108            read_l1_field(&mut input, L1_PRICE_PER_UNIT)
109        }
110        GET_MINIMUM_GAS_PRICE => read_l2_field(&mut input, L2_MIN_BASE_FEE),
111        GET_PRICES_IN_WEI | GET_PRICES_IN_WEI_WITH_AGG => handle_prices_in_wei(&mut input),
112        GET_GAS_ACCOUNTING_PARAMS => handle_gas_accounting_params(&mut input),
113        GET_CURRENT_TX_L1_FEES => {
114            // Nitro reads c.txProcessor.PosterFee (in-memory, no storage access).
115            // We use a thread-local set by the executor to avoid extra sloads
116            // that would charge EVM gas and cause gasUsed to differ.
117            let gas_limit = input.gas;
118            let fee_wei = crate::get_current_tx_poster_fee();
119            let fee = U256::from(fee_wei);
120            Ok(PrecompileOutput::new(
121                (SLOAD_GAS + COPY_GAS).min(gas_limit),
122                fee.to_be_bytes::<32>().to_vec().into(),
123            ))
124        }
125        GET_PRICES_IN_ARBGAS | GET_PRICES_IN_ARBGAS_WITH_AGG => handle_prices_in_arbgas(&mut input),
126        GET_L1_BASEFEE_ESTIMATE_INERTIA => read_l1_field(&mut input, L1_INERTIA),
127        GET_GAS_BACKLOG => read_l2_field(&mut input, L2_GAS_BACKLOG),
128        GET_PRICING_INERTIA => read_l2_field(&mut input, L2_PRICING_INERTIA),
129        GET_GAS_BACKLOG_TOLERANCE => read_l2_field(&mut input, L2_BACKLOG_TOLERANCE),
130        GET_L1_PRICING_SURPLUS => handle_l1_pricing_surplus(&mut input),
131        GET_PER_BATCH_GAS_CHARGE => read_l1_field(&mut input, L1_PER_BATCH_GAS_COST),
132        GET_AMORTIZED_COST_CAP_BIPS => read_l1_field(&mut input, L1_AMORTIZED_COST_CAP_BIPS),
133        // GetL1FeesAvailable: ArbOS >= 10
134        GET_L1_FEES_AVAILABLE => {
135            if let Some(r) = crate::check_method_version(10, 0) {
136                return r;
137            }
138            read_l1_field(&mut input, L1_FEES_AVAILABLE)
139        }
140        // GetL1RewardRate: ArbOS >= 11
141        GET_L1_REWARD_RATE => {
142            if let Some(r) = crate::check_method_version(11, 0) {
143                return r;
144            }
145            read_l1_field(&mut input, L1_PER_UNIT_REWARD)
146        }
147        // GetL1RewardRecipient: ArbOS >= 11
148        GET_L1_REWARD_RECIPIENT => {
149            if let Some(r) = crate::check_method_version(11, 0) {
150                return r;
151            }
152            read_l1_field(&mut input, L1_PAY_REWARDS_TO)
153        }
154        // GetL1PricingEquilibrationUnits: ArbOS >= 20
155        GET_L1_PRICING_EQUILIBRATION_UNITS => {
156            if let Some(r) = crate::check_method_version(20, 0) {
157                return r;
158            }
159            read_l1_field(&mut input, L1_EQUILIBRATION_UNITS)
160        }
161        // GetLastL1PricingUpdateTime: ArbOS >= 20
162        GET_LAST_L1_PRICING_UPDATE_TIME => {
163            if let Some(r) = crate::check_method_version(20, 0) {
164                return r;
165            }
166            read_l1_field(&mut input, L1_LAST_UPDATE_TIME)
167        }
168        // GetL1PricingFundsDueForRewards: ArbOS >= 20
169        GET_L1_PRICING_FUNDS_DUE_FOR_REWARDS => {
170            if let Some(r) = crate::check_method_version(20, 0) {
171                return r;
172            }
173            read_l1_field(&mut input, L1_FUNDS_DUE_FOR_REWARDS)
174        }
175        // GetL1PricingUnitsSinceUpdate: ArbOS >= 20
176        GET_L1_PRICING_UNITS_SINCE_UPDATE => {
177            if let Some(r) = crate::check_method_version(20, 0) {
178                return r;
179            }
180            read_l1_field(&mut input, L1_UNITS_SINCE)
181        }
182        // GetLastL1PricingSurplus: ArbOS >= 20
183        GET_LAST_L1_PRICING_SURPLUS => {
184            if let Some(r) = crate::check_method_version(20, 0) {
185                return r;
186            }
187            read_l1_field(&mut input, L1_LAST_SURPLUS)
188        }
189        // GetMaxBlockGasLimit: ArbOS >= 50
190        GET_MAX_BLOCK_GAS_LIMIT => {
191            if let Some(r) = crate::check_method_version(50, 0) {
192                return r;
193            }
194            read_l2_field(&mut input, L2_PER_BLOCK_GAS_LIMIT)
195        }
196        // GetMaxTxGasLimit: ArbOS >= 50
197        GET_MAX_TX_GAS_LIMIT => {
198            if let Some(r) = crate::check_method_version(50, 0) {
199                return r;
200            }
201            read_l2_field(&mut input, L2_PER_TX_GAS_LIMIT)
202        }
203        // GetGasPricingConstraints: ArbOS >= 50
204        GET_GAS_PRICING_CONSTRAINTS => {
205            if let Some(r) = crate::check_method_version(50, 0) {
206                return r;
207            }
208            handle_gas_pricing_constraints(&mut input)
209        }
210        // GetMultiGasPricingConstraints: ArbOS >= 60
211        GET_MULTI_GAS_PRICING_CONSTRAINTS => {
212            if let Some(r) = crate::check_method_version(60, 0) {
213                return r;
214            }
215            handle_multi_gas_pricing_constraints(&mut input)
216        }
217        // GetMultiGasBaseFee: ArbOS >= 60
218        GET_MULTI_GAS_BASE_FEE => {
219            if let Some(r) = crate::check_method_version(60, 0) {
220                return r;
221            }
222            handle_multi_gas_base_fee(&mut input)
223        }
224        _ => Err(PrecompileError::other("unknown selector")),
225    };
226    crate::gas_check(gas_limit, result)
227}
228
229// ── helpers ──────────────────────────────────────────────────────────
230
231fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
232    input
233        .internals_mut()
234        .load_account(ARBOS_STATE_ADDRESS)
235        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
236    Ok(())
237}
238
239fn sload_field(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
240    let val = input
241        .internals_mut()
242        .sload(ARBOS_STATE_ADDRESS, slot)
243        .map_err(|_| PrecompileError::other("sload failed"))?;
244    Ok(val.data)
245}
246
247fn read_l1_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
248    let gas_limit = input.gas;
249    load_arbos(input)?;
250    let field_slot = subspace_slot(L1_PRICING_SUBSPACE, offset);
251    let value = sload_field(input, field_slot)?;
252    Ok(PrecompileOutput::new(
253        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
254        value.to_be_bytes::<32>().to_vec().into(),
255    ))
256}
257
258fn read_l2_field(input: &mut PrecompileInput<'_>, offset: u64) -> PrecompileResult {
259    let gas_limit = input.gas;
260    load_arbos(input)?;
261    let field_slot = subspace_slot(L2_PRICING_SUBSPACE, offset);
262    let value = sload_field(input, field_slot)?;
263    Ok(PrecompileOutput::new(
264        (2 * SLOAD_GAS + COPY_GAS).min(gas_limit),
265        value.to_be_bytes::<32>().to_vec().into(),
266    ))
267}
268
269/// Compute L1 pricing surplus.
270/// v10+: `L1FeesAvailable - (TotalFundsDue + FundsDueForRewards)` (signed).
271/// pre-v10: `Balance(L1PricerFundsPool) - (TotalFundsDue + FundsDueForRewards)`.
272fn handle_l1_pricing_surplus(input: &mut PrecompileInput<'_>) -> PrecompileResult {
273    let gas_limit = input.gas;
274    let arbos_version = crate::get_arbos_version();
275
276    load_arbos(input)?;
277
278    // Read TotalFundsDue from batch poster table subspace.
279    let l1_sub_key = derive_subspace_key(ROOT_STORAGE_KEY, L1_PRICING_SUBSPACE);
280    let bpt_key = derive_subspace_key(l1_sub_key.as_slice(), BATCH_POSTER_TABLE_KEY);
281    let total_funds_due_slot = map_slot(bpt_key.as_slice(), TOTAL_FUNDS_DUE_OFFSET);
282    let total_funds_due = sload_field(input, total_funds_due_slot)?;
283
284    // Read FundsDueForRewards from L1 pricing subspace.
285    let fdr_slot = subspace_slot(L1_PRICING_SUBSPACE, L1_FUNDS_DUE_FOR_REWARDS);
286    let funds_due_for_rewards = sload_field(input, fdr_slot)?;
287
288    let need_funds = total_funds_due.saturating_add(funds_due_for_rewards);
289
290    let have_funds = if arbos_version >= 10 {
291        // v10+: read from stored L1FeesAvailable.
292        let slot = subspace_slot(L1_PRICING_SUBSPACE, L1_FEES_AVAILABLE);
293        sload_field(input, slot)?
294    } else {
295        // pre-v10: read actual balance of L1PricerFundsPool.
296        let account = input
297            .internals_mut()
298            .load_account(L1_PRICER_FUNDS_POOL_ADDRESS)
299            .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
300        account.data.info.balance
301    };
302
303    // Signed result: surplus can be negative.
304    let surplus = if have_funds >= need_funds {
305        have_funds - need_funds
306    } else {
307        // Two's complement encoding for negative value.
308        let deficit = need_funds - have_funds;
309        U256::ZERO.wrapping_sub(deficit)
310    };
311
312    let gas_cost = (4 * SLOAD_GAS + COPY_GAS).min(gas_limit);
313    Ok(PrecompileOutput::new(
314        gas_cost,
315        surplus.to_be_bytes::<32>().to_vec().into(),
316    ))
317}
318
319fn handle_prices_in_wei(input: &mut PrecompileInput<'_>) -> PrecompileResult {
320    let data_len = input.data.len();
321    let gas_limit = input.gas;
322    load_arbos(input)?;
323
324    let l1_price = sload_field(input, subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
325    let l2_base = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?;
326    let l2_min = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_MIN_BASE_FEE))?;
327
328    let wei_for_l1_calldata = l1_price.saturating_mul(U256::from(TX_DATA_NON_ZERO_GAS));
329    let per_l2_tx = wei_for_l1_calldata.saturating_mul(U256::from(ASSUMED_SIMPLE_TX_SIZE));
330    let per_arbgas_base = l2_base.min(l2_min);
331    let per_arbgas_congestion = l2_base.saturating_sub(per_arbgas_base);
332    let per_arbgas_total = l2_base;
333    let wei_for_l2_storage = l2_base.saturating_mul(U256::from(STORAGE_WRITE_COST));
334
335    let mut out = Vec::with_capacity(192);
336    out.extend_from_slice(&per_l2_tx.to_be_bytes::<32>());
337    out.extend_from_slice(&wei_for_l1_calldata.to_be_bytes::<32>());
338    out.extend_from_slice(&wei_for_l2_storage.to_be_bytes::<32>());
339    out.extend_from_slice(&per_arbgas_base.to_be_bytes::<32>());
340    out.extend_from_slice(&per_arbgas_congestion.to_be_bytes::<32>());
341    out.extend_from_slice(&per_arbgas_total.to_be_bytes::<32>());
342
343    // baseFee is read from evm.Context (free), only 2 body SLOADs (PricePerUnit, MinBaseFee).
344    let arg_words = (data_len as u64).saturating_sub(4).div_ceil(32);
345    let gas_cost = (3 * SLOAD_GAS + (arg_words + 6) * COPY_GAS).min(gas_limit);
346    Ok(PrecompileOutput::new(gas_cost, out.into()))
347}
348
349fn handle_gas_accounting_params(input: &mut PrecompileInput<'_>) -> PrecompileResult {
350    let gas_limit = input.gas;
351    load_arbos(input)?;
352
353    let speed_limit = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_SPEED_LIMIT))?;
354    let gas_limit_val = sload_field(
355        input,
356        subspace_slot(L2_PRICING_SUBSPACE, L2_PER_BLOCK_GAS_LIMIT),
357    )?;
358
359    let mut out = Vec::with_capacity(96);
360    out.extend_from_slice(&speed_limit.to_be_bytes::<32>());
361    out.extend_from_slice(&gas_limit_val.to_be_bytes::<32>());
362    out.extend_from_slice(&gas_limit_val.to_be_bytes::<32>());
363
364    Ok(PrecompileOutput::new(
365        (3 * SLOAD_GAS + 3 * COPY_GAS).min(gas_limit),
366        out.into(),
367    ))
368}
369
370fn handle_prices_in_arbgas(input: &mut PrecompileInput<'_>) -> PrecompileResult {
371    let data_len = input.data.len();
372    let gas_limit = input.gas;
373    load_arbos(input)?;
374
375    let l1_price = sload_field(input, subspace_slot(L1_PRICING_SUBSPACE, L1_PRICE_PER_UNIT))?;
376    let l2_base = sload_field(input, subspace_slot(L2_PRICING_SUBSPACE, L2_BASE_FEE))?;
377
378    let wei_for_l1_calldata = l1_price.saturating_mul(U256::from(TX_DATA_NON_ZERO_GAS));
379    let wei_per_l2_tx = wei_for_l1_calldata.saturating_mul(U256::from(ASSUMED_SIMPLE_TX_SIZE));
380
381    let (gas_for_l1_calldata, gas_per_l2_tx) = if l2_base > U256::ZERO {
382        (wei_for_l1_calldata / l2_base, wei_per_l2_tx / l2_base)
383    } else {
384        (U256::ZERO, U256::ZERO)
385    };
386
387    let mut out = Vec::with_capacity(96);
388    out.extend_from_slice(&gas_per_l2_tx.to_be_bytes::<32>());
389    out.extend_from_slice(&gas_for_l1_calldata.to_be_bytes::<32>());
390    out.extend_from_slice(&U256::from(STORAGE_WRITE_COST).to_be_bytes::<32>());
391
392    // baseFee is read from evm.Context (free), only 1 body SLOAD (PricePerUnit).
393    let arg_words = (data_len as u64).saturating_sub(4).div_ceil(32);
394    let gas_cost = (2 * SLOAD_GAS + (arg_words + 3) * COPY_GAS).min(gas_limit);
395    Ok(PrecompileOutput::new(gas_cost, out.into()))
396}
397
398// ── Constraint getters (ArbOS v50+) ─────────────────────────────────
399
400/// Constraint field offsets (matching gas_constraint.rs / multi_gas_constraint.rs).
401const CONSTRAINT_TARGET: u64 = 0;
402const CONSTRAINT_ADJ_WINDOW: u64 = 1;
403const CONSTRAINT_BACKLOG: u64 = 2;
404const MULTI_CONSTRAINT_WEIGHTED_BASE: u64 = 4;
405
406const NUM_RESOURCE_KIND: u64 = 8;
407/// Offset within MultiGasFees for current-block fees.
408const CURRENT_BLOCK_FEES_OFFSET: u64 = NUM_RESOURCE_KIND;
409
410/// Returns `[][3]uint64` — (target, adjustmentWindow, backlog) per constraint.
411fn handle_gas_pricing_constraints(input: &mut PrecompileInput<'_>) -> PrecompileResult {
412    let gas_limit = input.gas;
413    load_arbos(input)?;
414
415    let vec_key = gas_constraints_vec_key();
416    let count = sload_field(input, vector_length_slot(&vec_key))?.saturating_to::<u64>();
417    let mut sloads: u64 = 2; // 1 for OpenArbosState + 1 for vec length
418
419    // ABI: offset to dynamic array, then length, then N×3 uint64 values.
420    let mut out = Vec::with_capacity(64 + count as usize * 96);
421    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
422    out.extend_from_slice(&U256::from(count).to_be_bytes::<32>());
423
424    for i in 0..count {
425        let target = sload_field(input, vector_element_field(&vec_key, i, CONSTRAINT_TARGET))?;
426        let window = sload_field(
427            input,
428            vector_element_field(&vec_key, i, CONSTRAINT_ADJ_WINDOW),
429        )?;
430        let backlog = sload_field(input, vector_element_field(&vec_key, i, CONSTRAINT_BACKLOG))?;
431
432        out.extend_from_slice(&target.to_be_bytes::<32>());
433        out.extend_from_slice(&window.to_be_bytes::<32>());
434        out.extend_from_slice(&backlog.to_be_bytes::<32>());
435        sloads += 3;
436    }
437
438    let result_words = (out.len() as u64).div_ceil(32);
439    Ok(PrecompileOutput::new(
440        (sloads * SLOAD_GAS + result_words * COPY_GAS).min(gas_limit),
441        out.into(),
442    ))
443}
444
445/// Returns `[]MultiGasConstraint` ABI-encoded.
446///
447/// MultiGasConstraint = (WeightedResource[] resources, uint32 adjustmentWindowSecs,
448///                        uint64 targetPerSec, uint64 backlog)
449/// WeightedResource   = (uint8 resource, uint64 weight)
450fn handle_multi_gas_pricing_constraints(input: &mut PrecompileInput<'_>) -> PrecompileResult {
451    let gas_limit = input.gas;
452    load_arbos(input)?;
453
454    let vec_key = multi_gas_constraints_vec_key();
455    let count = sload_field(input, vector_length_slot(&vec_key))?.saturating_to::<u64>();
456    let mut sloads: u64 = 2; // 1 for OpenArbosState + 1 for vec length
457
458    // Collect per-constraint data before encoding, since we need to know sizes for offsets.
459    struct ConstraintData {
460        target: U256,
461        window: U256,
462        backlog: U256,
463        resources: Vec<(u8, U256)>,
464    }
465    let mut constraints = Vec::with_capacity(count as usize);
466
467    for i in 0..count {
468        let target = sload_field(input, vector_element_field(&vec_key, i, CONSTRAINT_TARGET))?;
469        let window = sload_field(
470            input,
471            vector_element_field(&vec_key, i, CONSTRAINT_ADJ_WINDOW),
472        )?;
473        let backlog = sload_field(input, vector_element_field(&vec_key, i, CONSTRAINT_BACKLOG))?;
474        sloads += 3;
475
476        let elem_key = vector_element_key(&vec_key, i);
477        let mut resources = Vec::new();
478        for kind in 0..NUM_RESOURCE_KIND {
479            let w = sload_field(
480                input,
481                map_slot(elem_key.as_slice(), MULTI_CONSTRAINT_WEIGHTED_BASE + kind),
482            )?;
483            sloads += 1;
484            if w > U256::ZERO {
485                resources.push((kind as u8, w));
486            }
487        }
488        constraints.push(ConstraintData {
489            target,
490            window,
491            backlog,
492            resources,
493        });
494    }
495
496    // ABI-encode: dynamic array of dynamic tuples.
497    let n = constraints.len();
498    let mut out = Vec::new();
499
500    // Top-level: offset to outer array.
501    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
502    // Array length.
503    out.extend_from_slice(&U256::from(n).to_be_bytes::<32>());
504
505    // Element tuple size = 4 × 32 (head) + 32 (resources length) + resources.len() × 64.
506    let elem_sizes: Vec<usize> = constraints
507        .iter()
508        .map(|c| 4 * 32 + 32 + c.resources.len() * 64)
509        .collect();
510
511    // Write offsets (relative to start of offsets area).
512    let mut running_offset = n * 32;
513    for size in &elem_sizes {
514        out.extend_from_slice(&U256::from(running_offset).to_be_bytes::<32>());
515        running_offset += size;
516    }
517
518    // Write each element.
519    for c in &constraints {
520        let m = c.resources.len();
521        // Tuple head: offset to Resources data = 4 × 32 = 128.
522        out.extend_from_slice(&U256::from(4u64 * 32).to_be_bytes::<32>());
523        // AdjustmentWindowSecs (uint32).
524        out.extend_from_slice(&c.window.to_be_bytes::<32>());
525        // TargetPerSec (uint64).
526        out.extend_from_slice(&c.target.to_be_bytes::<32>());
527        // Backlog (uint64).
528        out.extend_from_slice(&c.backlog.to_be_bytes::<32>());
529        // Resources array length.
530        out.extend_from_slice(&U256::from(m).to_be_bytes::<32>());
531        // Each WeightedResource (uint8 resource, uint64 weight).
532        for &(kind, ref weight) in &c.resources {
533            out.extend_from_slice(&U256::from(kind).to_be_bytes::<32>());
534            out.extend_from_slice(&weight.to_be_bytes::<32>());
535        }
536    }
537
538    let result_words = (out.len() as u64).div_ceil(32);
539    Ok(PrecompileOutput::new(
540        (sloads * SLOAD_GAS + result_words * COPY_GAS).min(gas_limit),
541        out.into(),
542    ))
543}
544
545/// Returns `uint256[]` — current-block base fee per resource kind.
546fn handle_multi_gas_base_fee(input: &mut PrecompileInput<'_>) -> PrecompileResult {
547    let gas_limit = input.gas;
548    load_arbos(input)?;
549
550    let fees_key = multi_gas_base_fees_subspace();
551
552    let mut out = Vec::with_capacity(64 + NUM_RESOURCE_KIND as usize * 32);
553    // ABI: offset, then length, then values.
554    out.extend_from_slice(&U256::from(32u64).to_be_bytes::<32>());
555    out.extend_from_slice(&U256::from(NUM_RESOURCE_KIND).to_be_bytes::<32>());
556
557    for kind in 0..NUM_RESOURCE_KIND {
558        let slot = map_slot(fees_key.as_slice(), CURRENT_BLOCK_FEES_OFFSET + kind);
559        let fee = sload_field(input, slot)?;
560        out.extend_from_slice(&fee.to_be_bytes::<32>());
561    }
562
563    let result_words = (out.len() as u64).div_ceil(32);
564    Ok(PrecompileOutput::new(
565        ((1 + NUM_RESOURCE_KIND) * SLOAD_GAS + result_words * COPY_GAS).min(gas_limit),
566        out.into(),
567    ))
568}