1use std::{
29 sync::{
30 Arc, OnceLock,
31 atomic::{AtomicU64, Ordering},
32 },
33 time::UNIX_EPOCH,
34};
35
36use dashmap::DashMap;
37use serde::{Deserialize, Deserializer, Serializer, de::Error};
38use thiserror::Error;
39
40const NONCE_PAST_NS: u64 = 90 * 24 * 60 * 60 * 1_000_000_000;
41pub(crate) const NONCE_FUTURE_NS: u64 = 60 * 60 * 1_000_000_000;
42const NONCE_UNINITIALIZED: u64 = u64::MAX;
43
44#[derive(Debug, Error, PartialEq, Eq)]
46pub enum NonceError {
47 #[error("system clock is before UNIX epoch")]
49 ClockBeforeEpoch,
50 #[error("nanosecond timestamp overflows u64")]
52 TimestampOverflow,
53 #[error("nonce {nonce} is outside the order window relative to time {now_ns}")]
55 OutsideWindow {
56 nonce: u64,
58 now_ns: u64,
60 },
61 #[error("next nonce exceeds u64::MAX")]
63 NonceOverflow,
64}
65
66#[derive(Debug, Default)]
68pub struct NonceManager;
69
70impl NonceManager {
71 #[must_use]
73 pub fn new() -> Self {
74 Self
75 }
76
77 pub fn next_nonce(&self, wallet: &str, subaccount_id: u64) -> Result<u64, NonceError> {
85 let now_ns = nautilus_core::time::wall_clock_now()
86 .duration_since(UNIX_EPOCH)
87 .map_err(|_| NonceError::ClockBeforeEpoch)?
88 .as_nanos()
89 .try_into()
90 .map_err(|_| NonceError::TimestampOverflow)?;
91 self.next_nonce_at(wallet, subaccount_id, now_ns)
92 }
93
94 pub fn next_nonce_at(
102 &self,
103 wallet: &str,
104 subaccount_id: u64,
105 now_ns: u64,
106 ) -> Result<u64, NonceError> {
107 if now_ns == NONCE_UNINITIALIZED {
108 return Err(NonceError::NonceOverflow);
109 }
110
111 let state = self.state_for(wallet, subaccount_id);
112 loop {
113 let last = state.load(Ordering::Acquire);
114
115 let candidate = if last == NONCE_UNINITIALIZED || now_ns > last {
116 now_ns
117 } else {
118 let next = last.checked_add(1).ok_or(NonceError::NonceOverflow)?;
119 if next == NONCE_UNINITIALIZED {
120 return Err(NonceError::NonceOverflow);
121 }
122
123 next
124 };
125
126 validate_nonce_at(candidate, now_ns)?;
127 if state
128 .compare_exchange_weak(last, candidate, Ordering::AcqRel, Ordering::Acquire)
129 .is_ok()
130 {
131 return Ok(candidate);
132 }
133 }
134 }
135
136 #[must_use]
138 pub fn last_issued(&self, wallet: &str, subaccount_id: u64) -> Option<u64> {
139 Self::states()
140 .get(&Self::normalize_key(wallet, subaccount_id))
141 .map(|s| s.load(Ordering::Acquire))
142 .filter(|n| *n != NONCE_UNINITIALIZED)
143 }
144
145 fn state_for(&self, wallet: &str, subaccount_id: u64) -> Arc<AtomicU64> {
146 let entry = Self::states()
147 .entry(Self::normalize_key(wallet, subaccount_id))
148 .or_insert_with(|| Arc::new(AtomicU64::new(NONCE_UNINITIALIZED)));
149 entry.value().clone()
150 }
151
152 fn normalize_key(wallet: &str, subaccount_id: u64) -> (String, u64) {
157 (wallet.to_ascii_lowercase(), subaccount_id)
158 }
159
160 fn states() -> &'static DashMap<(String, u64), Arc<AtomicU64>> {
161 static STATES: OnceLock<DashMap<(String, u64), Arc<AtomicU64>>> = OnceLock::new();
162 STATES.get_or_init(DashMap::new)
163 }
164}
165
166pub(crate) fn serialize_nonce<S>(nonce: &u64, serializer: S) -> Result<S::Ok, S::Error>
167where
168 S: Serializer,
169{
170 serializer.collect_str(nonce)
171}
172
173pub(crate) fn deserialize_nonce<'de, D>(deserializer: D) -> Result<u64, D::Error>
174where
175 D: Deserializer<'de>,
176{
177 let value = String::deserialize(deserializer)?;
178 value.parse::<u64>().map_err(D::Error::custom)
179}
180
181fn validate_nonce_at(nonce: u64, now_ns: u64) -> Result<(), NonceError> {
182 if nonce < now_ns.saturating_sub(NONCE_PAST_NS)
183 || nonce > now_ns.saturating_add(NONCE_FUTURE_NS)
184 {
185 return Err(NonceError::OutsideWindow { nonce, now_ns });
186 }
187
188 Ok(())
189}
190
191#[cfg(test)]
192mod tests {
193 use std::{
194 sync::{Arc as StdArc, Barrier},
195 thread,
196 };
197
198 use rstest::rstest;
199
200 use super::*;
201
202 const NOW_NS: u64 = 1_700_000_000_000_000_000;
203 const NONCE_START: u64 = NOW_NS;
204 const WALLET_A: &str = "0x000000000000000000000000000000000000aaaa";
205 const WALLET_B: &str = "0x000000000000000000000000000000000000bbbb";
206
207 #[rstest]
208 fn test_next_nonce_at_first_call_uses_nanoseconds() {
209 let mgr = NonceManager::new();
210 let nonce = mgr.next_nonce_at(WALLET_A, 1, NOW_NS).unwrap();
211
212 assert_eq!(nonce, 1_700_000_000_000_000_000);
213 }
214
215 #[rstest]
216 fn test_sequential_calls_within_same_ns_are_monotonic() {
217 let mgr = NonceManager::new();
218 let nonces = [
219 mgr.next_nonce_at(WALLET_A, 2, NOW_NS).unwrap(),
220 mgr.next_nonce_at(WALLET_A, 2, NOW_NS).unwrap(),
221 mgr.next_nonce_at(WALLET_A, 2, NOW_NS).unwrap(),
222 ];
223
224 assert_eq!(nonces, [NONCE_START, NONCE_START + 1, NONCE_START + 2]);
225 }
226
227 #[rstest]
228 fn test_separate_managers_share_state() {
229 let first = NonceManager::new()
230 .next_nonce_at(WALLET_A, 3, NOW_NS)
231 .unwrap();
232 let second = NonceManager::new()
233 .next_nonce_at(WALLET_A, 3, NOW_NS)
234 .unwrap();
235
236 assert_eq!(first, NONCE_START);
237 assert_eq!(second, NONCE_START + 1);
238 }
239
240 #[rstest]
241 #[expect(
242 clippy::needless_collect,
243 reason = "all threads must start before any can pass the barrier"
244 )]
245 fn test_simultaneous_managers_allocate_unique_ordered_range() {
246 const THREADS: u64 = 8;
247 const ALLOCATIONS: u64 = 128;
248
249 let barrier = StdArc::new(Barrier::new(THREADS as usize));
250 let handles: Vec<_> = (0..THREADS)
251 .map(|_| {
252 let barrier = StdArc::clone(&barrier);
253
254 thread::spawn(move || {
255 let mgr = NonceManager::new();
256 barrier.wait();
257 (0..ALLOCATIONS)
258 .map(|_| mgr.next_nonce_at(WALLET_A, 4, NOW_NS).unwrap())
259 .collect::<Vec<_>>()
260 })
261 })
262 .collect();
263
264 let mut nonces: Vec<_> = handles
265 .into_iter()
266 .flat_map(|handle| handle.join().unwrap())
267 .collect();
268 nonces.sort_unstable();
269
270 let expected: Vec<_> = (0..THREADS * ALLOCATIONS)
271 .map(|offset| NONCE_START + offset)
272 .collect();
273 assert_eq!(nonces, expected);
274 }
275
276 #[rstest]
277 fn test_advancing_clock_uses_nanosecond_timestamp() {
278 let mgr = NonceManager::new();
279 let first = mgr.next_nonce_at(WALLET_A, 5, NOW_NS).unwrap();
280 let second = mgr.next_nonce_at(WALLET_A, 5, NOW_NS + 1).unwrap();
281
282 assert_eq!(first, NONCE_START);
283 assert_eq!(second, NONCE_START + 1);
284 }
285
286 #[rstest]
287 fn test_clock_rollback_advances_last_nanosecond() {
288 let first = NonceManager::new()
289 .next_nonce_at(WALLET_A, 6, NOW_NS + 10)
290 .unwrap();
291 let second = NonceManager::new()
292 .next_nonce_at(WALLET_A, 6, NOW_NS)
293 .unwrap();
294
295 assert_eq!(first, NOW_NS + 10);
296 assert_eq!(second, first + 1);
297 }
298
299 #[rstest]
300 fn test_distinct_wallets_track_independent_state() {
301 let mgr = NonceManager::new();
302 let first_a = mgr.next_nonce_at(WALLET_A, 7, NOW_NS).unwrap();
303 let first_b = mgr.next_nonce_at(WALLET_B, 7, NOW_NS).unwrap();
304 let second_a = mgr.next_nonce_at(WALLET_A, 7, NOW_NS).unwrap();
305
306 assert_eq!(first_a, NONCE_START);
307 assert_eq!(first_b, NONCE_START);
308 assert_eq!(second_a, NONCE_START + 1);
309 assert_eq!(mgr.last_issued(WALLET_A, 7), Some(second_a));
310 assert_eq!(mgr.last_issued(WALLET_B, 7), Some(first_b));
311 }
312
313 #[rstest]
314 fn test_distinct_subaccounts_track_independent_state() {
315 let mgr = NonceManager::new();
316 let first = mgr.next_nonce_at(WALLET_A, 8, NOW_NS).unwrap();
317 let second = mgr.next_nonce_at(WALLET_A, 9, NOW_NS).unwrap();
318
319 assert_eq!(first, NONCE_START);
320 assert_eq!(second, NONCE_START);
321 }
322
323 #[rstest]
324 fn test_checksum_and_lowercase_wallet_share_state() {
325 let lowercase = "0x000000000000000000000000000000000000abcd";
326 let checksum = "0x000000000000000000000000000000000000ABCD";
327 let first = NonceManager::new()
328 .next_nonce_at(lowercase, 10, NOW_NS)
329 .unwrap();
330 let second = NonceManager::new()
331 .next_nonce_at(checksum, 10, NOW_NS)
332 .unwrap();
333
334 assert_eq!(first, NONCE_START);
335 assert_eq!(second, NONCE_START + 1);
336 assert_eq!(NonceManager::new().last_issued(lowercase, 10), Some(second),);
337 assert_eq!(NonceManager::new().last_issued(checksum, 10), Some(second),);
338 }
339
340 #[rstest]
341 fn test_last_issued_reports_latest_value() {
342 let mgr = NonceManager::new();
343 assert_eq!(mgr.last_issued(WALLET_A, 11), None);
344
345 let nonce = mgr.next_nonce_at(WALLET_A, 11, NOW_NS).unwrap();
346
347 assert_eq!(nonce, NONCE_START);
348 assert_eq!(mgr.last_issued(WALLET_A, 11), Some(NONCE_START));
349 }
350
351 #[rstest]
352 fn test_contention_has_no_millisecond_suffix_limit() {
353 let mgr = NonceManager::new();
354
355 for offset in 0..2_000 {
356 assert_eq!(
357 mgr.next_nonce_at(WALLET_A, 12, NOW_NS),
358 Ok(NONCE_START + offset)
359 );
360 }
361
362 assert_eq!(mgr.last_issued(WALLET_A, 12), Some(NONCE_START + 1_999));
363 }
364
365 #[rstest]
366 #[case(NOW_NS - NONCE_PAST_NS, true)]
367 #[case(NOW_NS - NONCE_PAST_NS - 1, false)]
368 #[case(NOW_NS + NONCE_FUTURE_NS, true)]
369 #[case(NOW_NS + NONCE_FUTURE_NS + 1, false)]
370 fn test_nonce_window_is_inclusive(#[case] nonce: u64, #[case] accepted: bool) {
371 let expected = if accepted {
372 Ok(())
373 } else {
374 Err(NonceError::OutsideWindow {
375 nonce,
376 now_ns: NOW_NS,
377 })
378 };
379
380 assert_eq!(validate_nonce_at(nonce, NOW_NS), expected);
381 }
382
383 #[rstest]
384 fn test_rollback_past_future_window_preserves_state() {
385 let mgr = NonceManager::new();
386 assert_eq!(mgr.next_nonce_at(WALLET_A, 13, NOW_NS), Ok(NOW_NS));
387 let now_ns = NOW_NS - NONCE_FUTURE_NS;
388 assert_eq!(
389 mgr.next_nonce_at(WALLET_A, 13, now_ns),
390 Err(NonceError::OutsideWindow {
391 nonce: NOW_NS + 1,
392 now_ns
393 })
394 );
395 assert_eq!(mgr.last_issued(WALLET_A, 13), Some(NOW_NS));
396 assert_eq!(mgr.next_nonce_at(WALLET_A, 13, now_ns + 1), Ok(NOW_NS + 1));
397 }
398
399 #[rstest]
400 fn test_window_bounds_do_not_overflow() {
401 assert_eq!(validate_nonce_at(0, 0), Ok(()));
402 assert_eq!(validate_nonce_at(u64::MAX - 1, u64::MAX), Ok(()));
403 }
404
405 #[rstest]
406 fn test_epoch_first_call_uses_zero_nonce() {
407 let mgr = NonceManager::new();
408 let first = mgr.next_nonce_at(WALLET_A, 17, 0).unwrap();
409 let second = mgr.next_nonce_at(WALLET_A, 17, 0).unwrap();
410
411 assert_eq!(first, 0);
412 assert_eq!(second, 1);
413 assert_eq!(mgr.last_issued(WALLET_A, 17), Some(1));
414 }
415
416 #[rstest]
417 fn test_nonce_overflow_does_not_emit_uninitialized_sentinel() {
418 let mgr = NonceManager::new();
419 assert_eq!(
420 mgr.next_nonce_at(WALLET_A, 15, u64::MAX - 1),
421 Ok(u64::MAX - 1)
422 );
423 assert_eq!(
424 mgr.next_nonce_at(WALLET_A, 15, u64::MAX - 1),
425 Err(NonceError::NonceOverflow)
426 );
427 assert_eq!(mgr.last_issued(WALLET_A, 15), Some(u64::MAX - 1));
428 assert_eq!(
429 mgr.next_nonce_at(WALLET_A, 14, u64::MAX),
430 Err(NonceError::NonceOverflow)
431 );
432 assert_eq!(mgr.last_issued(WALLET_A, 14), None);
433 }
434
435 #[rstest]
436 fn test_next_nonce_uses_system_clock_when_called_without_injection() {
437 let mgr = NonceManager::new();
438 let nonce = mgr.next_nonce(WALLET_A, 16).unwrap();
439
440 assert!(nonce > NONCE_START);
441 assert_eq!(mgr.last_issued(WALLET_A, 16), Some(nonce));
442 }
443}