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