1use std::sync::{
35 Arc, OnceLock,
36 atomic::{AtomicU64, Ordering},
37};
38
39use dashmap::DashMap;
40use thiserror::Error;
41
42use crate::signing::encoding::utc_now_ms;
43
44const NONCE_SUFFIX_BASE: u64 = 1_000;
45const NONCE_SUFFIX_MAX: u64 = NONCE_SUFFIX_BASE - 1;
46const NONCE_UNINITIALIZED: u64 = u64::MAX;
47
48#[derive(Debug, Error, PartialEq, Eq)]
50pub enum NonceError {
51 #[error("system clock is before UNIX epoch")]
53 ClockBeforeEpoch,
54 #[error("nonce suffix range exhausted for millisecond {millisecond}")]
56 SuffixExhausted { millisecond: u64 },
57 #[error("millisecond timestamp {milliseconds} overflows the nonce format")]
59 TimestampOverflow { milliseconds: u64 },
60 #[error("next nonce exceeds u64::MAX")]
62 NonceOverflow,
63}
64
65#[derive(Debug, Default)]
67pub struct NonceManager;
68
69impl NonceManager {
70 #[must_use]
72 pub fn new() -> Self {
73 Self
74 }
75
76 pub fn next_nonce(&self, wallet: &str, subaccount_id: u64) -> Result<u64, NonceError> {
84 let now_ms = utc_now_ms().map_err(|_| NonceError::ClockBeforeEpoch)?;
85 self.next_nonce_at(wallet, subaccount_id, now_ms)
86 }
87
88 pub fn next_nonce_at(
96 &self,
97 wallet: &str,
98 subaccount_id: u64,
99 now_ms: u64,
100 ) -> Result<u64, NonceError> {
101 let initial =
102 now_ms
103 .checked_mul(NONCE_SUFFIX_BASE)
104 .ok_or(NonceError::TimestampOverflow {
105 milliseconds: now_ms,
106 })?;
107 let state = self.state_for(wallet, subaccount_id);
108
109 loop {
110 let last = state.load(Ordering::Acquire);
111 let candidate = if last == NONCE_UNINITIALIZED || initial > last {
112 initial
113 } else {
114 if last % NONCE_SUFFIX_BASE == NONCE_SUFFIX_MAX {
115 return Err(NonceError::SuffixExhausted {
116 millisecond: last / NONCE_SUFFIX_BASE,
117 });
118 }
119 let next = last.checked_add(1).ok_or(NonceError::NonceOverflow)?;
120 if next == NONCE_UNINITIALIZED {
121 return Err(NonceError::NonceOverflow);
122 }
123 next
124 };
125
126 if state
127 .compare_exchange_weak(last, candidate, Ordering::AcqRel, Ordering::Acquire)
128 .is_ok()
129 {
130 return Ok(candidate);
131 }
132 }
133 }
134
135 #[must_use]
137 pub fn last_issued(&self, wallet: &str, subaccount_id: u64) -> Option<u64> {
138 Self::states()
139 .get(&Self::normalize_key(wallet, subaccount_id))
140 .map(|s| s.load(Ordering::Acquire))
141 .filter(|n| *n != NONCE_UNINITIALIZED)
142 }
143
144 fn state_for(&self, wallet: &str, subaccount_id: u64) -> Arc<AtomicU64> {
145 let entry = Self::states()
146 .entry(Self::normalize_key(wallet, subaccount_id))
147 .or_insert_with(|| Arc::new(AtomicU64::new(NONCE_UNINITIALIZED)));
148 entry.value().clone()
149 }
150
151 fn normalize_key(wallet: &str, subaccount_id: u64) -> (String, u64) {
156 (wallet.to_ascii_lowercase(), subaccount_id)
157 }
158
159 fn states() -> &'static DashMap<(String, u64), Arc<AtomicU64>> {
160 static STATES: OnceLock<DashMap<(String, u64), Arc<AtomicU64>>> = OnceLock::new();
161 STATES.get_or_init(DashMap::new)
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use std::{
168 sync::{Arc as StdArc, Barrier},
169 thread,
170 };
171
172 use rstest::rstest;
173
174 use super::*;
175
176 const NOW_MS: u64 = 1_700_000_000_000;
177 const NONCE_START: u64 = NOW_MS * NONCE_SUFFIX_BASE;
178 const WALLET_A: &str = "0x000000000000000000000000000000000000aaaa";
179 const WALLET_B: &str = "0x000000000000000000000000000000000000bbbb";
180
181 #[rstest]
182 fn test_next_nonce_at_first_call_uses_zero_suffix() {
183 let mgr = NonceManager::new();
184 let nonce = mgr.next_nonce_at(WALLET_A, 1, NOW_MS).unwrap();
185
186 assert_eq!(nonce, 1_700_000_000_000_000);
187 }
188
189 #[rstest]
190 fn test_sequential_calls_within_same_ms_are_monotonic() {
191 let mgr = NonceManager::new();
192 let nonces = [
193 mgr.next_nonce_at(WALLET_A, 2, NOW_MS).unwrap(),
194 mgr.next_nonce_at(WALLET_A, 2, NOW_MS).unwrap(),
195 mgr.next_nonce_at(WALLET_A, 2, NOW_MS).unwrap(),
196 ];
197
198 assert_eq!(nonces, [NONCE_START, NONCE_START + 1, NONCE_START + 2]);
199 }
200
201 #[rstest]
202 fn test_separate_managers_share_state() {
203 let first = NonceManager::new()
204 .next_nonce_at(WALLET_A, 3, NOW_MS)
205 .unwrap();
206 let second = NonceManager::new()
207 .next_nonce_at(WALLET_A, 3, NOW_MS)
208 .unwrap();
209
210 assert_eq!(first, NONCE_START);
211 assert_eq!(second, NONCE_START + 1);
212 }
213
214 #[rstest]
215 #[expect(
216 clippy::needless_collect,
217 reason = "all threads must start before any can pass the barrier"
218 )]
219 fn test_simultaneous_managers_allocate_unique_ordered_range() {
220 const THREADS: u64 = 8;
221
222 let barrier = StdArc::new(Barrier::new(THREADS as usize));
223 let handles: Vec<_> = (0..THREADS)
224 .map(|_| {
225 let barrier = StdArc::clone(&barrier);
226
227 thread::spawn(move || {
228 let mgr = NonceManager::new();
229 barrier.wait();
230 mgr.next_nonce_at(WALLET_A, 4, NOW_MS).unwrap()
231 })
232 })
233 .collect();
234 let mut nonces: Vec<_> = handles
235 .into_iter()
236 .map(|handle| handle.join().unwrap())
237 .collect();
238 nonces.sort_unstable();
239
240 let expected: Vec<_> = (0..THREADS).map(|suffix| NONCE_START + suffix).collect();
241 assert_eq!(nonces, expected);
242 }
243
244 #[rstest]
245 fn test_advancing_clock_starts_new_suffix_range() {
246 let mgr = NonceManager::new();
247 let first = mgr.next_nonce_at(WALLET_A, 5, NOW_MS).unwrap();
248 let second = mgr.next_nonce_at(WALLET_A, 5, NOW_MS + 1).unwrap();
249
250 assert_eq!(first, NONCE_START);
251 assert_eq!(second, NONCE_START + NONCE_SUFFIX_BASE);
252 }
253
254 #[rstest]
255 fn test_clock_rollback_advances_last_logical_millisecond() {
256 let first = NonceManager::new()
257 .next_nonce_at(WALLET_A, 6, NOW_MS + 10)
258 .unwrap();
259 let second = NonceManager::new()
260 .next_nonce_at(WALLET_A, 6, NOW_MS)
261 .unwrap();
262
263 assert_eq!(first, (NOW_MS + 10) * NONCE_SUFFIX_BASE);
264 assert_eq!(second, first + 1);
265 }
266
267 #[rstest]
268 fn test_distinct_wallets_track_independent_state() {
269 let mgr = NonceManager::new();
270 let first_a = mgr.next_nonce_at(WALLET_A, 7, NOW_MS).unwrap();
271 let first_b = mgr.next_nonce_at(WALLET_B, 7, NOW_MS).unwrap();
272 let second_a = mgr.next_nonce_at(WALLET_A, 7, NOW_MS).unwrap();
273
274 assert_eq!(first_a, NONCE_START);
275 assert_eq!(first_b, NONCE_START);
276 assert_eq!(second_a, NONCE_START + 1);
277 assert_eq!(mgr.last_issued(WALLET_A, 7), Some(second_a));
278 assert_eq!(mgr.last_issued(WALLET_B, 7), Some(first_b));
279 }
280
281 #[rstest]
282 fn test_distinct_subaccounts_track_independent_state() {
283 let mgr = NonceManager::new();
284 let first = mgr.next_nonce_at(WALLET_A, 8, NOW_MS).unwrap();
285 let second = mgr.next_nonce_at(WALLET_A, 9, NOW_MS).unwrap();
286
287 assert_eq!(first, NONCE_START);
288 assert_eq!(second, NONCE_START);
289 }
290
291 #[rstest]
292 fn test_checksum_and_lowercase_wallet_share_state() {
293 let lowercase = "0x000000000000000000000000000000000000abcd";
294 let checksum = "0x000000000000000000000000000000000000ABCD";
295 let first = NonceManager::new()
296 .next_nonce_at(lowercase, 10, NOW_MS)
297 .unwrap();
298 let second = NonceManager::new()
299 .next_nonce_at(checksum, 10, NOW_MS)
300 .unwrap();
301
302 assert_eq!(first, NONCE_START);
303 assert_eq!(second, NONCE_START + 1);
304 assert_eq!(NonceManager::new().last_issued(lowercase, 10), Some(second),);
305 assert_eq!(NonceManager::new().last_issued(checksum, 10), Some(second),);
306 }
307
308 #[rstest]
309 fn test_last_issued_reports_latest_value() {
310 let mgr = NonceManager::new();
311 assert_eq!(mgr.last_issued(WALLET_A, 11), None);
312
313 let nonce = mgr.next_nonce_at(WALLET_A, 11, NOW_MS).unwrap();
314
315 assert_eq!(nonce, NONCE_START);
316 assert_eq!(mgr.last_issued(WALLET_A, 11), Some(NONCE_START));
317 }
318
319 #[rstest]
320 fn test_suffix_exhaustion_stops_after_suffix_999() {
321 let mgr = NonceManager::new();
322 for suffix in 0..=NONCE_SUFFIX_MAX {
323 let nonce = mgr.next_nonce_at(WALLET_A, 12, NOW_MS).unwrap();
324 assert_eq!(nonce, NONCE_START + suffix);
325 }
326
327 assert_eq!(
328 mgr.next_nonce_at(WALLET_A, 12, NOW_MS),
329 Err(NonceError::SuffixExhausted {
330 millisecond: NOW_MS,
331 }),
332 );
333 assert_eq!(
334 mgr.last_issued(WALLET_A, 12),
335 Some(NONCE_START + NONCE_SUFFIX_MAX),
336 );
337 }
338
339 #[rstest]
340 fn test_suffix_exhaustion_during_clock_rollback_reports_logical_millisecond() {
341 let mgr = NonceManager::new();
342 for suffix in 0..=NONCE_SUFFIX_MAX {
343 let nonce = mgr.next_nonce_at(WALLET_A, 13, NOW_MS).unwrap();
344 assert_eq!(nonce, NONCE_START + suffix);
345 }
346
347 assert_eq!(
348 NonceManager::new().next_nonce_at(WALLET_A, 13, NOW_MS - 1),
349 Err(NonceError::SuffixExhausted {
350 millisecond: NOW_MS,
351 }),
352 );
353 }
354
355 #[rstest]
356 fn test_timestamp_overflow_does_not_create_stream_state() {
357 let now_ms = (u64::MAX / NONCE_SUFFIX_BASE) + 1;
358 let mgr = NonceManager::new();
359
360 assert_eq!(
361 mgr.next_nonce_at(WALLET_A, 14, now_ms),
362 Err(NonceError::TimestampOverflow {
363 milliseconds: now_ms,
364 }),
365 );
366 assert_eq!(mgr.last_issued(WALLET_A, 14), None);
367 }
368
369 #[rstest]
370 fn test_epoch_first_call_uses_zero_nonce() {
371 let mgr = NonceManager::new();
372 let first = mgr.next_nonce_at(WALLET_A, 17, 0).unwrap();
373 let second = mgr.next_nonce_at(WALLET_A, 17, 0).unwrap();
374
375 assert_eq!(first, 0);
376 assert_eq!(second, 1);
377 assert_eq!(mgr.last_issued(WALLET_A, 17), Some(1));
378 }
379
380 #[rstest]
381 fn test_nonce_overflow_does_not_emit_uninitialized_sentinel() {
382 let now_ms = u64::MAX / NONCE_SUFFIX_BASE;
383 let start = now_ms * NONCE_SUFFIX_BASE;
384 let mgr = NonceManager::new();
385 for suffix in 0..(u64::MAX - start) {
386 let nonce = mgr.next_nonce_at(WALLET_A, 15, now_ms).unwrap();
387 assert_eq!(nonce, start + suffix);
388 }
389
390 assert_eq!(
391 mgr.next_nonce_at(WALLET_A, 15, now_ms),
392 Err(NonceError::NonceOverflow),
393 );
394 assert_eq!(mgr.last_issued(WALLET_A, 15), Some(u64::MAX - 1));
395 }
396
397 #[rstest]
398 fn test_next_nonce_uses_system_clock_when_called_without_injection() {
399 let mgr = NonceManager::new();
400 let nonce = mgr.next_nonce(WALLET_A, 16).unwrap();
401
402 assert!(nonce > NONCE_START);
403 assert_eq!(nonce % NONCE_SUFFIX_BASE, 0);
404 }
405}