nautilus_hyperliquid/signing/
nonce.rs1use std::{
17 collections::{HashMap, VecDeque},
18 fmt::Display,
19 sync::Arc,
20 time::{SystemTime, UNIX_EPOCH},
21};
22
23use parking_lot::Mutex;
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> {
203 let mut states = self.signer_states.lock();
204 let state = states.entry(signer).or_insert_with(|| {
205 SignerState::new(TimeNonce::now_millis().0, self.policy.keep_last_n)
206 });
207 Ok(state.next_nonce())
208 }
209
210 pub fn fast_forward_to(&self, now_ms: i128) {
212 let mut states = self.signer_states.lock();
213 for state in states.values_mut() {
214 state.fast_forward_to(now_ms);
215 }
216 }
217
218 pub fn validate_local(&self, signer: &SignerId, nonce: TimeNonce) -> Result<()> {
220 let states = self.signer_states.lock();
221
222 let now_ms = TimeNonce::now_millis().0;
224 let window_start = now_ms - self.policy.past_ms as i128;
225 let window_end = now_ms + self.policy.future_ms as i128;
226
227 if nonce.0 < window_start {
228 return Err(Error::nonce_window(format!(
229 "Nonce too old: {} is before window start {}",
230 nonce,
231 TimeNonce::from_millis(window_start)
232 )));
233 }
234
235 if nonce.0 > window_end {
236 return Err(Error::nonce_window(format!(
237 "Nonce too new: {} is after window end {}",
238 nonce,
239 TimeNonce::from_millis(window_end)
240 )));
241 }
242
243 if let Some(state) = states.get(signer) {
245 state
246 .validate_local(nonce, &self.policy)
247 .map_err(|e| Error::nonce_window(e.to_string()))?;
248 }
249
250 Ok(())
251 }
252
253 pub fn policy(&self) -> &NoncePolicy {
254 &self.policy
255 }
256}
257
258impl Default for NonceManager {
259 fn default() -> Self {
260 Self::new()
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use std::thread;
267
268 use rstest::rstest;
269
270 use super::*;
271
272 #[rstest]
273 fn test_time_nonce_creation() {
274 let nonce_ms = TimeNonce::from_millis(1640995200000);
275 assert_eq!(nonce_ms.as_millis(), 1640995200000);
276 }
277
278 #[rstest]
279 fn test_nonce_monotonicity() {
280 let manager = NonceManager::new();
281 let signer = SignerId::from("test_signer");
282
283 let nonce1 = manager.next(signer.clone()).unwrap();
284 let nonce2 = manager.next(signer.clone()).unwrap();
285 let nonce3 = manager.next(signer).unwrap();
286
287 assert!(nonce2 > nonce1);
288 assert!(nonce3 > nonce2);
289 }
290
291 #[rstest]
292 fn test_nonce_window_validation() {
293 let manager = NonceManager::new();
294 let signer = SignerId::from("test_signer");
295
296 let valid_nonce = TimeNonce::now_millis();
297 assert!(manager.validate_local(&signer, valid_nonce).is_ok());
298
299 let old_nonce = TimeNonce::from_millis(TimeNonce::now_millis().0 - 3 * 24 * 60 * 60 * 1000);
300 assert!(manager.validate_local(&signer, old_nonce).is_err());
301
302 let future_nonce =
303 TimeNonce::from_millis(TimeNonce::now_millis().0 + 2 * 24 * 60 * 60 * 1000);
304 assert!(manager.validate_local(&signer, future_nonce).is_err());
305 }
306
307 #[rstest]
308 fn test_nonce_deduplication() {
309 let manager = NonceManager::new();
310 let signer = SignerId::from("test_signer");
311
312 let nonce = manager.next(signer.clone()).unwrap();
313 assert!(manager.validate_local(&signer, nonce).is_err());
314 }
315
316 #[rstest]
317 fn test_fast_forward() {
318 let manager = NonceManager::new();
319 let signer = SignerId::from("test_signer");
320
321 let nonce1 = manager.next(signer.clone()).unwrap();
322
323 let future_time = TimeNonce::now_millis().0 + 10_000;
324 manager.fast_forward_to(future_time);
325
326 let nonce2 = manager.next(signer).unwrap();
327 assert!(nonce2.0 >= future_time);
328 assert!(nonce2 > nonce1); }
330
331 #[rstest]
332 #[expect(clippy::needless_collect)] fn test_concurrent_nonce_generation() {
334 let manager = Arc::new(NonceManager::new());
335 let signer = SignerId::from("concurrent_signer");
336
337 let handles: Vec<_> = (0..10)
338 .map(|_| {
339 let manager = Arc::clone(&manager);
340 let signer = signer.clone();
341 thread::spawn(move || manager.next(signer).unwrap())
342 })
343 .collect();
344
345 let mut nonces: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
346
347 nonces.sort();
348
349 for i in 1..nonces.len() {
350 assert!(nonces[i] > nonces[i - 1]);
351 }
352 }
353
354 #[rstest]
355 fn test_custom_policy() {
356 let policy = NoncePolicy::new(1000, 2000, 50);
357 let manager = NonceManager::with_policy(policy);
358
359 assert_eq!(manager.policy().past_ms, 1000);
360 assert_eq!(manager.policy().future_ms, 2000);
361 assert_eq!(manager.policy().keep_last_n, 50);
362 }
363}