Skip to main content

tempo_alloy/fillers/
nonce.rs

1use crate::rpc::TempoTransactionRequest;
2use alloy_network::{Network, TransactionBuilder};
3use alloy_primitives::{Address, U256};
4use alloy_provider::{
5    Provider, SendableTx,
6    fillers::{FillerControlFlow, TxFiller},
7};
8use alloy_transport::{TransportErrorKind, TransportResult};
9use core::num::NonZeroU64;
10use dashmap::DashMap;
11use std::{
12    sync::Arc,
13    time::{SystemTime, UNIX_EPOCH},
14};
15use tempo_contracts::precompiles::{INonce, NONCE_PRECOMPILE_ADDRESS};
16use tempo_primitives::{
17    subblock::has_sub_block_nonce_key_prefix, transaction::TEMPO_EXPIRING_NONCE_KEY,
18};
19
20/// A [`TxFiller`] that populates the [`TempoTransaction`](`tempo_primitives::TempoTransaction`) transaction with a random `nonce_key`, and `nonce` set to `0`.
21///
22/// This filler can be used to avoid nonce gaps by having a random 2D nonce key that doesn't conflict with any other transactions.
23#[derive(Clone, Copy, Debug, Default)]
24pub struct Random2DNonceFiller;
25
26impl Random2DNonceFiller {
27    /// Returns `true` if either the nonce or nonce key is already filled.
28    fn is_filled(tx: &TempoTransactionRequest) -> bool {
29        tx.nonce().is_some() || tx.nonce_key.is_some()
30    }
31}
32
33impl<N: Network<TransactionRequest = TempoTransactionRequest>> TxFiller<N> for Random2DNonceFiller {
34    type Fillable = ();
35
36    fn status(&self, tx: &N::TransactionRequest) -> FillerControlFlow {
37        if Self::is_filled(tx) {
38            return FillerControlFlow::Finished;
39        }
40        FillerControlFlow::Ready
41    }
42
43    fn fill_sync(&self, tx: &mut SendableTx<N>) {
44        if let Some(builder) = tx.as_mut_builder()
45            && !Self::is_filled(builder)
46        {
47            let nonce_key = loop {
48                let key = U256::random();
49                // We need to ensure that it doesn't use the subblock nonce key prefix
50                if !has_sub_block_nonce_key_prefix(&key) {
51                    break key;
52                }
53            };
54            builder.set_nonce_key(nonce_key);
55            builder.set_nonce(0);
56        }
57    }
58
59    async fn prepare<P>(
60        &self,
61        _provider: &P,
62        _tx: &N::TransactionRequest,
63    ) -> TransportResult<Self::Fillable>
64    where
65        P: alloy_provider::Provider<N>,
66    {
67        Ok(())
68    }
69
70    async fn fill(
71        &self,
72        _fillable: Self::Fillable,
73        tx: SendableTx<N>,
74    ) -> TransportResult<SendableTx<N>> {
75        Ok(tx)
76    }
77}
78
79/// A [`TxFiller`] that populates transactions with expiring nonce fields ([TIP-1009]).
80///
81/// Sets `nonce_key` to `U256::MAX`, `nonce` to `0`, and `valid_before` to current time + expiry window.
82/// This enables transactions to use the circular buffer replay protection instead of 2D nonce storage.
83///
84/// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
85#[derive(Clone, Copy, Debug)]
86pub struct ExpiringNonceFiller {
87    /// Expiry window in seconds from current time.
88    expiry_secs: u64,
89}
90
91impl Default for ExpiringNonceFiller {
92    fn default() -> Self {
93        Self {
94            expiry_secs: Self::DEFAULT_EXPIRY_SECS,
95        }
96    }
97}
98
99impl ExpiringNonceFiller {
100    /// Default expiry window in seconds (25s, within the 30s max allowed by [TIP-1009]).
101    ///
102    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
103    pub const DEFAULT_EXPIRY_SECS: u64 = 25;
104
105    /// Create a new filler with a custom expiry window.
106    ///
107    /// For benchmarking purposes, use a large value (e.g., 3600 for 1 hour) to avoid
108    /// transactions expiring before they're sent.
109    pub fn with_expiry_secs(expiry_secs: u64) -> Self {
110        Self { expiry_secs }
111    }
112
113    /// Returns `true` if all expiring nonce fields are properly set:
114    /// - `nonce_key` is `TEMPO_EXPIRING_NONCE_KEY`
115    /// - `nonce` is `0`
116    /// - `valid_before` is set
117    fn is_filled(tx: &TempoTransactionRequest) -> bool {
118        tx.nonce_key == Some(TEMPO_EXPIRING_NONCE_KEY)
119            && tx.nonce() == Some(0)
120            && tx.valid_before.is_some()
121    }
122
123    /// Returns the current unix timestamp, saturating to 0 if system time is before UNIX_EPOCH
124    /// (which can occur due to NTP adjustments or VM clock drift).
125    fn current_timestamp() -> u64 {
126        SystemTime::now()
127            .duration_since(UNIX_EPOCH)
128            .map(|d| d.as_secs())
129            .unwrap_or_else(|_| {
130                tracing::warn!("system clock before UNIX_EPOCH, using 0");
131                0
132            })
133    }
134}
135
136impl<N: Network<TransactionRequest = TempoTransactionRequest>> TxFiller<N> for ExpiringNonceFiller {
137    type Fillable = ();
138
139    fn status(&self, tx: &N::TransactionRequest) -> FillerControlFlow {
140        if Self::is_filled(tx) {
141            return FillerControlFlow::Finished;
142        }
143        FillerControlFlow::Ready
144    }
145
146    fn fill_sync(&self, tx: &mut SendableTx<N>) {
147        if let Some(builder) = tx.as_mut_builder()
148            && !Self::is_filled(builder)
149        {
150            // Set expiring nonce key (U256::MAX)
151            builder.set_nonce_key(TEMPO_EXPIRING_NONCE_KEY);
152            // Nonce must be 0 for expiring nonce transactions
153            builder.set_nonce(0);
154            // Set valid_before to current time + expiry window
155            builder.set_valid_before(
156                NonZeroU64::new(Self::current_timestamp() + self.expiry_secs)
157                    .expect("expiring nonce filler requires a non-zero valid_before"),
158            );
159        }
160    }
161
162    async fn prepare<P>(
163        &self,
164        _provider: &P,
165        _tx: &N::TransactionRequest,
166    ) -> TransportResult<Self::Fillable>
167    where
168        P: alloy_provider::Provider<N>,
169    {
170        Ok(())
171    }
172
173    async fn fill(
174        &self,
175        _fillable: Self::Fillable,
176        tx: SendableTx<N>,
177    ) -> TransportResult<SendableTx<N>> {
178        Ok(tx)
179    }
180}
181
182/// A [`TxFiller`] that fills the nonce for transactions with a pre-set `nonce_key`.
183///
184/// This filler requires `nonce_key` to already be set on the transaction request and fills
185/// the correct next nonce by querying the chain. By default, nonces are cached per
186/// `(address, nonce_key)` pair so that batched sends get sequential nonces without extra RPC
187/// calls. Caching can be disabled to force every fill to refetch from the chain.
188///
189/// Nonce resolution depends on the key:
190/// - `U256::ZERO` (protocol nonce): uses `get_transaction_count`
191/// - `TEMPO_EXPIRING_NONCE_KEY` (U256::MAX): always 0, no caching (use [`ExpiringNonceFiller`]
192///   instead for full expiring nonce support including `valid_before`)
193/// - Any other key: queries the `NonceManager` precompile via `eth_call`
194#[derive(Clone, Debug)]
195pub struct NonceKeyFiller {
196    cache_enabled: bool,
197    #[allow(clippy::type_complexity)]
198    nonces: Arc<DashMap<(Address, U256), Arc<futures::lock::Mutex<u64>>>>,
199}
200
201/// Sentinel value indicating the nonce has not been fetched yet.
202const NONCE_NOT_FETCHED: u64 = u64::MAX;
203
204impl Default for NonceKeyFiller {
205    fn default() -> Self {
206        Self {
207            cache_enabled: true,
208            nonces: Arc::default(),
209        }
210    }
211}
212
213impl NonceKeyFiller {
214    /// Enables or disables nonce caching.
215    pub const fn with_caching_enabled(mut self, cache_enabled: bool) -> Self {
216        self.cache_enabled = cache_enabled;
217        self
218    }
219
220    /// Enables or disables nonce caching.
221    pub fn set_caching_enabled(&mut self, cache_enabled: bool) {
222        self.cache_enabled = cache_enabled;
223    }
224
225    /// Disables nonce caching.
226    pub fn disable_caching(&mut self) {
227        self.set_caching_enabled(false);
228    }
229
230    /// Clears every tracked `(address, nonce_key)` pair.
231    ///
232    /// Future fills will refetch nonces from the chain.
233    pub fn clear(&self) {
234        self.nonces.clear();
235    }
236}
237
238impl<N: Network<TransactionRequest = TempoTransactionRequest>> TxFiller<N> for NonceKeyFiller {
239    type Fillable = u64;
240
241    fn status(&self, tx: &N::TransactionRequest) -> FillerControlFlow {
242        if tx.nonce().is_some() {
243            return FillerControlFlow::Finished;
244        }
245        if tx.nonce_key.is_none() {
246            return FillerControlFlow::missing("NonceKeyFiller", vec!["nonce_key"]);
247        }
248        if TransactionBuilder::from(tx).is_none() {
249            return FillerControlFlow::missing("NonceKeyFiller", vec!["from"]);
250        }
251        FillerControlFlow::Ready
252    }
253
254    fn fill_sync(&self, _tx: &mut SendableTx<N>) {}
255
256    async fn prepare<P>(
257        &self,
258        provider: &P,
259        tx: &N::TransactionRequest,
260    ) -> TransportResult<Self::Fillable>
261    where
262        P: Provider<N>,
263    {
264        let from = TransactionBuilder::from(tx)
265            .ok_or_else(|| TransportErrorKind::custom_str("missing `from` address"))?;
266        let nonce_key = tx
267            .nonce_key
268            .ok_or_else(|| TransportErrorKind::custom_str("missing `nonce_key`"))?;
269
270        // Expiring nonces always use nonce 0
271        if nonce_key == TEMPO_EXPIRING_NONCE_KEY {
272            return Ok(0);
273        }
274
275        let key = (from, nonce_key);
276        let mutex = self
277            .nonces
278            .entry(key)
279            .or_insert_with(|| Arc::new(futures::lock::Mutex::new(NONCE_NOT_FETCHED)))
280            .clone();
281
282        let mut nonce = mutex.lock().await;
283
284        if *nonce == NONCE_NOT_FETCHED || !self.cache_enabled {
285            *nonce = if nonce_key.is_zero() {
286                provider.get_transaction_count(from).await?
287            } else {
288                let contract = INonce::new(NONCE_PRECOMPILE_ADDRESS, provider);
289                contract
290                    .getNonce(from, nonce_key)
291                    .call()
292                    .await
293                    .map_err(|e| TransportErrorKind::custom_str(&e.to_string()))?
294            };
295        } else {
296            *nonce += 1;
297        }
298
299        Ok(*nonce)
300    }
301
302    async fn fill(
303        &self,
304        fillable: Self::Fillable,
305        mut tx: SendableTx<N>,
306    ) -> TransportResult<SendableTx<N>> {
307        if let Some(builder) = tx.as_mut_builder() {
308            builder.set_nonce(fillable);
309        }
310        Ok(tx)
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::{TempoNetwork, fillers::Random2DNonceFiller, rpc::TempoTransactionRequest};
318    use alloy::sol_types::SolCall;
319    use alloy_network::TransactionBuilder;
320    use alloy_primitives::{Bytes, ruint::aliases::U256};
321    use alloy_provider::{ProviderBuilder, mock::Asserter};
322    use eyre;
323
324    #[tokio::test]
325    async fn test_random_2d_nonce_filler() -> eyre::Result<()> {
326        let provider = ProviderBuilder::<_, _, TempoNetwork>::default()
327            .filler(Random2DNonceFiller)
328            .connect_mocked_client(Asserter::default());
329
330        // No nonce key, no nonce => nonce key and nonce are filled
331        let filled_request = provider
332            .fill(TempoTransactionRequest::default())
333            .await?
334            .try_into_request()?;
335        assert!(filled_request.nonce_key.is_some());
336        assert_eq!(filled_request.nonce(), Some(0));
337
338        // Has nonce => nothing is filled
339        let filled_request = provider
340            .fill(TempoTransactionRequest::default().with_nonce(1))
341            .await?
342            .try_into_request()?;
343        assert!(filled_request.nonce_key.is_none());
344        assert_eq!(filled_request.nonce(), Some(1));
345
346        // Has nonce key => nothing is filled
347        let filled_request = provider
348            .fill(TempoTransactionRequest::default().with_nonce_key(U256::ONE))
349            .await?
350            .try_into_request()?;
351        assert_eq!(filled_request.nonce_key, Some(U256::ONE));
352        assert!(filled_request.nonce().is_none());
353
354        Ok(())
355    }
356
357    #[tokio::test]
358    async fn test_nonce_key_filler_clear_refetches_chain_nonce() -> eyre::Result<()> {
359        let asserter = Asserter::new();
360        let provider = ProviderBuilder::<_, _, TempoNetwork>::default()
361            .connect_mocked_client(asserter.clone());
362        let filler = NonceKeyFiller::default();
363        let account = Address::repeat_byte(0x11);
364        let nonce_key = U256::from(7_u64);
365        let mut tx = TempoTransactionRequest::default().with_nonce_key(nonce_key);
366        tx.set_from(account);
367
368        asserter.push_success(&Bytes::from(INonce::getNonceCall::abi_encode_returns(
369            &10_u64,
370        )));
371
372        let first = TxFiller::<TempoNetwork>::prepare(&filler, &provider, &tx).await?;
373        let second = TxFiller::<TempoNetwork>::prepare(&filler, &provider, &tx).await?;
374
375        assert_eq!(first, 10);
376        assert_eq!(second, 11);
377
378        filler.clear();
379
380        asserter.push_success(&Bytes::from(INonce::getNonceCall::abi_encode_returns(
381            &42_u64,
382        )));
383
384        let reset = TxFiller::<TempoNetwork>::prepare(&filler, &provider, &tx).await?;
385
386        assert_eq!(reset, 42);
387
388        Ok(())
389    }
390
391    #[tokio::test]
392    async fn test_nonce_key_filler_can_disable_caching() -> eyre::Result<()> {
393        let asserter = Asserter::new();
394        let provider = ProviderBuilder::<_, _, TempoNetwork>::default()
395            .connect_mocked_client(asserter.clone());
396        let mut filler = NonceKeyFiller::default();
397        filler.disable_caching();
398        let account = Address::repeat_byte(0x22);
399        let nonce_key = U256::from(7_u64);
400        let mut tx = TempoTransactionRequest::default().with_nonce_key(nonce_key);
401        tx.set_from(account);
402
403        asserter.push_success(&Bytes::from(INonce::getNonceCall::abi_encode_returns(
404            &10_u64,
405        )));
406        asserter.push_success(&Bytes::from(INonce::getNonceCall::abi_encode_returns(
407            &42_u64,
408        )));
409
410        let first = TxFiller::<TempoNetwork>::prepare(&filler, &provider, &tx).await?;
411        let second = TxFiller::<TempoNetwork>::prepare(&filler, &provider, &tx).await?;
412
413        assert_eq!(first, 10);
414        assert_eq!(second, 42);
415
416        Ok(())
417    }
418}