arbos/retryables/
mod.rs

1use alloy_primitives::{keccak256, Address, B256, U256};
2use revm::Database;
3
4use arb_storage::{
5    initialize_queue, open_queue, Queue, Storage, StorageBackedAddress, StorageBackedAddressOrNil,
6    StorageBackedBigUint, StorageBackedBytes, StorageBackedUint64,
7};
8
9pub const RETRYABLE_LIFETIME_SECONDS: u64 = 7 * 24 * 60 * 60; // one week
10pub const RETRYABLE_REAP_PRICE: u64 = 58000;
11
12const TIMEOUT_QUEUE_KEY: &[u8] = &[0];
13const CALLDATA_KEY: &[u8] = &[1];
14
15// Storage offsets for Retryable fields.
16const NUM_TRIES_OFFSET: u64 = 0;
17const FROM_OFFSET: u64 = 1;
18const TO_OFFSET: u64 = 2;
19const CALLVALUE_OFFSET: u64 = 3;
20const BENEFICIARY_OFFSET: u64 = 4;
21const TIMEOUT_OFFSET: u64 = 5;
22const TIMEOUT_WINDOWS_LEFT_OFFSET: u64 = 6;
23
24/// Manages the collection of retryable tickets.
25pub struct RetryableState<D> {
26    retryables: Storage<D>,
27    pub timeout_queue: Queue<D>,
28}
29
30/// A single retryable ticket.
31pub struct Retryable<D> {
32    pub id: B256,
33    #[allow(dead_code)]
34    backing_storage: Storage<D>,
35    num_tries: StorageBackedUint64<D>,
36    from: StorageBackedAddress<D>,
37    to: StorageBackedAddressOrNil<D>,
38    callvalue: StorageBackedBigUint<D>,
39    beneficiary: StorageBackedAddress<D>,
40    calldata: StorageBackedBytes<D>,
41    timeout: StorageBackedUint64<D>,
42    timeout_windows_left: StorageBackedUint64<D>,
43}
44
45pub fn initialize_retryable_state<D: Database>(sto: &Storage<D>) -> Result<(), ()> {
46    initialize_queue(&sto.open_sub_storage(TIMEOUT_QUEUE_KEY))
47}
48
49pub fn open_retryable_state<D: Database>(sto: Storage<D>) -> RetryableState<D> {
50    let queue_sto = sto.open_sub_storage(TIMEOUT_QUEUE_KEY);
51    RetryableState {
52        timeout_queue: open_queue(queue_sto),
53        retryables: sto,
54    }
55}
56
57impl<D: Database> RetryableState<D> {
58    pub fn initialize(sto: &Storage<D>) -> Result<(), ()> {
59        initialize_retryable_state(sto)
60    }
61
62    pub fn open(sto: Storage<D>) -> Self {
63        open_retryable_state(sto)
64    }
65
66    /// Creates a new retryable ticket. The id must be unique.
67    pub fn create_retryable(
68        &self,
69        id: B256,
70        timeout: u64,
71        from: Address,
72        to: Option<Address>,
73        callvalue: U256,
74        beneficiary: Address,
75        calldata: &[u8],
76    ) -> Result<Retryable<D>, ()> {
77        let ret = self.internal_open(id);
78        ret.num_tries.set(0)?;
79        ret.from.set(from)?;
80        ret.to.set(to)?;
81        ret.callvalue.set(callvalue)?;
82        ret.beneficiary.set(beneficiary)?;
83        ret.calldata.set(calldata)?;
84        ret.timeout.set(timeout)?;
85        ret.timeout_windows_left.set(0)?;
86        self.timeout_queue.put(id)?;
87        Ok(ret)
88    }
89
90    /// Opens an existing retryable if it exists and hasn't expired.
91    pub fn open_retryable(
92        &self,
93        id: B256,
94        current_timestamp: u64,
95    ) -> Result<Option<Retryable<D>>, ()> {
96        let sto = self.retryables.open_sub_storage(id.as_slice());
97        let timeout_storage =
98            StorageBackedUint64::new(sto.state_ptr(), sto.base_key(), TIMEOUT_OFFSET);
99        let timeout = timeout_storage.get()?;
100        if timeout == 0 || timeout < current_timestamp {
101            return Ok(None);
102        }
103        Ok(Some(self.internal_open(id)))
104    }
105
106    /// Gets the size in bytes a retryable occupies in storage.
107    pub fn retryable_size_bytes(&self, id: B256, current_time: u64) -> Result<u64, ()> {
108        let retryable = self.open_retryable(id, current_time)?;
109        match retryable {
110            None => Ok(0),
111            Some(ret) => {
112                let size = ret.calldata_size()?;
113                let calldata_slots = 32 + 32 * words_for_bytes(size);
114                Ok(6 * 32 + calldata_slots)
115            }
116        }
117    }
118
119    /// Deletes a retryable and returns whether it existed.
120    /// Moves the escrow's entire balance to the beneficiary via the provided closures.
121    pub fn delete_retryable<F, G>(
122        &self,
123        id: B256,
124        mut transfer_fn: F,
125        mut balance_of: G,
126    ) -> Result<bool, ()>
127    where
128        F: FnMut(Address, Address, U256) -> Result<(), ()>,
129        G: FnMut(Address) -> U256,
130    {
131        let ret_storage = self.retryables.open_sub_storage(id.as_slice());
132        let timeout_val = ret_storage.get_by_uint64(TIMEOUT_OFFSET)?;
133        if timeout_val == B256::ZERO {
134            return Ok(false);
135        }
136
137        // Move escrowed funds to beneficiary.
138        let beneficiary_val = ret_storage.get_by_uint64(BENEFICIARY_OFFSET)?;
139        let escrow_address = retryable_escrow_address(id);
140        let beneficiary_address = Address::from_slice(&beneficiary_val[12..]);
141        let amount = balance_of(escrow_address);
142        transfer_fn(escrow_address, beneficiary_address, amount)?;
143
144        // Clear all storage slots.
145        let _ = ret_storage.set_by_uint64(NUM_TRIES_OFFSET, B256::ZERO);
146        let _ = ret_storage.set_by_uint64(FROM_OFFSET, B256::ZERO);
147        let _ = ret_storage.set_by_uint64(TO_OFFSET, B256::ZERO);
148        let _ = ret_storage.set_by_uint64(CALLVALUE_OFFSET, B256::ZERO);
149        let _ = ret_storage.set_by_uint64(BENEFICIARY_OFFSET, B256::ZERO);
150        let _ = ret_storage.set_by_uint64(TIMEOUT_OFFSET, B256::ZERO);
151        let _ = ret_storage.set_by_uint64(TIMEOUT_WINDOWS_LEFT_OFFSET, B256::ZERO);
152        let bytes_storage = StorageBackedBytes::new(ret_storage.open_sub_storage(CALLDATA_KEY));
153        bytes_storage.clear()?;
154        Ok(true)
155    }
156
157    /// Extends the lifetime of a retryable ticket.
158    pub fn keepalive(
159        &self,
160        ticket_id: B256,
161        current_timestamp: u64,
162        limit_before_add: u64,
163        _time_to_add: u64,
164    ) -> Result<u64, ()> {
165        let retryable = self.open_retryable(ticket_id, current_timestamp)?;
166        let retryable = retryable.ok_or(())?;
167        let timeout = retryable.calculate_timeout()?;
168        if timeout > limit_before_add {
169            return Err(());
170        }
171        self.timeout_queue.put(retryable.id)?;
172        retryable.increment_timeout_windows()?;
173        let new_timeout = timeout + RETRYABLE_LIFETIME_SECONDS;
174        // In Go, this also burns RetryableReapPrice gas.
175        Ok(new_timeout)
176    }
177
178    /// Tries to reap one expired retryable from the timeout queue.
179    pub fn try_to_reap_one_retryable<F, G>(
180        &self,
181        current_timestamp: u64,
182        mut transfer_fn: F,
183        mut balance_of: G,
184    ) -> Result<(), ()>
185    where
186        F: FnMut(Address, Address, U256) -> Result<(), ()>,
187        G: FnMut(Address) -> U256,
188    {
189        let id = self.timeout_queue.peek()?;
190        let id = match id {
191            None => return Ok(()),
192            Some(id) => id,
193        };
194
195        let ret_storage = self.retryables.open_sub_storage(id.as_slice());
196        let timeout_storage = StorageBackedUint64::new(
197            ret_storage.state_ptr(),
198            ret_storage.base_key(),
199            TIMEOUT_OFFSET,
200        );
201        let timeout = timeout_storage.get()?;
202
203        if timeout == 0 {
204            // Already deleted, discard queue entry.
205            let _ = self.timeout_queue.get()?;
206            return Ok(());
207        }
208
209        let windows_left_storage = StorageBackedUint64::new(
210            ret_storage.state_ptr(),
211            ret_storage.base_key(),
212            TIMEOUT_WINDOWS_LEFT_OFFSET,
213        );
214        let windows_left = windows_left_storage.get()?;
215
216        if timeout >= current_timestamp {
217            return Ok(());
218        }
219
220        // Retryable has expired or lost a lifetime window.
221        let _ = self.timeout_queue.get()?;
222
223        if windows_left == 0 {
224            // Fully expired — delete it.
225            self.delete_retryable(id, &mut transfer_fn, &mut balance_of)?;
226            return Ok(());
227        }
228
229        // Consume a window, delaying timeout by one lifetime.
230        timeout_storage.set(timeout + RETRYABLE_LIFETIME_SECONDS)?;
231        windows_left_storage.set(windows_left - 1)?;
232        Ok(())
233    }
234
235    fn internal_open(&self, id: B256) -> Retryable<D> {
236        let sto = self.retryables.open_sub_storage(id.as_slice());
237        let state = sto.state_ptr();
238        let base_key = sto.base_key();
239        Retryable {
240            id,
241            num_tries: StorageBackedUint64::new(state, base_key, NUM_TRIES_OFFSET),
242            from: StorageBackedAddress::new(state, base_key, FROM_OFFSET),
243            to: StorageBackedAddressOrNil::new(state, base_key, TO_OFFSET),
244            callvalue: StorageBackedBigUint::new(state, base_key, CALLVALUE_OFFSET),
245            beneficiary: StorageBackedAddress::new(state, base_key, BENEFICIARY_OFFSET),
246            calldata: StorageBackedBytes::new(sto.open_sub_storage(CALLDATA_KEY)),
247            timeout: StorageBackedUint64::new(state, base_key, TIMEOUT_OFFSET),
248            timeout_windows_left: StorageBackedUint64::new(
249                state,
250                base_key,
251                TIMEOUT_WINDOWS_LEFT_OFFSET,
252            ),
253            backing_storage: sto,
254        }
255    }
256}
257
258impl<D: Database> Retryable<D> {
259    pub fn num_tries(&self) -> Result<u64, ()> {
260        self.num_tries.get()
261    }
262
263    pub fn increment_num_tries(&self) -> Result<u64, ()> {
264        let current = self.num_tries.get()?;
265        let new_val = current + 1;
266        self.num_tries.set(new_val)?;
267        Ok(new_val)
268    }
269
270    pub fn beneficiary(&self) -> Result<Address, ()> {
271        self.beneficiary.get()
272    }
273
274    pub fn calculate_timeout(&self) -> Result<u64, ()> {
275        let timeout = self.timeout.get()?;
276        let windows = self.timeout_windows_left.get()?;
277        Ok(timeout + windows * RETRYABLE_LIFETIME_SECONDS)
278    }
279
280    pub fn set_timeout(&self, val: u64) -> Result<(), ()> {
281        self.timeout.set(val)
282    }
283
284    pub fn timeout_windows_left(&self) -> Result<u64, ()> {
285        self.timeout_windows_left.get()
286    }
287
288    fn increment_timeout_windows(&self) -> Result<u64, ()> {
289        let current = self.timeout_windows_left.get()?;
290        let new_val = current + 1;
291        self.timeout_windows_left.set(new_val)?;
292        Ok(new_val)
293    }
294
295    pub fn from(&self) -> Result<Address, ()> {
296        self.from.get()
297    }
298
299    pub fn to(&self) -> Result<Option<Address>, ()> {
300        self.to.get()
301    }
302
303    pub fn callvalue(&self) -> Result<U256, ()> {
304        self.callvalue.get()
305    }
306
307    pub fn calldata(&self) -> Result<Vec<u8>, ()> {
308        self.calldata.get()
309    }
310
311    pub fn calldata_size(&self) -> Result<u64, ()> {
312        self.calldata.size()
313    }
314
315    /// Constructs a retry transaction from this retryable's stored fields
316    /// combined with the provided runtime parameters.
317    pub fn make_tx(
318        &self,
319        chain_id: U256,
320        nonce: u64,
321        gas_fee_cap: U256,
322        gas: u64,
323        ticket_id: B256,
324        refund_to: Address,
325        max_refund: U256,
326        submission_fee_refund: U256,
327    ) -> Result<arb_alloy_consensus::tx::ArbRetryTx, ()> {
328        Ok(arb_alloy_consensus::tx::ArbRetryTx {
329            chain_id,
330            nonce,
331            from: self.from()?,
332            gas_fee_cap,
333            gas,
334            to: self.to()?,
335            value: self.callvalue()?,
336            data: self.calldata()?.into(),
337            ticket_id,
338            refund_to,
339            max_refund,
340            submission_fee_refund,
341        })
342    }
343}
344
345/// Computes the escrow address for a retryable ticket.
346pub fn retryable_escrow_address(ticket_id: B256) -> Address {
347    let mut data = Vec::with_capacity(16 + 32);
348    data.extend_from_slice(b"retryable escrow");
349    data.extend_from_slice(ticket_id.as_slice());
350    let hash = keccak256(&data);
351    Address::from_slice(&hash[12..])
352}
353
354/// Computes the submission fee for a retryable ticket.
355pub fn retryable_submission_fee(calldata_length: usize, l1_base_fee: U256) -> U256 {
356    l1_base_fee * U256::from(1400 + 6 * calldata_length as u64)
357}
358
359/// Rounds up byte count to number of 32-byte words.
360fn words_for_bytes(bytes: u64) -> u64 {
361    bytes.div_ceil(32)
362}