nautilus_hyperliquid/signing/
nonce.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// Time-based nonce in Unix milliseconds for Hyperliquid.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30pub struct TimeNonce(pub i128);
31
32impl TimeNonce {
33    /// Create from Unix milliseconds.
34    pub fn from_millis(ms: i128) -> Self {
35        Self(ms)
36    }
37
38    /// Get as milliseconds.
39    pub fn as_millis(self) -> i128 {
40        self.0
41    }
42
43    /// Current time in milliseconds.
44    ///
45    /// # Panics
46    ///
47    /// Panics if the system time is before the Unix epoch.
48    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/// Nonce policy configuration for Hyperliquid.
63#[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/// Error types for Hyperliquid nonce validation.
91#[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/// Per-signer nonce state for Hyperliquid.
116#[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        // Always ensure we're at least at current time
134        let now = TimeNonce::now_millis().0;
135        self.next_nonce = self.next_nonce.max(now);
136
137        // Use and increment atomically to prevent reuse
138        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        // Check for replay attacks
155        if self.used_nonces.contains(&nonce) {
156            return Err(NonceError::AlreadyUsed { nonce });
157        }
158
159        // Check for monotonicity (nonce must be greater than the oldest tracked nonce)
160        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/// Thread-safe nonce manager for Hyperliquid signers.
180#[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    /// Generate the next nonce for a given signer.
202    ///
203    /// # Panics
204    ///
205    /// Panics if the internal mutex is poisoned.
206    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    /// Fast-forward all signers to a given time.
215    ///
216    /// # Panics
217    ///
218    /// Panics if the internal mutex is poisoned.
219    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    /// Validate a nonce locally without network calls.
227    ///
228    /// # Panics
229    ///
230    /// Panics if the internal mutex is poisoned.
231    pub fn validate_local(&self, signer: SignerId, nonce: TimeNonce) -> Result<()> {
232        let states = self.signer_states.lock().expect(MUTEX_POISONED);
233
234        // Always validate time window, even for new signers
235        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 signer state exists, validate against used nonces and monotonicity
256        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); // Ensure the new nonce is greater than the old one
341    }
342
343    #[rstest]
344    #[allow(clippy::needless_collect)] // Collect needed for thread handles
345    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}