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
15pub 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
31const REDEEM_SCHEDULED_DATA_BYTES: u64 = 128;
33
34const 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
143fn 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
153fn 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 crate::charge_computation(gas_used, ctx, gas_to_donate);
365 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 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 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}