arb_precompiles/
arbretryabletx.rs

1use std::sync::Arc;
2
3use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
4use alloy_primitives::{Address, B256, Log, U256, keccak256};
5use alloy_sol_types::{SolError, SolEvent, SolInterface};
6use arb_context::ArbPrecompileCtx;
7use arb_storage::ARBOS_STATE_ADDRESS;
8use arbos::retryables::{
9    CancelOutcome, LookupOutcome, RETRYABLE_LIFETIME_SECONDS, RETRYABLE_REAP_PRICE, RetryableError,
10};
11use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
12
13use crate::{ArbPrecompileError, interfaces::IArbRetryableTx};
14
15/// ArbRetryableTx precompile address (0x6e).
16pub const ARBRETRYABLETX_ADDRESS: Address = Address::new([
17    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
18    0x00, 0x00, 0x00, 0x6e,
19]);
20
21const SLOAD_GAS: u64 = 800;
22const SSTORE_GAS: u64 = 20_000;
23const SSTORE_ZERO_GAS: u64 = 5_000;
24const SSTORE_RESET_GAS: u64 = 5_000;
25const COPY_GAS: u64 = 3;
26const TX_GAS: u64 = 21_000;
27const LOG_GAS: u64 = 375;
28const LOG_TOPIC_GAS: u64 = 375;
29const LOG_DATA_GAS: u64 = 8;
30
31/// ABI-encoded data size for RedeemScheduled: 4 non-indexed params × 32 bytes.
32const REDEEM_SCHEDULED_DATA_BYTES: u64 = 128;
33
34/// Gas cost for emitting the RedeemScheduled event (LOG4 with 128 data bytes).
35const REDEEM_SCHEDULED_EVENT_COST: u64 =
36    LOG_GAS + 4 * LOG_TOPIC_GAS + LOG_DATA_GAS * REDEEM_SCHEDULED_DATA_BYTES;
37
38pub fn ticket_created_topic() -> B256 {
39    IArbRetryableTx::TicketCreated::SIGNATURE_HASH
40}
41
42pub fn redeem_scheduled_topic() -> B256 {
43    IArbRetryableTx::RedeemScheduled::SIGNATURE_HASH
44}
45
46pub fn lifetime_extended_topic() -> B256 {
47    IArbRetryableTx::LifetimeExtended::SIGNATURE_HASH
48}
49
50pub fn canceled_topic() -> B256 {
51    IArbRetryableTx::Canceled::SIGNATURE_HASH
52}
53
54pub fn create_arbretryabletx_precompile(ctx: Arc<ArbPrecompileCtx>) -> DynPrecompile {
55    DynPrecompile::new_stateful(PrecompileId::custom("arbretryabletx"), move |input| {
56        handler(input, &ctx)
57    })
58}
59
60fn handler(mut input: PrecompileInput<'_>, ctx: &ArbPrecompileCtx) -> PrecompileResult {
61    let mut gas_used = 0u64;
62    let gas_limit = input.gas;
63    crate::init_precompile_gas(&mut gas_used, ctx, input.data.len());
64
65    let call = match IArbRetryableTx::ArbRetryableTxCalls::abi_decode(input.data) {
66        Ok(c) => c,
67        Err(_) => return crate::burn_all_revert(gas_limit),
68    };
69    if let Some(r) = crate::reject_nonpayable_value(input.value, input.data, gas_limit, &[]) {
70        return r;
71    }
72    if let Some(r) = crate::reject_static_write(
73        input.is_static,
74        input.data,
75        gas_limit,
76        &[
77            [0xed, 0xa1, 0x12, 0x2c],
78            [0xf0, 0xb2, 0x1a, 0x41],
79            [0xc4, 0xd2, 0x52, 0xf5],
80            [0xc9, 0xf9, 0x5d, 0x32],
81        ],
82    ) {
83        return r;
84    }
85    if let Some(r) = crate::reject_delegate_nonpure(
86        input.target_address != input.bytecode_address,
87        input.data,
88        gas_limit,
89        &[],
90    ) {
91        return r;
92    }
93
94    use IArbRetryableTx::ArbRetryableTxCalls as Calls;
95    let result = match call {
96        Calls::getLifetime(_) => {
97            let lifetime = U256::from(RETRYABLE_LIFETIME_SECONDS);
98            crate::charge_computation(&mut gas_used, ctx, COPY_GAS);
99            Ok(PrecompileOutput::new(
100                (gas_used).min(gas_limit),
101                lifetime.to_be_bytes::<32>().to_vec().into(),
102            ))
103        }
104        Calls::getCurrentRedeemer(_) => {
105            let redeemer = ctx.tx_snapshot().redeemer_word();
106            crate::charge_computation(&mut gas_used, ctx, COPY_GAS);
107            Ok(PrecompileOutput::new(
108                (gas_used).min(gas_limit),
109                redeemer.to_be_bytes::<32>().to_vec().into(),
110            ))
111        }
112        Calls::submitRetryable(_) => {
113            let data = IArbRetryableTx::NotCallable {}.abi_encode();
114            return crate::sol_error_revert(&mut gas_used, ctx, data, gas_limit);
115        }
116        Calls::getTimeout(c) => handle_get_timeout(&mut input, &mut gas_used, c.ticketId, ctx),
117        Calls::getBeneficiary(c) => {
118            handle_get_beneficiary(&mut input, ctx, &mut gas_used, c.ticketId)
119        }
120        Calls::redeem(c) => handle_redeem(&mut input, ctx, &mut gas_used, c.ticketId),
121        Calls::keepalive(c) => handle_keepalive(&mut input, ctx, &mut gas_used, c.ticketId),
122        Calls::cancel(c) => handle_cancel(&mut input, ctx, &mut gas_used, c.ticketId),
123    };
124    crate::gas_check(ctx, gas_limit, gas_used, result)
125}
126
127fn load_arbos(input: &mut PrecompileInput<'_>) -> Result<(), ArbPrecompileError> {
128    input
129        .internals_mut()
130        .load_account(ARBOS_STATE_ADDRESS)
131        .map_err(ArbPrecompileError::fatal)?;
132    Ok(())
133}
134
135fn current_timestamp(input: &PrecompileInput<'_>) -> u64 {
136    input
137        .internals()
138        .block_timestamp()
139        .try_into()
140        .unwrap_or(u64::MAX)
141}
142
143/// Maps `RetryableError` from a precompile-layer lookup into the appropriate
144/// revert/fatal outcome: storage faults are fatal, missing-ticket/auth/window
145/// failures revert.
146fn map_retryable_error(err: RetryableError, gas_used: u64) -> ArbPrecompileError {
147    match err {
148        RetryableError::Storage(_) => ArbPrecompileError::fatal(err),
149        _ => ArbPrecompileError::empty_revert(gas_used),
150    }
151}
152
153/// Pre-v3 burns the remaining gas; v3+ emits the `NoTicketWithIDError`
154/// sol-error.
155fn not_found_revert(
156    ctx: &ArbPrecompileCtx,
157    gas_used: &mut u64,
158    gas_limit: u64,
159) -> PrecompileResult {
160    if ctx.block.arbos_version < arb_chainspec::arbos_version::ARBOS_VERSION_3 {
161        return crate::burn_all_revert(gas_limit);
162    }
163    let data = IArbRetryableTx::NoTicketWithID {}.abi_encode();
164    crate::sol_error_revert(gas_used, ctx, data, gas_limit)
165}
166
167fn handle_get_timeout(
168    input: &mut PrecompileInput<'_>,
169    gas_used: &mut u64,
170    ticket_id: B256,
171    ctx: &ArbPrecompileCtx,
172) -> PrecompileResult {
173    let gas_limit = input.gas;
174    let now = current_timestamp(input);
175    load_arbos(input)?;
176
177    let internals = input.internals_mut();
178    let arb_state = ctx
179        .block
180        .arbos_state(internals)
181        .map_err(ArbPrecompileError::fatal)?;
182
183    let lookup = arb_state
184        .retryable_state
185        .get_timeout(internals, ticket_id, now)
186        .map_err(|e| map_retryable_error(e, *gas_used))?;
187    crate::charge_storage_read(gas_used, ctx, lookup.extra_gas);
188    let effective_timeout = match lookup.outcome {
189        LookupOutcome::Found(t) => t,
190        LookupOutcome::NoTicket => {
191            crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
192            let data = IArbRetryableTx::NoTicketWithID {}.abi_encode();
193            return crate::sol_error_revert(gas_used, ctx, data, gas_limit);
194        }
195    };
196
197    crate::charge_storage_read(gas_used, ctx, 3 * SLOAD_GAS);
198    crate::charge_computation(gas_used, ctx, COPY_GAS);
199    Ok(PrecompileOutput::new(
200        (*gas_used).min(gas_limit),
201        U256::from(effective_timeout)
202            .to_be_bytes::<32>()
203            .to_vec()
204            .into(),
205    ))
206}
207
208fn handle_get_beneficiary(
209    input: &mut PrecompileInput<'_>,
210    ctx: &ArbPrecompileCtx,
211    gas_used: &mut u64,
212    ticket_id: B256,
213) -> PrecompileResult {
214    let gas_limit = input.gas;
215    let now = current_timestamp(input);
216    load_arbos(input)?;
217
218    let internals = input.internals_mut();
219    let arb_state = ctx
220        .block
221        .arbos_state(internals)
222        .map_err(ArbPrecompileError::fatal)?;
223
224    let lookup = arb_state
225        .retryable_state
226        .get_beneficiary(internals, ticket_id, now)
227        .map_err(|e| map_retryable_error(e, *gas_used))?;
228    crate::charge_storage_read(gas_used, ctx, lookup.extra_gas);
229    let beneficiary = match lookup.outcome {
230        LookupOutcome::Found(addr) => addr,
231        LookupOutcome::NoTicket => {
232            crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
233            return not_found_revert(ctx, gas_used, gas_limit);
234        }
235    };
236
237    crate::charge_storage_read(gas_used, ctx, 2 * SLOAD_GAS);
238    crate::charge_computation(gas_used, ctx, COPY_GAS);
239    Ok(PrecompileOutput::new(
240        (*gas_used).min(gas_limit),
241        U256::from_be_slice(beneficiary.as_slice())
242            .to_be_bytes::<32>()
243            .to_vec()
244            .into(),
245    ))
246}
247
248fn handle_redeem(
249    input: &mut PrecompileInput<'_>,
250    ctx: &ArbPrecompileCtx,
251    gas_used: &mut u64,
252    ticket_id: B256,
253) -> PrecompileResult {
254    let gas_limit = input.gas;
255    let caller = input.caller;
256    let now = current_timestamp(input);
257
258    {
259        let current_retryable = ctx.tx_snapshot().retryable_id;
260        if !current_retryable.is_zero() && current_retryable == ticket_id {
261            return Err(ArbPrecompileError::empty_revert(*gas_used).into());
262        }
263    }
264
265    load_arbos(input)?;
266    let internals = input.internals_mut();
267    let arb_state = ctx
268        .block
269        .arbos_state(internals)
270        .map_err(ArbPrecompileError::fatal)?;
271    let retryable_state = &arb_state.retryable_state;
272
273    let (opened, open_extra) = retryable_state
274        .open_retryable_metered(internals, ticket_id, now)
275        .map_err(|e| map_retryable_error(e, *gas_used))?;
276    crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
277    crate::charge_storage_read(gas_used, ctx, open_extra);
278
279    let calldata_raw_size = if let Some(ref ret) = opened {
280        let size = ret
281            .calldata_size(internals)
282            .map_err(|e| map_retryable_error(e, *gas_used))?;
283        crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
284        size
285    } else {
286        0
287    };
288
289    let calldata_words = calldata_raw_size.div_ceil(32);
290    let write_bytes = if opened.is_some() {
291        let nbytes = 6 * 32 + 32 + 32 * calldata_words;
292        nbytes.div_ceil(32)
293    } else {
294        0
295    };
296
297    const PARAMS_SLOAD_GAS: u64 = 50;
298    crate::charge_storage_read(gas_used, ctx, PARAMS_SLOAD_GAS.saturating_mul(write_bytes));
299
300    let inc = retryable_state
301        .increment_num_tries_for(internals, ticket_id, now)
302        .map_err(|e| map_retryable_error(e, *gas_used))?;
303    crate::charge_storage_read(gas_used, ctx, inc.extra_gas);
304    let nonce = match inc.outcome {
305        LookupOutcome::Found(n) => n,
306        LookupOutcome::NoTicket => {
307            crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
308            return not_found_revert(ctx, gas_used, gas_limit);
309        }
310    };
311    crate::charge_storage_read(gas_used, ctx, 2 * SLOAD_GAS);
312    crate::charge_storage_write(gas_used, ctx, SSTORE_GAS);
313
314    let make_tx_reads = 5 + calldata_raw_size / 32;
315    crate::charge_storage_read(gas_used, ctx, make_tx_reads * SLOAD_GAS);
316
317    let mut hash_input = [0u8; 64];
318    hash_input[..32].copy_from_slice(ticket_id.as_slice());
319    hash_input[32..].copy_from_slice(&U256::from(nonce).to_be_bytes::<32>());
320    let retry_tx_hash = keccak256(hash_input);
321
322    let backlog_reservation = compute_backlog_update_cost(input, ctx, gas_used)?;
323
324    let gas_used_so_far = *gas_used;
325    let future_gas_costs = REDEEM_SCHEDULED_EVENT_COST + COPY_GAS + backlog_reservation;
326    let gas_remaining = gas_limit.saturating_sub(gas_used_so_far);
327    if gas_remaining < future_gas_costs {
328        *gas_used = gas_limit;
329        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
330    }
331    let gas_to_donate = gas_remaining - future_gas_costs;
332    if gas_to_donate < TX_GAS {
333        return Err(ArbPrecompileError::empty_revert(*gas_used).into());
334    }
335
336    let actual_backlog_cost = compute_actual_backlog_cost(input, ctx, gas_to_donate)?;
337
338    let max_refund = U256::MAX;
339    let submission_fee_refund = U256::ZERO;
340
341    let topic0 = redeem_scheduled_topic();
342    let topic1 = ticket_id;
343    let topic2 = B256::from(retry_tx_hash);
344    let mut seq_bytes = [0u8; 32];
345    seq_bytes[24..32].copy_from_slice(&nonce.to_be_bytes());
346    let topic3 = B256::from(seq_bytes);
347
348    let mut event_data = Vec::with_capacity(128);
349    event_data.extend_from_slice(&U256::from(gas_to_donate).to_be_bytes::<32>());
350    event_data.extend_from_slice(&B256::left_padding_from(caller.as_slice()).0);
351    event_data.extend_from_slice(&max_refund.to_be_bytes::<32>());
352    event_data.extend_from_slice(&submission_fee_refund.to_be_bytes::<32>());
353
354    input.internals_mut().log(Log::new_unchecked(
355        ARBRETRYABLETX_ADDRESS,
356        vec![topic0, topic1, topic2, topic3],
357        event_data.into(),
358    ));
359
360    crate::charge_history_growth(gas_used, ctx, REDEEM_SCHEDULED_EVENT_COST);
361    // `gas_to_donate` is forwarded to the scheduled retry tx and folds into
362    // its execution gas; attribute as Computation at this precompile's
363    // receipt to preserve the single-gas total.
364    crate::charge_computation(gas_used, ctx, gas_to_donate);
365    // `actual_backlog_cost` is the per-constraint SLOAD + SSTORE charge for
366    // updating each constraint's backlog when the donation is applied.
367    crate::charge_storage_write(gas_used, ctx, actual_backlog_cost);
368    crate::charge_computation(gas_used, ctx, COPY_GAS);
369    let _ = gas_used_so_far;
370
371    Ok(PrecompileOutput::new(
372        (*gas_used).min(gas_limit),
373        retry_tx_hash.to_vec().into(),
374    ))
375}
376
377fn handle_keepalive(
378    input: &mut PrecompileInput<'_>,
379    ctx: &ArbPrecompileCtx,
380    gas_used: &mut u64,
381    ticket_id: B256,
382) -> PrecompileResult {
383    let gas_limit = input.gas;
384    let now = current_timestamp(input);
385    load_arbos(input)?;
386
387    let internals = input.internals_mut();
388    let arb_state = ctx
389        .block
390        .arbos_state(internals)
391        .map_err(ArbPrecompileError::fatal)?;
392    let retryable_state = &arb_state.retryable_state;
393
394    let (calldata_size, ka_extra_a) = retryable_state
395        .calldata_size_for(internals, ticket_id, now)
396        .map_err(|e| map_retryable_error(e, *gas_used))?;
397    crate::charge_storage_read(gas_used, ctx, ka_extra_a);
398
399    let window_limit = now + RETRYABLE_LIFETIME_SECONDS;
400    let lookup = match retryable_state.keepalive(internals, ticket_id, now, window_limit, 0) {
401        Ok(l) => l,
402        Err(RetryableError::TimeoutTooFarFuture) => {
403            return Err(ArbPrecompileError::empty_revert(*gas_used).into());
404        }
405        Err(e) => return Err(map_retryable_error(e, *gas_used).into()),
406    };
407    crate::charge_storage_read(gas_used, ctx, lookup.extra_gas);
408    let new_timeout = match lookup.outcome {
409        LookupOutcome::Found(t) => t,
410        LookupOutcome::NoTicket => {
411            crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
412            return not_found_revert(ctx, gas_used, gas_limit);
413        }
414    };
415
416    let topic0 = lifetime_extended_topic();
417    let mut event_data = Vec::with_capacity(32);
418    event_data.extend_from_slice(&U256::from(new_timeout).to_be_bytes::<32>());
419    input.internals_mut().log(Log::new_unchecked(
420        ARBRETRYABLETX_ADDRESS,
421        vec![topic0, ticket_id],
422        event_data.into(),
423    ));
424
425    let calldata_words = calldata_size.div_ceil(32);
426    let nbytes = 6 * 32 + 32 + 32 * calldata_words;
427    let update_cost = nbytes.div_ceil(32) * (SSTORE_GAS / 100);
428    let event_cost = LOG_GAS + 2 * LOG_TOPIC_GAS + LOG_DATA_GAS * 32;
429
430    crate::charge_storage_read(gas_used, ctx, 7 * SLOAD_GAS + update_cost);
431    crate::charge_storage_write(gas_used, ctx, 3 * SSTORE_GAS);
432    crate::charge_history_growth(gas_used, ctx, event_cost);
433    crate::charge_computation(gas_used, ctx, COPY_GAS + RETRYABLE_REAP_PRICE);
434
435    Ok(PrecompileOutput::new(
436        (*gas_used).min(gas_limit),
437        U256::from(new_timeout).to_be_bytes::<32>().to_vec().into(),
438    ))
439}
440
441fn handle_cancel(
442    input: &mut PrecompileInput<'_>,
443    ctx: &ArbPrecompileCtx,
444    gas_used: &mut u64,
445    ticket_id: B256,
446) -> PrecompileResult {
447    let gas_limit = input.gas;
448    let caller = input.caller;
449    let now = current_timestamp(input);
450
451    {
452        let current_retryable = ctx.tx_snapshot().retryable_id;
453        if !current_retryable.is_zero() && current_retryable == ticket_id {
454            return Err(ArbPrecompileError::empty_revert(*gas_used).into());
455        }
456    }
457
458    load_arbos(input)?;
459
460    let internals = input.internals_mut();
461    let arb_state = ctx
462        .block
463        .arbos_state(internals)
464        .map_err(ArbPrecompileError::fatal)?;
465    let retryable_state = &arb_state.retryable_state;
466
467    let lookup = retryable_state
468        .cancel(internals, ticket_id, caller, now)
469        .map_err(|e| map_retryable_error(e, *gas_used))?;
470    crate::charge_storage_read(gas_used, ctx, lookup.extra_gas);
471    let calldata_size = match lookup.outcome {
472        CancelOutcome::Cleared {
473            calldata_size,
474            beneficiary,
475        } => {
476            let escrow = arbos::retryables::retryable_escrow_address(ticket_id);
477            ctx.set_cancel_escrow_sweep(escrow, beneficiary);
478            calldata_size
479        }
480        CancelOutcome::NoTicket => {
481            crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
482            return not_found_revert(ctx, gas_used, gas_limit);
483        }
484        CancelOutcome::NotBeneficiary => {
485            crate::charge_storage_read(gas_used, ctx, 2 * SLOAD_GAS);
486            return Err(ArbPrecompileError::empty_revert(*gas_used).into());
487        }
488    };
489
490    input.internals_mut().log(Log::new_unchecked(
491        ARBRETRYABLETX_ADDRESS,
492        vec![canceled_topic(), ticket_id],
493        Default::default(),
494    ));
495
496    let calldata_words = calldata_size.div_ceil(32);
497    // Length-slot reset is charged even for empty calldata.
498    let clear_bytes_cost = (calldata_words + 1) * SSTORE_ZERO_GAS;
499    let event_cost = LOG_GAS + 2 * LOG_TOPIC_GAS;
500
501    crate::charge_storage_read(gas_used, ctx, 5 * SLOAD_GAS);
502    crate::charge_storage_write(gas_used, ctx, 7 * SSTORE_ZERO_GAS + clear_bytes_cost);
503    crate::charge_history_growth(gas_used, ctx, event_cost);
504
505    Ok(PrecompileOutput::new(
506        (*gas_used).min(gas_limit),
507        Vec::new().into(),
508    ))
509}
510
511fn compute_backlog_update_cost(
512    input: &mut PrecompileInput<'_>,
513    ctx: &ArbPrecompileCtx,
514    gas_used: &mut u64,
515) -> Result<u64, ArbPrecompileError> {
516    use arb_chainspec::arbos_version as arb_ver;
517    let arbos_version = ctx.block.arbos_version;
518    if arbos_version >= arb_ver::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS {
519        return Ok(arbos::l2_pricing::MULTI_CONSTRAINT_STATIC_BACKLOG_UPDATE_COST);
520    }
521
522    let mut result = 0u64;
523    if arbos_version >= arb_ver::ARBOS_VERSION_50 {
524        result += SLOAD_GAS;
525    }
526    if arbos_version >= arb_ver::ARBOS_VERSION_MULTI_CONSTRAINT_FIX {
527        let len = read_gas_constraints_length(input, ctx)?;
528        crate::charge_storage_read(gas_used, ctx, SLOAD_GAS);
529        if len > 0 {
530            result += SLOAD_GAS;
531            result += len.saturating_mul(SLOAD_GAS + SSTORE_GAS);
532            return Ok(result);
533        }
534    }
535    result += SLOAD_GAS + SSTORE_GAS;
536    Ok(result)
537}
538
539fn compute_actual_backlog_cost(
540    input: &mut PrecompileInput<'_>,
541    ctx: &ArbPrecompileCtx,
542    gas_to_donate: u64,
543) -> Result<u64, ArbPrecompileError> {
544    use arb_chainspec::arbos_version as arb_ver;
545    let arbos_version = ctx.block.arbos_version;
546    if arbos_version >= arb_ver::ARBOS_VERSION_MULTI_GAS_CONSTRAINTS {
547        return Ok(arbos::l2_pricing::MULTI_CONSTRAINT_STATIC_BACKLOG_UPDATE_COST);
548    }
549    if arbos_version >= arb_ver::ARBOS_VERSION_MULTI_CONSTRAINT_FIX {
550        let internals = input.internals_mut();
551        let arb_state = ctx
552            .block
553            .arbos_state(internals)
554            .map_err(ArbPrecompileError::fatal)?;
555        let len = arb_state
556            .l2_pricing_state
557            .gas_constraints_length(internals)
558            .map_err(ArbPrecompileError::fatal)?;
559        if len > 0 {
560            let mut total = 2 * SLOAD_GAS;
561            for i in 0..len {
562                let constraint = arb_state.l2_pricing_state.open_gas_constraint_at(i);
563                let backlog = constraint
564                    .backlog(internals)
565                    .map_err(ArbPrecompileError::fatal)?;
566                total += constraint_actual_backlog_cost(backlog, gas_to_donate);
567            }
568            return Ok(total);
569        }
570    }
571    let mut cost = legacy_actual_backlog_cost(ctx.block.current_gas_backlog(), gas_to_donate);
572    if arbos_version >= arb_ver::ARBOS_VERSION_50 {
573        // Matches the constraints-length read reserved in compute_backlog_update_cost.
574        cost += SLOAD_GAS;
575    }
576    Ok(cost)
577}
578
579fn legacy_actual_backlog_cost(current_backlog: u64, gas_to_donate: u64) -> u64 {
580    let new_backlog = current_backlog.saturating_sub(gas_to_donate);
581    let write_cost = if new_backlog == 0 {
582        SSTORE_RESET_GAS
583    } else {
584        SSTORE_GAS
585    };
586    SLOAD_GAS + write_cost
587}
588
589#[inline]
590fn constraint_actual_backlog_cost(current_backlog: u64, gas_to_donate: u64) -> u64 {
591    let new_backlog = current_backlog.saturating_sub(gas_to_donate);
592    let write_cost = if new_backlog == 0 {
593        SSTORE_RESET_GAS
594    } else {
595        SSTORE_GAS
596    };
597    SLOAD_GAS + write_cost
598}
599
600fn read_gas_constraints_length(
601    input: &mut PrecompileInput<'_>,
602    ctx: &ArbPrecompileCtx,
603) -> Result<u64, ArbPrecompileError> {
604    let internals = input.internals_mut();
605    let arb_state = ctx
606        .block
607        .arbos_state(internals)
608        .map_err(ArbPrecompileError::fatal)?;
609    arb_state
610        .l2_pricing_state
611        .gas_constraints_length(internals)
612        .map_err(ArbPrecompileError::fatal)
613}
614
615#[cfg(test)]
616mod redeem_gas_tests {
617    use super::*;
618
619    #[test]
620    fn drains_backlog_to_zero_uses_sstore_reset() {
621        assert_eq!(
622            legacy_actual_backlog_cost(100_000, 100_000),
623            SLOAD_GAS + SSTORE_RESET_GAS,
624        );
625        assert_eq!(legacy_actual_backlog_cost(100_000, 100_000), 5_800);
626    }
627
628    #[test]
629    fn drains_backlog_partially_uses_sstore_set() {
630        assert_eq!(
631            legacy_actual_backlog_cost(100_000, 99_000),
632            SLOAD_GAS + SSTORE_GAS,
633        );
634        assert_eq!(legacy_actual_backlog_cost(100_000, 99_000), 20_800);
635    }
636
637    #[test]
638    fn donate_exceeds_backlog_saturates_to_zero() {
639        assert_eq!(
640            legacy_actual_backlog_cost(50_000, 200_000),
641            SLOAD_GAS + SSTORE_RESET_GAS,
642        );
643    }
644
645    #[test]
646    fn empty_backlog_zero_donate_still_writes_zero() {
647        assert_eq!(
648            legacy_actual_backlog_cost(0, 0),
649            SLOAD_GAS + SSTORE_RESET_GAS,
650        );
651    }
652
653    #[test]
654    fn full_drain_uses_reset_cost_15000_below_static_set() {
655        let static_set_cost = SLOAD_GAS + SSTORE_GAS;
656        let drain_cost = legacy_actual_backlog_cost(100_000, 100_000);
657        assert_eq!(static_set_cost - drain_cost, 15_000);
658    }
659
660    #[test]
661    fn constraint_with_remaining_backlog_charges_sstore_set() {
662        assert_eq!(
663            constraint_actual_backlog_cost(10_000_000, 1_100_000),
664            SLOAD_GAS + SSTORE_GAS,
665        );
666    }
667
668    #[test]
669    fn constraint_draining_to_zero_charges_sstore_reset() {
670        assert_eq!(
671            constraint_actual_backlog_cost(100_000, 100_000),
672            SLOAD_GAS + SSTORE_RESET_GAS,
673        );
674    }
675
676    #[test]
677    fn six_non_draining_constraints_match_reservation() {
678        let len = 6u64;
679        let gas_to_donate = 1_100_000u64;
680        let per_constraint_backlog = 10_000_000u64;
681        let reservation = 2 * SLOAD_GAS + len * (SLOAD_GAS + SSTORE_GAS);
682        let actual = 2 * SLOAD_GAS
683            + (0..len)
684                .map(|_| constraint_actual_backlog_cost(per_constraint_backlog, gas_to_donate))
685                .sum::<u64>();
686        assert_eq!(actual, reservation);
687    }
688
689    #[test]
690    fn non_draining_constraints_redeem_recovers_full_gas_limit() {
691        let gas_limit = 1_200_000u64;
692        let len = 6u64;
693        let backlog_per_constraint = 10_000_000u64;
694        let gas_used_so_far = 50_000u64;
695        let reservation = 2 * SLOAD_GAS + len * (SLOAD_GAS + SSTORE_GAS);
696        let future = REDEEM_SCHEDULED_EVENT_COST + COPY_GAS + reservation;
697        let gas_to_donate = gas_limit - gas_used_so_far - future;
698
699        let reset_actual = 2 * SLOAD_GAS + len * (SLOAD_GAS + SSTORE_RESET_GAS);
700        let reset_total =
701            gas_used_so_far + REDEEM_SCHEDULED_EVENT_COST + gas_to_donate + reset_actual + COPY_GAS;
702        assert_eq!(gas_limit - reset_total, 90_000);
703
704        let actual = 2 * SLOAD_GAS
705            + (0..len)
706                .map(|_| constraint_actual_backlog_cost(backlog_per_constraint, gas_to_donate))
707                .sum::<u64>();
708        let total =
709            gas_used_so_far + REDEEM_SCHEDULED_EVENT_COST + gas_to_donate + actual + COPY_GAS;
710        assert_eq!(total, gas_limit);
711    }
712}