Skip to main content

df_hyperliquid_core/account/
signer.rs

1//! secp256k1 signatures as the venue reads them, recovery, and the [`Signer`] trait: a browser
2//! wallet through its typed-data JSON, or a local key. Without the `sign` feature a [`Signature`]
3//! can still be posted, but nothing can make or check one.
4
5#[cfg(feature = "sign")]
6use std::fmt;
7use std::future::Future;
8
9#[cfg(feature = "sign")]
10use k256::ecdsa::{RecoveryId, Signature as CurveSignature, SigningKey, VerifyingKey};
11use serde::{Serialize, Serializer, ser::SerializeStruct};
12#[cfg(feature = "sign")]
13use zeroize::{ZeroizeOnDrop, Zeroizing};
14
15use crate::utils::address::Address;
16#[cfg(feature = "sign")]
17use crate::utils::hex;
18use crate::utils::json::Json;
19use crate::utils::sign::eip712::{Domain, TypedStruct};
20#[cfg(feature = "sign")]
21use crate::utils::sign::keccak::keccak256;
22
23const V_OFFSET: u8 = 27;
24
25/// `{r, s, v}`: low-s, `v = 27 + recovery id`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Signature {
28    r: [u8; 32],
29    s: [u8; 32],
30    v: u8,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
34pub enum SignError {
35    #[error("the key could not sign this digest")]
36    Sign,
37    #[error("a signature is 65 bytes of hex")]
38    NotASignature,
39    #[error("the signature components are not a valid curve point")]
40    Malformed,
41    #[error("no public key recovers from this signature")]
42    Unrecoverable,
43    #[error("`{0}` is not an Ethereum recovery parameter; expected 27 or 28")]
44    BadV(u8),
45    #[error("the signature is from {recovered}, not {expected}")]
46    WrongSigner { expected: Address, recovered: Address },
47    #[error("the key is not a valid secp256k1 private key")]
48    NotAKey,
49}
50
51impl Signature {
52    #[must_use]
53    pub const fn from_parts(r: [u8; 32], s: [u8; 32], v: u8) -> Self {
54        Signature { r, s, v }
55    }
56
57    /// What a wallet's `eth_signTypedData_v4` answers: `0x` and 65 bytes,
58    /// `r || s || v`. `v` may be 0/1 or 27/28 and comes out 27/28; a high `s`
59    /// is folded to its low twin, which is the same signature to the curve and
60    /// the only form recovery here and the venue take.
61    #[cfg(feature = "sign")]
62    pub fn from_hex(text: &str) -> Result<Signature, SignError> {
63        let bytes: [u8; 65] = hex::decode_array(text).map_err(|_| SignError::NotASignature)?;
64        let mut r = [0u8; 32];
65        let mut s = [0u8; 32];
66        r.copy_from_slice(&bytes[..32]);
67        s.copy_from_slice(&bytes[32..64]);
68        let parity = match bytes[64] {
69            0 | 27 => 0,
70            1 | 28 => 1,
71            other => return Err(SignError::BadV(other)),
72        };
73        let compact = Signature { r, s, v: V_OFFSET + parity }.to_compact();
74        let curve = CurveSignature::from_slice(&compact).map_err(|_| SignError::Malformed)?;
75        Ok(match curve.normalize_s() {
76            None => Signature { r, s, v: V_OFFSET + parity },
77            Some(low) => {
78                s.copy_from_slice(&low.to_bytes()[32..]);
79                Signature { r, s, v: V_OFFSET + (parity ^ 1) }
80            }
81        })
82    }
83
84    #[must_use]
85    pub const fn r(&self) -> &[u8; 32] {
86        &self.r
87    }
88
89    #[must_use]
90    pub const fn s(&self) -> &[u8; 32] {
91        &self.s
92    }
93
94    /// 27 or 28.
95    #[must_use]
96    pub const fn v(&self) -> u8 {
97        self.v
98    }
99
100    #[must_use]
101    pub const fn recovery_id(&self) -> Option<u8> {
102        self.v.checked_sub(V_OFFSET)
103    }
104
105    #[must_use]
106    pub fn r_hex(&self) -> String {
107        crate::utils::hex::encode_prefixed(&self.r)
108    }
109
110    #[must_use]
111    pub fn s_hex(&self) -> String {
112        crate::utils::hex::encode_prefixed(&self.s)
113    }
114
115    #[must_use]
116    pub fn to_compact(&self) -> [u8; 64] {
117        let mut bytes = [0u8; 64];
118        bytes[..32].copy_from_slice(&self.r);
119        bytes[32..].copy_from_slice(&self.s);
120        bytes
121    }
122
123    /// `{"r":"0x..","s":"0x..","v":27}`, as `/exchange` reads it: `r` and `s`
124    /// as full 32-byte words.
125    #[must_use]
126    pub fn to_json(&self) -> String {
127        self.json().compact()
128    }
129
130    pub(crate) fn json(&self) -> Json {
131        Json::Object(vec![
132            ("r".to_owned(), Json::String(self.r_hex())),
133            ("s".to_owned(), Json::String(self.s_hex())),
134            ("v".to_owned(), Json::Number(self.v.to_string())),
135        ])
136    }
137}
138
139impl Serialize for Signature {
140    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
141        let mut signature = serializer.serialize_struct("Signature", 3)?;
142        signature.serialize_field("r", &self.r_hex())?;
143        signature.serialize_field("s", &self.s_hex())?;
144        signature.serialize_field("v", &self.v)?;
145        signature.end()
146    }
147}
148
149/// The address that signed `digest`.
150#[cfg(feature = "sign")]
151pub fn recover(digest: &[u8; 32], signature: &Signature) -> Result<Address, SignError> {
152    let recovery_id = signature.recovery_id().and_then(RecoveryId::from_byte).ok_or(SignError::BadV(signature.v))?;
153    let curve_signature = CurveSignature::from_slice(&signature.to_compact()).map_err(|_| SignError::Malformed)?;
154    let key = VerifyingKey::recover_from_prehash(digest, &curve_signature, recovery_id)
155        .map_err(|_| SignError::Unrecoverable)?;
156    Ok(address_of_public_key(&key))
157}
158
159/// Recovers `signature` and requires `expected`: what every wallet signature
160/// passes before it is posted, since a wallet can sign as another account.
161#[cfg(feature = "sign")]
162pub fn verify(digest: &[u8; 32], signature: &Signature, expected: Address) -> Result<(), SignError> {
163    let recovered = recover(digest, signature)?;
164    match recovered == expected {
165        true => Ok(()),
166        false => Err(SignError::WrongSigner { expected, recovered }),
167    }
168}
169
170/// `keccak256(uncompressed public key without its 0x04 tag)`, last twenty bytes.
171#[cfg(feature = "sign")]
172#[must_use]
173pub fn address_of_public_key(key: &VerifyingKey) -> Address {
174    let point = key.to_encoded_point(false);
175    let coordinates = &point.as_bytes()[1..];
176    let hash = keccak256(coordinates);
177    let mut bytes = [0u8; 20];
178    bytes.copy_from_slice(&hash[12..]);
179    Address::from_bytes(bytes)
180}
181
182/// Whoever signs for an account. In the browser that is the wallet, handed
183/// [`TypedStruct::wallet_json`]; in tests and native tools, a [`LocalKey`].
184/// No `Send` bound: the browser is single-threaded.
185pub trait Signer {
186    fn address(&self) -> Address;
187
188    fn sign_typed(&self, domain: &Domain, data: &TypedStruct<'_>)
189    -> impl Future<Output = Result<Signature, SignError>>;
190}
191
192/// If `k256` ever stops wiping its key on drop, this stops compiling.
193#[cfg(feature = "sign")]
194const _: fn() = || {
195    fn wiped_on_drop<T: ZeroizeOnDrop>() {}
196    wiped_on_drop::<SigningKey>();
197};
198
199/// A private key held in memory, for tests and native tools. The browser
200/// never holds one: its wallet signs.
201#[cfg(feature = "sign")]
202pub struct LocalKey {
203    key: SigningKey,
204    address: Address,
205}
206
207#[cfg(feature = "sign")]
208impl LocalKey {
209    pub fn from_bytes(secret: &[u8; 32]) -> Result<Self, SignError> {
210        let key = SigningKey::from_bytes(secret.into()).map_err(|_| SignError::NotAKey)?;
211        let address = address_of_public_key(key.verifying_key());
212        Ok(LocalKey { key, address })
213    }
214
215    /// Thirty-two bytes of hex, `0x` optional. A rejected key is never quoted back.
216    pub fn from_hex(secret: &str) -> Result<Self, SignError> {
217        let mut bytes = Zeroizing::new([0u8; 32]);
218        hex::decode_into(secret, bytes.as_mut_slice()).map_err(|_| SignError::NotAKey)?;
219        LocalKey::from_bytes(&bytes)
220    }
221
222    #[must_use]
223    pub const fn address(&self) -> Address {
224        self.address
225    }
226
227    /// RFC 6979, so the same digest always signs the same; low-s.
228    pub fn sign_digest(&self, digest: &[u8; 32]) -> Result<Signature, SignError> {
229        let (signature, recovery_id): (CurveSignature, RecoveryId) =
230            self.key.sign_prehash_recoverable(digest).map_err(|_| SignError::Sign)?;
231        let bytes = signature.to_bytes();
232        let mut r = [0u8; 32];
233        let mut s = [0u8; 32];
234        r.copy_from_slice(&bytes[..32]);
235        s.copy_from_slice(&bytes[32..]);
236        Ok(Signature { r, s, v: recovery_id.to_byte() + V_OFFSET })
237    }
238}
239
240#[cfg(feature = "sign")]
241impl Signer for LocalKey {
242    fn address(&self) -> Address {
243        self.address
244    }
245
246    fn sign_typed(
247        &self,
248        domain: &Domain,
249        data: &TypedStruct<'_>,
250    ) -> impl Future<Output = Result<Signature, SignError>> {
251        std::future::ready(self.sign_digest(&data.digest(domain)))
252    }
253}
254
255#[cfg(feature = "sign")]
256impl fmt::Debug for LocalKey {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        f.debug_struct("LocalKey").field("address", &self.address.to_hex()).finish_non_exhaustive()
259    }
260}
261
262/// Holds because `SigningKey` wipes itself; the assertion above checks that.
263#[cfg(feature = "sign")]
264impl ZeroizeOnDrop for LocalKey {}
265
266#[cfg(test)]
267#[cfg(feature = "sign")]
268#[path = "tests/signer.rs"]
269mod tests;