nautilus_hyperliquid/signing/
nonce.rs1use std::{
17 collections::{HashMap, VecDeque},
18 fmt::Display,
19 sync::{Arc, Mutex},
20 time::{SystemTime, UNIX_EPOCH},
21};
22
23use nautilus_core::MUTEX_POISONED;
24
25use super::types::SignerId;
26use crate::http::error::{Error, Result};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30pub struct TimeNonce(pub i128);
31
32impl TimeNonce {
33 pub fn from_millis(ms: i128) -> Self {
35 Self(ms)
36 }
37
38 pub fn as_millis(self) -> i128 {
40 self.0
41 }
42
43 pub fn now_millis() -> Self {
49 let now = SystemTime::now()
50 .duration_since(UNIX_EPOCH)
51 .expect("Time went backwards");
52 Self::from_millis(now.as_millis() as i128)
53 }
54}
55
56impl Display for TimeNonce {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "{}", self.0)
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct NoncePolicy {
65 pub past_ms: i64,
66 pub future_ms: i64,
67 pub keep_last_n: usize,
68}
69
70impl NoncePolicy {
71 pub fn new(past_ms: i64, future_ms: i64, keep_last_n: usize) -> Self {
72 Self {
73 past_ms,
74 future_ms,
75 keep_last_n,
76 }
77 }
78}
79
80impl Default for NoncePolicy {
81 fn default() -> Self {
82 Self {
83 past_ms: 2 * 24 * 60 * 60 * 1000,
84 future_ms: 24 * 60 * 60 * 1000,
85 keep_last_n: 100,
86 }
87 }
88}
89
90#[derive(Debug, thiserror::Error)]
92pub enum NonceError {
93 #[error("Nonce too old: {nonce} is before window start {window_start}")]
94 TooOld {
95 nonce: TimeNonce,
96 window_start: TimeNonce,
97 },
98
99 #[error("Nonce too new: {nonce} is after window end {window_end}")]
100 TooNew {
101 nonce: TimeNonce,
102 window_end: TimeNonce,
103 },
104
105 #[error("Nonce already used: {nonce}")]
106 AlreadyUsed { nonce: TimeNonce },
107
108 #[error("Nonce must be greater than minimum: {nonce} <= {min_nonce}")]
109 NotMonotonic {
110 nonce: TimeNonce,
111 min_nonce: TimeNonce,
112 },
113}
114
115#[derive(Debug)]
117struct SignerState {
118 next_nonce: i128,
119 used_nonces: VecDeque<TimeNonce>,
120 max_used: usize,
121}
122
123impl SignerState {
124 fn new(initial_nonce: i128, max_used: usize) -> Self {
125 Self {
126 next_nonce: initial_nonce,
127 used_nonces: VecDeque::with_capacity(max_used),
128 max_used,
129 }
130 }
131
132 fn next_nonce(&mut self) -> TimeNonce {
133 let now = TimeNonce::now_millis().0;
135 self.next_nonce = self.next_nonce.max(now);
136
137 let nonce = TimeNonce::from_millis(self.next_nonce);
139 self.next_nonce += 1;
140
141 self.used_nonces.push_back(nonce);
142 if self.used_nonces.len() > self.max_used {
143 self.used_nonces.pop_front();
144 }
145
146 nonce
147 }
148
149 fn validate_local(
150 &self,
151 nonce: TimeNonce,
152 _policy: &NoncePolicy,
153 ) -> std::result::Result<(), NonceError> {
154 if self.used_nonces.contains(&nonce) {
156 return Err(NonceError::AlreadyUsed { nonce });
157 }
158
159 if let Some(&min_used) = self.used_nonces.front()
161 && nonce.0 <= min_used.0
162 {
163 return Err(NonceError::NotMonotonic {
164 nonce,
165 min_nonce: min_used,
166 });
167 }
168
169 Ok(())
170 }
171
172 fn fast_forward_to(&mut self, now_ms: i128) {
173 if now_ms > self.next_nonce {
174 self.next_nonce = now_ms;
175 }
176 }
177}
178
179#[derive(Debug)]
181pub struct NonceManager {
182 policy: NoncePolicy,
183 signer_states: Arc<Mutex<HashMap<SignerId, SignerState>>>,
184}
185
186impl NonceManager {
187 pub fn new() -> Self {
188 Self {
189 policy: NoncePolicy::default(),
190 signer_states: Arc::new(Mutex::new(HashMap::new())),
191 }
192 }
193
194 pub fn with_policy(policy: NoncePolicy) -> Self {
195 Self {
196 policy,
197 signer_states: Arc::new(Mutex::new(HashMap::new())),
198 }
199 }
200
201 pub fn next(&self, signer: SignerId) -> Result<TimeNonce> {
207 let mut states = self.signer_states.lock().expect(MUTEX_POISONED);
208 let state = states.entry(signer).or_insert_with(|| {
209 SignerState::new(TimeNonce::now_millis().0, self.policy.keep_last_n)
210 });
211 Ok(state.next_nonce())
212 }
213
214 pub fn fast_forward_to(&self, now_ms: i128) {
220 let mut states = self.signer_states.lock().expect(MUTEX_POISONED);
221 for state in states.values_mut() {
222 state.fast_forward_to(now_ms);
223 }
224 }
225
226 pub fn validate_local(&self, signer: SignerId, nonce: TimeNonce) -> Result<()> {
232 let states = self.signer_states.lock().expect(MUTEX_POISONED);
233
234 let now_ms = TimeNonce::now_millis().0;
236 let window_start = now_ms - self.policy.past_ms as i128;
237 let window_end = now_ms + self.policy.future_ms as i128;
238
239 if nonce.0 < window_start {
240 return Err(Error::nonce_window(format!(
241 "Nonce too old: {} is before window start {}",
242 nonce,
243 TimeNonce::from_millis(window_start)
244 )));
245 }
246
247 if nonce.0 > window_end {
248 return Err(Error::nonce_window(format!(
249 "Nonce too new: {} is after window end {}",
250 nonce,
251 TimeNonce::from_millis(window_end)
252 )));
253 }
254
255 if let Some(state) = states.get(&signer) {
257 state
258 .validate_local(nonce, &self.policy)
259 .map_err(|e| Error::nonce_window(e.to_string()))?;
260 }
261
262 Ok(())
263 }
264
265 pub fn policy(&self) -> &NoncePolicy {
266 &self.policy
267 }
268}
269
270impl Default for NonceManager {
271 fn default() -> Self {
272 Self::new()
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use std::thread;
279
280 use rstest::rstest;
281
282 use super::*;
283
284 #[rstest]
285 fn test_time_nonce_creation() {
286 let nonce_ms = TimeNonce::from_millis(1640995200000);
287 assert_eq!(nonce_ms.as_millis(), 1640995200000);
288 }
289
290 #[rstest]
291 fn test_nonce_monotonicity() {
292 let manager = NonceManager::new();
293 let signer = SignerId::from("test_signer");
294
295 let nonce1 = manager.next(signer.clone()).unwrap();
296 let nonce2 = manager.next(signer.clone()).unwrap();
297 let nonce3 = manager.next(signer).unwrap();
298
299 assert!(nonce2 > nonce1);
300 assert!(nonce3 > nonce2);
301 }
302
303 #[rstest]
304 fn test_nonce_window_validation() {
305 let manager = NonceManager::new();
306 let signer = SignerId::from("test_signer");
307
308 let valid_nonce = TimeNonce::now_millis();
309 assert!(manager.validate_local(signer.clone(), valid_nonce).is_ok());
310
311 let old_nonce = TimeNonce::from_millis(TimeNonce::now_millis().0 - 3 * 24 * 60 * 60 * 1000);
312 assert!(manager.validate_local(signer.clone(), old_nonce).is_err());
313
314 let future_nonce =
315 TimeNonce::from_millis(TimeNonce::now_millis().0 + 2 * 24 * 60 * 60 * 1000);
316 assert!(manager.validate_local(signer, future_nonce).is_err());
317 }
318
319 #[rstest]
320 fn test_nonce_deduplication() {
321 let manager = NonceManager::new();
322 let signer = SignerId::from("test_signer");
323
324 let nonce = manager.next(signer.clone()).unwrap();
325 assert!(manager.validate_local(signer, nonce).is_err());
326 }
327
328 #[rstest]
329 fn test_fast_forward() {
330 let manager = NonceManager::new();
331 let signer = SignerId::from("test_signer");
332
333 let nonce1 = manager.next(signer.clone()).unwrap();
334
335 let future_time = TimeNonce::now_millis().0 + 10_000;
336 manager.fast_forward_to(future_time);
337
338 let nonce2 = manager.next(signer).unwrap();
339 assert!(nonce2.0 >= future_time);
340 assert!(nonce2 > nonce1); }
342
343 #[rstest]
344 #[allow(clippy::needless_collect)] fn test_concurrent_nonce_generation() {
346 let manager = Arc::new(NonceManager::new());
347 let signer = SignerId::from("concurrent_signer");
348
349 let handles: Vec<_> = (0..10)
350 .map(|_| {
351 let manager = Arc::clone(&manager);
352 let signer = signer.clone();
353 thread::spawn(move || manager.next(signer).unwrap())
354 })
355 .collect();
356
357 let mut nonces: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
358
359 nonces.sort();
360
361 for i in 1..nonces.len() {
362 assert!(nonces[i] > nonces[i - 1]);
363 }
364 }
365
366 #[rstest]
367 fn test_custom_policy() {
368 let policy = NoncePolicy::new(1000, 2000, 50);
369 let manager = NonceManager::with_policy(policy);
370
371 assert_eq!(manager.policy().past_ms, 1000);
372 assert_eq!(manager.policy().future_ms, 2000);
373 assert_eq!(manager.policy().keep_last_n, 50);
374 }
375}