Skip to main content

nautilus_hyperliquid/signing/
nonce.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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,
20    time::{SystemTime, UNIX_EPOCH},
21};
22
23use parking_lot::Mutex;
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    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    /// Fast-forward all signers to a given time.
211    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    /// Validate a nonce locally without network calls.
219    pub fn validate_local(&self, signer: &SignerId, nonce: TimeNonce) -> Result<()> {
220        let states = self.signer_states.lock();
221
222        // Always validate time window, even for new signers
223        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 signer state exists, validate against used nonces and monotonicity
244        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); // Ensure the new nonce is greater than the old one
329    }
330
331    #[rstest]
332    #[expect(clippy::needless_collect)] // Collect needed for thread handles
333    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}