1pub mod dispatch;
7
8pub use tempo_contracts::precompiles::INonce;
9use tempo_contracts::precompiles::{NonceError, NonceEvent};
10use tempo_precompiles_macros::contract;
11
12use crate::{
13 NONCE_PRECOMPILE_ADDRESS,
14 error::Result,
15 storage::{Handler, Mapping},
16};
17use alloy::primitives::{Address, B256, U256};
18
19pub const EXPIRING_NONCE_SET_CAPACITY: u32 = 300_000;
21
22pub const EXPIRING_NONCE_MAX_EXPIRY_SECS: u64 = 30;
25
26#[contract(addr = NONCE_PRECOMPILE_ADDRESS)]
51pub struct NonceManager {
52 nonces: Mapping<Address, Mapping<U256, u64>>,
53 expiring_nonce_seen: Mapping<B256, u64>,
54 expiring_nonce_ring: Mapping<u32, B256>,
55 expiring_nonce_ring_ptr: u32,
56}
57
58impl NonceManager {
59 pub fn initialize(&mut self) -> Result<()> {
61 self.__initialize()
62 }
63
64 pub fn get_nonce(&self, call: INonce::getNonceCall) -> Result<u64> {
69 if call.nonceKey == 0 {
72 return Err(NonceError::protocol_nonce_not_supported().into());
73 }
74
75 self.nonces[call.account][call.nonceKey].read()
77 }
78
79 pub fn increment_nonce(&mut self, account: Address, nonce_key: U256) -> Result<u64> {
86 if nonce_key == 0 {
87 return Err(NonceError::invalid_nonce_key().into());
88 }
89
90 let current = self.nonces[account][nonce_key].read()?;
91
92 let new_nonce = current
93 .checked_add(1)
94 .ok_or_else(NonceError::nonce_overflow)?;
95
96 self.nonces[account][nonce_key].write(new_nonce)?;
97
98 self.emit_event(NonceEvent::nonce_incremented(account, nonce_key, new_nonce))?;
99
100 Ok(new_nonce)
101 }
102
103 pub fn is_expiring_nonce_seen(&self, hash: B256, now: u64) -> Result<bool> {
106 let expiry = self.expiring_nonce_seen[hash].read()?;
107 Ok(expiry != 0 && expiry > now)
108 }
109
110 pub fn check_and_mark_expiring_nonce(
131 &mut self,
132 expiring_nonce_hash: B256,
133 valid_before: u64,
134 ) -> Result<()> {
135 let now: u64 = self.storage.timestamp().saturating_to();
136
137 if valid_before <= now || valid_before > now.saturating_add(EXPIRING_NONCE_MAX_EXPIRY_SECS)
139 {
140 return Err(NonceError::invalid_expiring_nonce_expiry().into());
141 }
142
143 let seen_expiry = self.expiring_nonce_seen[expiring_nonce_hash].read()?;
145 if seen_expiry != 0 && seen_expiry > now {
146 return Err(NonceError::expiring_nonce_replay().into());
147 }
148
149 let ptr = self.expiring_nonce_ring_ptr.read()?;
151 let idx = ptr;
152 let old_hash = self.expiring_nonce_ring[idx].read()?;
153
154 if old_hash != B256::ZERO {
158 let old_expiry = self.expiring_nonce_seen[old_hash].read()?;
159 if old_expiry != 0 && old_expiry > now {
160 return Err(NonceError::expiring_nonce_set_full().into());
162 }
163 self.expiring_nonce_seen[old_hash].write(0)?;
165 }
166
167 self.expiring_nonce_ring[idx].write(expiring_nonce_hash)?;
169 self.expiring_nonce_seen[expiring_nonce_hash].write(valid_before)?;
170
171 let next = if ptr + 1 >= EXPIRING_NONCE_SET_CAPACITY {
173 0
174 } else {
175 ptr + 1
176 };
177 self.expiring_nonce_ring_ptr.write(next)?;
178
179 Ok(())
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use crate::{
186 error::TempoPrecompileError,
187 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
188 };
189
190 use super::*;
191 use alloy::primitives::address;
192
193 #[test]
194 fn test_get_nonce_returns_zero_for_new_key() -> eyre::Result<()> {
195 let mut storage = HashMapStorageProvider::new(1);
196 StorageCtx::enter(&mut storage, || {
197 let mgr = NonceManager::new();
198
199 let account = address!("0x1111111111111111111111111111111111111111");
200 let nonce = mgr.get_nonce(INonce::getNonceCall {
201 account,
202 nonceKey: U256::from(5),
203 })?;
204
205 assert_eq!(nonce, 0);
206 Ok(())
207 })
208 }
209
210 #[test]
211 fn test_get_nonce_rejects_protocol_nonce() -> eyre::Result<()> {
212 let mut storage = HashMapStorageProvider::new(1);
213 StorageCtx::enter(&mut storage, || {
214 let mgr = NonceManager::new();
215
216 let account = address!("0x1111111111111111111111111111111111111111");
217 let result = mgr.get_nonce(INonce::getNonceCall {
218 account,
219 nonceKey: U256::ZERO,
220 });
221
222 assert_eq!(
223 result.unwrap_err(),
224 TempoPrecompileError::NonceError(NonceError::protocol_nonce_not_supported())
225 );
226 Ok(())
227 })
228 }
229
230 #[test]
231 fn test_increment_nonce() -> eyre::Result<()> {
232 let mut storage = HashMapStorageProvider::new(1);
233 StorageCtx::enter(&mut storage, || {
234 let mut mgr = NonceManager::new();
235
236 let account = address!("0x1111111111111111111111111111111111111111");
237 let nonce_key = U256::from(5);
238
239 let new_nonce = mgr.increment_nonce(account, nonce_key)?;
240 assert_eq!(new_nonce, 1);
241 assert_eq!(mgr.emitted_events().len(), 1);
242
243 let new_nonce = mgr.increment_nonce(account, nonce_key)?;
244 assert_eq!(new_nonce, 2);
245 mgr.assert_emitted_events(vec![
246 INonce::NonceIncremented {
247 account,
248 nonceKey: nonce_key,
249 newNonce: 1,
250 },
251 INonce::NonceIncremented {
252 account,
253 nonceKey: nonce_key,
254 newNonce: 2,
255 },
256 ]);
257
258 Ok(())
259 })
260 }
261
262 #[test]
263 fn test_different_accounts_independent() -> eyre::Result<()> {
264 let mut storage = HashMapStorageProvider::new(1);
265 StorageCtx::enter(&mut storage, || {
266 let mut mgr = NonceManager::new();
267
268 let account1 = address!("0x1111111111111111111111111111111111111111");
269 let account2 = address!("0x2222222222222222222222222222222222222222");
270 let nonce_key = U256::from(5);
271
272 for _ in 0..10 {
273 mgr.increment_nonce(account1, nonce_key)?;
274 }
275 for _ in 0..20 {
276 mgr.increment_nonce(account2, nonce_key)?;
277 }
278
279 let nonce1 = mgr.get_nonce(INonce::getNonceCall {
280 account: account1,
281 nonceKey: nonce_key,
282 })?;
283 let nonce2 = mgr.get_nonce(INonce::getNonceCall {
284 account: account2,
285 nonceKey: nonce_key,
286 })?;
287
288 assert_eq!(nonce1, 10);
289 assert_eq!(nonce2, 20);
290 Ok(())
291 })
292 }
293
294 #[test]
297 fn test_expiring_nonce_basic_flow() -> eyre::Result<()> {
298 let mut storage = HashMapStorageProvider::new(1);
299 let now = 1000u64;
300 storage.set_timestamp(U256::from(now));
301 StorageCtx::enter(&mut storage, || {
302 let mut mgr = NonceManager::new();
303
304 let tx_hash = B256::repeat_byte(0x11);
305 let valid_before = now + 20; mgr.check_and_mark_expiring_nonce(tx_hash, valid_before)?;
309
310 let result = mgr.check_and_mark_expiring_nonce(tx_hash, valid_before);
312 assert_eq!(
313 result.unwrap_err(),
314 TempoPrecompileError::NonceError(NonceError::expiring_nonce_replay())
315 );
316
317 Ok(())
318 })
319 }
320
321 #[test]
322 fn test_expiring_nonce_expiry_validation() -> eyre::Result<()> {
323 let mut storage = HashMapStorageProvider::new(1);
324 let now = 1000u64;
325 storage.set_timestamp(U256::from(now));
326 StorageCtx::enter(&mut storage, || {
327 let mut mgr = NonceManager::new();
328
329 let tx_hash = B256::repeat_byte(0x22);
330
331 let result = mgr.check_and_mark_expiring_nonce(tx_hash, now - 1);
333 assert_eq!(
334 result.unwrap_err(),
335 TempoPrecompileError::NonceError(NonceError::invalid_expiring_nonce_expiry())
336 );
337
338 let result = mgr.check_and_mark_expiring_nonce(tx_hash, now);
340 assert_eq!(
341 result.unwrap_err(),
342 TempoPrecompileError::NonceError(NonceError::invalid_expiring_nonce_expiry())
343 );
344
345 let result = mgr.check_and_mark_expiring_nonce(tx_hash, now + 31);
347 assert_eq!(
348 result.unwrap_err(),
349 TempoPrecompileError::NonceError(NonceError::invalid_expiring_nonce_expiry())
350 );
351
352 mgr.check_and_mark_expiring_nonce(tx_hash, now + 30)?;
354
355 Ok(())
356 })
357 }
358
359 #[test]
360 fn test_expiring_nonce_expired_entry_eviction() -> eyre::Result<()> {
361 let mut storage = HashMapStorageProvider::new(1);
362 let now = 1000u64;
363 let valid_before = now + 20;
364 storage.set_timestamp(U256::from(now));
365 StorageCtx::enter(&mut storage, || {
366 let mut mgr = NonceManager::new();
367
368 let tx_hash1 = B256::repeat_byte(0x33);
369
370 mgr.check_and_mark_expiring_nonce(tx_hash1, valid_before)?;
372
373 assert!(mgr.is_expiring_nonce_seen(tx_hash1, now)?);
375
376 assert!(!mgr.is_expiring_nonce_seen(tx_hash1, valid_before + 1)?);
378
379 Ok::<_, eyre::Report>(())
380 })?;
381
382 let new_now = valid_before + 1;
384 let new_valid_before = new_now + 20;
385 storage.set_timestamp(U256::from(new_now));
386 StorageCtx::enter(&mut storage, || {
387 let mut mgr = NonceManager::new();
388
389 let tx_hash2 = B256::repeat_byte(0x44);
390 mgr.check_and_mark_expiring_nonce(tx_hash2, new_valid_before)?;
391
392 assert!(mgr.is_expiring_nonce_seen(tx_hash2, new_now)?);
395
396 Ok(())
397 })
398 }
399
400 #[test]
401 fn test_ring_buffer_pointer_wraps_at_capacity() -> eyre::Result<()> {
402 let mut storage = HashMapStorageProvider::new(1);
403 let now = 1000u64;
404 storage.set_timestamp(U256::from(now));
405 StorageCtx::enter(&mut storage, || {
406 let mut mgr = NonceManager::new();
407
408 mgr.expiring_nonce_ring_ptr
410 .write(EXPIRING_NONCE_SET_CAPACITY - 1)?;
411
412 let tx_hash = B256::repeat_byte(0x77);
414 let valid_before = now + 20;
415 mgr.check_and_mark_expiring_nonce(tx_hash, valid_before)?;
416
417 let ptr = mgr.expiring_nonce_ring_ptr.read()?;
419 assert_eq!(ptr, 0, "Pointer should wrap to 0 at capacity");
420
421 let tx_hash2 = B256::repeat_byte(0x88);
423 mgr.check_and_mark_expiring_nonce(tx_hash2, valid_before)?;
424
425 let ptr = mgr.expiring_nonce_ring_ptr.read()?;
426 assert_eq!(ptr, 1, "Pointer should increment to 1 after wrap");
427
428 Ok(())
429 })
430 }
431
432 #[test]
433 fn test_initialize_sets_storage_state() -> eyre::Result<()> {
434 let mut storage = HashMapStorageProvider::new(1);
435 StorageCtx::enter(&mut storage, || {
436 let mut mgr = NonceManager::new();
437
438 assert!(!mgr.is_initialized()?);
440
441 mgr.initialize()?;
443
444 assert!(mgr.is_initialized()?);
446
447 let mgr2 = NonceManager::new();
449 assert!(mgr2.is_initialized()?);
450
451 Ok(())
452 })
453 }
454}