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::NonceIncremented(INonce::NonceIncremented {
99 account,
100 nonceKey: nonce_key,
101 newNonce: new_nonce,
102 }))?;
103
104 Ok(new_nonce)
105 }
106
107 pub fn is_expiring_nonce_seen(&self, hash: B256, now: u64) -> Result<bool> {
110 let expiry = self.expiring_nonce_seen[hash].read()?;
111 Ok(expiry != 0 && expiry > now)
112 }
113
114 pub fn check_and_mark_expiring_nonce(
135 &mut self,
136 expiring_nonce_hash: B256,
137 valid_before: u64,
138 ) -> Result<()> {
139 let now: u64 = self.storage.timestamp().saturating_to();
140
141 if valid_before <= now || valid_before > now.saturating_add(EXPIRING_NONCE_MAX_EXPIRY_SECS)
143 {
144 return Err(NonceError::invalid_expiring_nonce_expiry().into());
145 }
146
147 let seen_expiry = self.expiring_nonce_seen[expiring_nonce_hash].read()?;
149 if seen_expiry != 0 && seen_expiry > now {
150 return Err(NonceError::expiring_nonce_replay().into());
151 }
152
153 let ptr = self.expiring_nonce_ring_ptr.read()?;
155 let idx = ptr;
156 let old_hash = self.expiring_nonce_ring[idx].read()?;
157
158 if old_hash != B256::ZERO {
162 let old_expiry = self.expiring_nonce_seen[old_hash].read()?;
163 if old_expiry != 0 && old_expiry > now {
164 return Err(NonceError::expiring_nonce_set_full().into());
166 }
167 self.expiring_nonce_seen[old_hash].write(0)?;
169 }
170
171 self.expiring_nonce_ring[idx].write(expiring_nonce_hash)?;
173 self.expiring_nonce_seen[expiring_nonce_hash].write(valid_before)?;
174
175 let next = if ptr + 1 >= EXPIRING_NONCE_SET_CAPACITY {
177 0
178 } else {
179 ptr + 1
180 };
181 self.expiring_nonce_ring_ptr.write(next)?;
182
183 Ok(())
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use crate::{
190 error::TempoPrecompileError,
191 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
192 };
193
194 use super::*;
195 use alloy::primitives::address;
196
197 #[test]
198 fn test_get_nonce_returns_zero_for_new_key() -> eyre::Result<()> {
199 let mut storage = HashMapStorageProvider::new(1);
200 StorageCtx::enter(&mut storage, || {
201 let mgr = NonceManager::new();
202
203 let account = address!("0x1111111111111111111111111111111111111111");
204 let nonce = mgr.get_nonce(INonce::getNonceCall {
205 account,
206 nonceKey: U256::from(5),
207 })?;
208
209 assert_eq!(nonce, 0);
210 Ok(())
211 })
212 }
213
214 #[test]
215 fn test_get_nonce_rejects_protocol_nonce() -> eyre::Result<()> {
216 let mut storage = HashMapStorageProvider::new(1);
217 StorageCtx::enter(&mut storage, || {
218 let mgr = NonceManager::new();
219
220 let account = address!("0x1111111111111111111111111111111111111111");
221 let result = mgr.get_nonce(INonce::getNonceCall {
222 account,
223 nonceKey: U256::ZERO,
224 });
225
226 assert_eq!(
227 result.unwrap_err(),
228 TempoPrecompileError::NonceError(NonceError::protocol_nonce_not_supported())
229 );
230 Ok(())
231 })
232 }
233
234 #[test]
235 fn test_increment_nonce() -> eyre::Result<()> {
236 let mut storage = HashMapStorageProvider::new(1);
237 StorageCtx::enter(&mut storage, || {
238 let mut mgr = NonceManager::new();
239
240 let account = address!("0x1111111111111111111111111111111111111111");
241 let nonce_key = U256::from(5);
242
243 let new_nonce = mgr.increment_nonce(account, nonce_key)?;
244 assert_eq!(new_nonce, 1);
245 assert_eq!(mgr.emitted_events().len(), 1);
246
247 let new_nonce = mgr.increment_nonce(account, nonce_key)?;
248 assert_eq!(new_nonce, 2);
249 mgr.assert_emitted_events(vec![
250 INonce::NonceIncremented {
251 account,
252 nonceKey: nonce_key,
253 newNonce: 1,
254 },
255 INonce::NonceIncremented {
256 account,
257 nonceKey: nonce_key,
258 newNonce: 2,
259 },
260 ]);
261
262 Ok(())
263 })
264 }
265
266 #[test]
267 fn test_different_accounts_independent() -> eyre::Result<()> {
268 let mut storage = HashMapStorageProvider::new(1);
269 StorageCtx::enter(&mut storage, || {
270 let mut mgr = NonceManager::new();
271
272 let account1 = address!("0x1111111111111111111111111111111111111111");
273 let account2 = address!("0x2222222222222222222222222222222222222222");
274 let nonce_key = U256::from(5);
275
276 for _ in 0..10 {
277 mgr.increment_nonce(account1, nonce_key)?;
278 }
279 for _ in 0..20 {
280 mgr.increment_nonce(account2, nonce_key)?;
281 }
282
283 let nonce1 = mgr.get_nonce(INonce::getNonceCall {
284 account: account1,
285 nonceKey: nonce_key,
286 })?;
287 let nonce2 = mgr.get_nonce(INonce::getNonceCall {
288 account: account2,
289 nonceKey: nonce_key,
290 })?;
291
292 assert_eq!(nonce1, 10);
293 assert_eq!(nonce2, 20);
294 Ok(())
295 })
296 }
297
298 #[test]
301 fn test_expiring_nonce_basic_flow() -> eyre::Result<()> {
302 let mut storage = HashMapStorageProvider::new(1);
303 let now = 1000u64;
304 storage.set_timestamp(U256::from(now));
305 StorageCtx::enter(&mut storage, || {
306 let mut mgr = NonceManager::new();
307
308 let tx_hash = B256::repeat_byte(0x11);
309 let valid_before = now + 20; mgr.check_and_mark_expiring_nonce(tx_hash, valid_before)?;
313
314 let result = mgr.check_and_mark_expiring_nonce(tx_hash, valid_before);
316 assert_eq!(
317 result.unwrap_err(),
318 TempoPrecompileError::NonceError(NonceError::expiring_nonce_replay())
319 );
320
321 Ok(())
322 })
323 }
324
325 #[test]
326 fn test_expiring_nonce_expiry_validation() -> eyre::Result<()> {
327 let mut storage = HashMapStorageProvider::new(1);
328 let now = 1000u64;
329 storage.set_timestamp(U256::from(now));
330 StorageCtx::enter(&mut storage, || {
331 let mut mgr = NonceManager::new();
332
333 let tx_hash = B256::repeat_byte(0x22);
334
335 let result = mgr.check_and_mark_expiring_nonce(tx_hash, now - 1);
337 assert_eq!(
338 result.unwrap_err(),
339 TempoPrecompileError::NonceError(NonceError::invalid_expiring_nonce_expiry())
340 );
341
342 let result = mgr.check_and_mark_expiring_nonce(tx_hash, now);
344 assert_eq!(
345 result.unwrap_err(),
346 TempoPrecompileError::NonceError(NonceError::invalid_expiring_nonce_expiry())
347 );
348
349 let result = mgr.check_and_mark_expiring_nonce(tx_hash, now + 31);
351 assert_eq!(
352 result.unwrap_err(),
353 TempoPrecompileError::NonceError(NonceError::invalid_expiring_nonce_expiry())
354 );
355
356 mgr.check_and_mark_expiring_nonce(tx_hash, now + 30)?;
358
359 Ok(())
360 })
361 }
362
363 #[test]
364 fn test_expiring_nonce_expired_entry_eviction() -> eyre::Result<()> {
365 let mut storage = HashMapStorageProvider::new(1);
366 let now = 1000u64;
367 let valid_before = now + 20;
368 storage.set_timestamp(U256::from(now));
369 StorageCtx::enter(&mut storage, || {
370 let mut mgr = NonceManager::new();
371
372 let tx_hash1 = B256::repeat_byte(0x33);
373
374 mgr.check_and_mark_expiring_nonce(tx_hash1, valid_before)?;
376
377 assert!(mgr.is_expiring_nonce_seen(tx_hash1, now)?);
379
380 assert!(!mgr.is_expiring_nonce_seen(tx_hash1, valid_before + 1)?);
382
383 Ok::<_, eyre::Report>(())
384 })?;
385
386 let new_now = valid_before + 1;
388 let new_valid_before = new_now + 20;
389 storage.set_timestamp(U256::from(new_now));
390 StorageCtx::enter(&mut storage, || {
391 let mut mgr = NonceManager::new();
392
393 let tx_hash2 = B256::repeat_byte(0x44);
394 mgr.check_and_mark_expiring_nonce(tx_hash2, new_valid_before)?;
395
396 assert!(mgr.is_expiring_nonce_seen(tx_hash2, new_now)?);
399
400 Ok(())
401 })
402 }
403
404 #[test]
405 fn test_ring_buffer_pointer_wraps_at_capacity() -> eyre::Result<()> {
406 let mut storage = HashMapStorageProvider::new(1);
407 let now = 1000u64;
408 storage.set_timestamp(U256::from(now));
409 StorageCtx::enter(&mut storage, || {
410 let mut mgr = NonceManager::new();
411
412 mgr.expiring_nonce_ring_ptr
414 .write(EXPIRING_NONCE_SET_CAPACITY - 1)?;
415
416 let tx_hash = B256::repeat_byte(0x77);
418 let valid_before = now + 20;
419 mgr.check_and_mark_expiring_nonce(tx_hash, valid_before)?;
420
421 let ptr = mgr.expiring_nonce_ring_ptr.read()?;
423 assert_eq!(ptr, 0, "Pointer should wrap to 0 at capacity");
424
425 let tx_hash2 = B256::repeat_byte(0x88);
427 mgr.check_and_mark_expiring_nonce(tx_hash2, valid_before)?;
428
429 let ptr = mgr.expiring_nonce_ring_ptr.read()?;
430 assert_eq!(ptr, 1, "Pointer should increment to 1 after wrap");
431
432 Ok(())
433 })
434 }
435
436 #[test]
437 fn test_initialize_sets_storage_state() -> eyre::Result<()> {
438 let mut storage = HashMapStorageProvider::new(1);
439 StorageCtx::enter(&mut storage, || {
440 let mut mgr = NonceManager::new();
441
442 assert!(!mgr.is_initialized()?);
444
445 mgr.initialize()?;
447
448 assert!(mgr.is_initialized()?);
450
451 let mgr2 = NonceManager::new();
453 assert!(mgr2.is_initialized()?);
454
455 Ok(())
456 })
457 }
458}