1use std::sync::{
36 Arc,
37 atomic::{AtomicU64, Ordering},
38};
39
40use dashmap::DashMap;
41use thiserror::Error;
42
43use crate::signing::encoding::utc_now_ms;
44
45#[derive(Debug, Error, PartialEq, Eq)]
47pub enum NonceError {
48 #[error("system clock is before UNIX epoch")]
50 ClockBeforeEpoch,
51}
52
53#[derive(Debug, Default)]
55pub struct NonceManager {
56 states: DashMap<(String, u64), Arc<AtomicU64>>,
57}
58
59impl NonceManager {
60 #[must_use]
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn next_nonce(&self, wallet: &str, subaccount_id: u64) -> Result<u64, NonceError> {
74 let now_ms = utc_now_ms().map_err(|_| NonceError::ClockBeforeEpoch)?;
75 Ok(self.next_nonce_at(wallet, subaccount_id, now_ms))
76 }
77
78 pub fn next_nonce_at(&self, wallet: &str, subaccount_id: u64, now_ms: u64) -> u64 {
81 let state = self.state_for(wallet, subaccount_id);
82 let initial = now_ms.saturating_mul(10);
84
85 loop {
86 let last = state.load(Ordering::Acquire);
87 let candidate = if initial > last { initial } else { last + 1 };
88 if state
89 .compare_exchange_weak(last, candidate, Ordering::AcqRel, Ordering::Acquire)
90 .is_ok()
91 {
92 return candidate;
93 }
94 }
95 }
96
97 pub fn refresh(&self, wallet: &str, subaccount_id: u64, last_seen_nonce: u64) {
107 let state = self.state_for(wallet, subaccount_id);
108 loop {
109 let current = state.load(Ordering::Acquire);
110 if last_seen_nonce <= current {
111 return;
112 }
113
114 if state
115 .compare_exchange_weak(
116 current,
117 last_seen_nonce,
118 Ordering::AcqRel,
119 Ordering::Acquire,
120 )
121 .is_ok()
122 {
123 return;
124 }
125 }
126 }
127
128 #[must_use]
130 pub fn last_issued(&self, wallet: &str, subaccount_id: u64) -> Option<u64> {
131 self.states
132 .get(&Self::normalize_key(wallet, subaccount_id))
133 .map(|s| s.load(Ordering::Acquire))
134 .filter(|n| *n != 0)
135 }
136
137 fn state_for(&self, wallet: &str, subaccount_id: u64) -> Arc<AtomicU64> {
138 let entry = self
139 .states
140 .entry(Self::normalize_key(wallet, subaccount_id))
141 .or_insert_with(|| Arc::new(AtomicU64::new(0)));
142 entry.value().clone()
143 }
144
145 fn normalize_key(wallet: &str, subaccount_id: u64) -> (String, u64) {
150 (wallet.to_ascii_lowercase(), subaccount_id)
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use std::{sync::Arc as StdArc, thread};
157
158 use rstest::rstest;
159
160 use super::*;
161
162 const WALLET_A: &str = "0x000000000000000000000000000000000000aaaa";
163 const WALLET_B: &str = "0x000000000000000000000000000000000000bbbb";
164
165 #[rstest]
166 fn test_next_nonce_at_first_call_concatenates_zero_suffix() {
167 let mgr = NonceManager::new();
168 let nonce = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
169 assert_eq!(nonce, 17_000_000_000_000);
171 }
172
173 #[rstest]
174 fn test_sequential_calls_within_same_ms_are_monotonic() {
175 let mgr = NonceManager::new();
176 let n1 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
177 let n2 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
178 let n3 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
179 assert!(
180 n1 < n2 && n2 < n3,
181 "nonces must be monotonic, was {n1}, {n2}, {n3}"
182 );
183 assert_eq!(n2 - n1, 1);
184 assert_eq!(n3 - n2, 1);
185 }
186
187 #[rstest]
188 fn test_advancing_clock_jumps_to_new_prefix() {
189 let mgr = NonceManager::new();
190 let n1 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
191 let n2 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_001);
192 assert_eq!(n1, 17_000_000_000_000);
195 assert_eq!(n2, 17_000_000_000_010);
196 assert!(n2 > n1);
197 }
198
199 #[rstest]
200 fn test_distinct_keys_track_independent_state() {
201 let mgr = NonceManager::new();
202 let a = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
203 let b = mgr.next_nonce_at(WALLET_B, 1, 1_700_000_000_000);
204 assert_eq!(a, b);
206 let a2 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
208 assert_eq!(a2, a + 1);
209 assert_eq!(mgr.last_issued(WALLET_B, 1), Some(b));
210 }
211
212 #[rstest]
213 fn test_distinct_subaccounts_track_independent_state() {
214 let mgr = NonceManager::new();
215 let a1 = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
216 let a2 = mgr.next_nonce_at(WALLET_A, 2, 1_700_000_000_000);
217 assert_eq!(a1, a2);
218 }
219
220 #[rstest]
221 fn test_refresh_advances_last_issued() {
222 let mgr = NonceManager::new();
223 mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
224 mgr.refresh(WALLET_A, 1, 99_999_999_999_999);
225 let n = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
226 assert_eq!(n, 99_999_999_999_999 + 1);
227 }
228
229 #[rstest]
230 fn test_refresh_never_rewinds_below_local_state() {
231 let mgr = NonceManager::new();
232 let high = mgr.next_nonce_at(WALLET_A, 1, 9_000_000_000_000);
236 mgr.refresh(WALLET_A, 1, 1_000);
237 let next = mgr.next_nonce_at(WALLET_A, 1, 1_000);
238 assert!(
239 next > high,
240 "refresh below local state must not rewind, last={high}, next={next}",
241 );
242 }
243
244 #[rstest]
245 fn test_checksum_and_lowercase_wallet_share_state() {
246 let mgr = NonceManager::new();
247 let lowercase = "0x000000000000000000000000000000000000abcd";
248 let checksum = "0x000000000000000000000000000000000000ABCD";
249 let n1 = mgr.next_nonce_at(lowercase, 1, 1_700_000_000_000);
250 let n2 = mgr.next_nonce_at(checksum, 1, 1_700_000_000_000);
251 assert_eq!(
252 n2,
253 n1 + 1,
254 "checksum and lowercase forms of the same address must share one nonce stream",
255 );
256 }
257
258 #[rstest]
259 fn test_last_issued_finds_state_regardless_of_address_case() {
260 let mgr = NonceManager::new();
261 let lowercase = "0x000000000000000000000000000000000000abcd";
262 let checksum = "0x000000000000000000000000000000000000ABCD";
263 let issued = mgr.next_nonce_at(lowercase, 1, 1_700_000_000_000);
264 assert_eq!(mgr.last_issued(lowercase, 1), Some(issued));
265 assert_eq!(
266 mgr.last_issued(checksum, 1),
267 Some(issued),
268 "lookup must normalize the same way as next_nonce/refresh",
269 );
270 }
271
272 #[rstest]
273 fn test_last_issued_reports_latest_value() {
274 let mgr = NonceManager::new();
275 assert_eq!(mgr.last_issued(WALLET_A, 1), None);
276 let n = mgr.next_nonce_at(WALLET_A, 1, 1_700_000_000_000);
277 assert_eq!(mgr.last_issued(WALLET_A, 1), Some(n));
278 }
279
280 #[rstest]
281 fn test_concurrent_callers_see_no_duplicates_or_gaps() {
282 let mgr = StdArc::new(NonceManager::new());
283 let threads = 8;
284 let per_thread = 250;
285 let now_ms = 1_700_000_000_000;
286 let handles: Vec<_> = (0..threads)
287 .map(|_| {
288 let mgr = StdArc::clone(&mgr);
289
290 thread::spawn(move || -> Vec<u64> {
291 (0..per_thread)
292 .map(|_| mgr.next_nonce_at(WALLET_A, 1, now_ms))
293 .collect()
294 })
295 })
296 .collect();
297 let mut all = Vec::with_capacity(threads * per_thread);
298 for h in handles {
299 all.extend(h.join().unwrap());
300 }
301 all.sort_unstable();
302 let total = (threads * per_thread) as u64;
303 let expected: Vec<u64> = (0..total).map(|i| now_ms * 10 + i).collect();
304 assert_eq!(
305 all, expected,
306 "concurrent issuance must be contiguous from ms*10",
307 );
308 }
309
310 #[rstest]
311 fn test_next_nonce_uses_system_clock_when_called_without_injection() {
312 let mgr = NonceManager::new();
313 let n = mgr.next_nonce(WALLET_A, 1).unwrap();
314 assert!(n > 17_000_000_000_000, "nonce too small: {n}");
317 }
318}