Skip to main content

mina_signer/
pubkey.rs

1//! Public key structures and algorithms
2//!
3//! Definition of public key structure and helpers
4
5extern crate alloc;
6
7use crate::{BaseField, CurvePoint, ScalarField, SecKey};
8use alloc::{string::String, vec, vec::Vec};
9use ark_ec::{short_weierstrass::Affine, AffineRepr, CurveGroup};
10use ark_ff::{BigInteger, PrimeField, Zero};
11use bs58;
12use core::{
13    fmt,
14    ops::{Mul, Neg},
15};
16use o1_utils::FieldHelpers;
17use sha2::{Digest, Sha256};
18use thiserror::Error;
19
20/// Public key errors
21#[derive(Error, Debug, Clone, PartialEq, Eq)]
22pub enum PubKeyError {
23    /// Invalid address length
24    #[error("invalid address length")]
25    AddressLength,
26    /// Invalid address base58
27    #[error("invalid address base58")]
28    AddressBase58,
29    /// Invalid raw address bytes length
30    #[error("invalid raw address bytes length")]
31    AddressRawByteLength,
32    /// Invalid address checksum
33    #[error("invalid address checksum")]
34    AddressChecksum,
35    /// Invalid address version
36    #[error("invalid address version")]
37    AddressVersion,
38    /// Invalid x-coordinate bytes
39    #[error("invalid x-coordinate bytes")]
40    XCoordinateBytes,
41    /// Invalid x-coordinate
42    #[error("invalid x-coordinate")]
43    XCoordinate,
44    /// Point not on curve
45    #[error("point not on curve")]
46    YCoordinateBytes,
47    /// Invalid y-coordinate
48    #[error("invalid y-coordinate bytes")]
49    YCoordinateParityBytes,
50    /// Invalid y-coordinate parity
51    #[error("invalid y-coordinate parity bytes")]
52    YCoordinateParity,
53    /// Invalid y-coordinate parity
54    #[error("invalid y-coordinate parity")]
55    NonCurvePoint,
56    /// Invalid hex
57    #[error("invalid public key hex")]
58    Hex,
59    /// Invalid secret key
60    #[error("invalid secret key")]
61    SecKey,
62}
63/// Public key Result
64pub type Result<T> = core::result::Result<T, PubKeyError>;
65
66/// Length of Mina addresses
67pub const MINA_ADDRESS_LEN: usize = 55;
68const MINA_ADDRESS_RAW_LEN: usize = 40;
69
70/// Public key
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct PubKey(CurvePoint);
73
74impl PubKey {
75    /// Create public key from curve point
76    /// Note: Does not check point is on curve
77    #[allow(clippy::needless_pass_by_value)]
78    #[must_use]
79    pub const fn from_point_unsafe(point: CurvePoint) -> Self {
80        Self(point)
81    }
82
83    /// Deserialize public key from bytes
84    /// # Errors
85    ///
86    /// Will give error if `bytes` do not match certain requirements.
87    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
88        if bytes.len() != BaseField::size_in_bytes() * 2 {
89            return Err(PubKeyError::YCoordinateBytes);
90        }
91        let x = BaseField::from_bytes(&bytes[0..BaseField::size_in_bytes()])
92            .map_err(|_| PubKeyError::XCoordinateBytes)?;
93        let y = BaseField::from_bytes(&bytes[BaseField::size_in_bytes()..])
94            .map_err(|_| PubKeyError::YCoordinateBytes)?;
95        let pt = CurvePoint::get_point_from_x_unchecked(x, y.0.is_odd())
96            .ok_or(PubKeyError::XCoordinate)?;
97        if pt.y != y {
98            return Err(PubKeyError::NonCurvePoint);
99        }
100
101        let public = Affine {
102            x,
103            y,
104            infinity: pt.infinity,
105        };
106        if !public.is_on_curve() {
107            return Err(PubKeyError::NonCurvePoint);
108        }
109
110        // Safe now because we checked point is on the curve
111        Ok(Self::from_point_unsafe(public))
112    }
113
114    /// Deserialize public key from hex
115    ///
116    /// # Errors
117    ///
118    /// Will give error if `hex` string does not match certain requirements.
119    pub fn from_hex(public_hex: &str) -> Result<Self> {
120        let bytes: Vec<u8> = hex::decode(public_hex).map_err(|_| PubKeyError::Hex)?;
121        Self::from_bytes(&bytes)
122    }
123
124    /// Create public key from a secret key
125    ///
126    /// # Errors
127    ///
128    /// Returns [`PubKeyError::SecKey`] if the scalar is zero, or
129    /// [`PubKeyError::NonCurvePoint`] if the derived point is not on the curve.
130    pub fn from_secret_key(secret_key: &SecKey) -> Result<Self> {
131        if *secret_key.scalar() == ScalarField::zero() {
132            return Err(PubKeyError::SecKey);
133        }
134        let pt = CurvePoint::generator()
135            .mul(*secret_key.scalar())
136            .into_affine();
137        if !pt.is_on_curve() {
138            return Err(PubKeyError::NonCurvePoint);
139        }
140        Ok(Self::from_point_unsafe(pt))
141    }
142
143    /// Deserialize Mina address into public key
144    ///
145    /// # Errors
146    ///
147    /// Will give error if `address` string does not match certain requirements.
148    pub fn from_address(address: &str) -> Result<Self> {
149        if address.len() != MINA_ADDRESS_LEN {
150            return Err(PubKeyError::AddressLength);
151        }
152
153        let bytes = bs58::decode(address)
154            .into_vec()
155            .map_err(|_| PubKeyError::AddressBase58)?;
156
157        if bytes.len() != MINA_ADDRESS_RAW_LEN {
158            return Err(PubKeyError::AddressRawByteLength);
159        }
160
161        let (raw, checksum) = (&bytes[..bytes.len() - 4], &bytes[bytes.len() - 4..]);
162        let hash = Sha256::digest(&Sha256::digest(raw)[..]);
163        if checksum != &hash[..4] {
164            return Err(PubKeyError::AddressChecksum);
165        }
166
167        let (version, x_bytes, y_parity) = (
168            &raw[..3],
169            &raw[3..bytes.len() - 5],
170            raw[bytes.len() - 5] == 0x01,
171        );
172        if version != [0xcb, 0x01, 0x01] {
173            return Err(PubKeyError::AddressVersion);
174        }
175
176        let x = BaseField::from_bytes(x_bytes).map_err(|_| PubKeyError::XCoordinateBytes)?;
177        let mut pt =
178            CurvePoint::get_point_from_x_unchecked(x, y_parity).ok_or(PubKeyError::XCoordinate)?;
179
180        if pt.y.into_bigint().is_even() == y_parity {
181            pt.y = pt.y.neg();
182        }
183
184        if !pt.is_on_curve() {
185            return Err(PubKeyError::NonCurvePoint);
186        }
187
188        // Safe now because we checked point pt is on curve
189        Ok(Self::from_point_unsafe(pt))
190    }
191
192    /// Borrow public key as curve point
193    #[must_use = "borrowed curve point must be used"]
194    pub const fn point(&self) -> &CurvePoint {
195        &self.0
196    }
197
198    /// Convert public key into curve point
199    pub const fn into_point(self) -> CurvePoint {
200        self.0
201    }
202
203    /// Convert public key into compressed public key
204    #[must_use]
205    pub fn into_compressed(&self) -> CompressedPubKey {
206        let point = self.0;
207        CompressedPubKey {
208            x: point.x,
209            is_odd: point.y.into_bigint().is_odd(),
210        }
211    }
212
213    /// Serialize public key into corresponding Mina address
214    #[must_use]
215    pub fn into_address(&self) -> String {
216        let point = self.point();
217        into_address(&point.x, point.y.into_bigint().is_odd())
218    }
219
220    /// Deserialize public key into bytes
221    ///
222    /// # Panics
223    ///
224    /// Panics if the field element byte conversion fails.
225    #[must_use]
226    pub fn to_bytes(&self) -> Vec<u8> {
227        let point = self.point();
228        [point.x.to_bytes(), point.y.to_bytes()].concat()
229    }
230
231    /// Deserialize public key into hex
232    ///
233    /// # Panics
234    ///
235    /// Panics if the field element byte conversion fails.
236    #[must_use]
237    pub fn to_hex(&self) -> String {
238        let point = self.point();
239        point.x.to_hex() + point.y.to_hex().as_str()
240    }
241}
242
243impl fmt::Display for PubKey {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(f, "{}", self.to_hex())
246    }
247}
248
249/// Compressed public keys consist of x-coordinate and y-coordinate parity.
250#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
251pub struct CompressedPubKey {
252    /// X-coordinate
253    pub x: BaseField,
254
255    /// Parity of y-coordinate
256    pub is_odd: bool,
257}
258
259fn into_address(x: &BaseField, is_odd: bool) -> String {
260    let mut raw: Vec<u8> = vec![
261        0xcb, // version for base58 check
262        0x01, // non_zero_curve_point version
263        0x01, // compressed_poly version
264    ];
265
266    // pub key x-coordinate
267    raw.extend(x.to_bytes());
268
269    // pub key y-coordinate parity
270    raw.push(u8::from(is_odd));
271
272    // 4-byte checksum
273    let hash = Sha256::digest(&Sha256::digest(&raw[..])[..]);
274    raw.extend(&hash[..4]);
275
276    // The raw buffer is MINA_ADDRESS_RAW_LEN (= 40) bytes in length
277    bs58::encode(raw).into_string()
278}
279
280impl CompressedPubKey {
281    /// Serialize compressed public key into corresponding Mina address
282    #[must_use]
283    pub fn into_address(&self) -> String {
284        into_address(&self.x, self.is_odd)
285    }
286
287    /// Deserialize compressed public key from bytes
288    /// # Errors
289    ///
290    /// Will give error if `bytes` do not match certain requirements.
291    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
292        let x = BaseField::from_bytes(&bytes[0..BaseField::size_in_bytes()])
293            .map_err(|_| PubKeyError::XCoordinateBytes)?;
294        let parity_bytes = &bytes[BaseField::size_in_bytes()..];
295        if parity_bytes.len() != 1 {
296            return Err(PubKeyError::YCoordinateParityBytes);
297        }
298        let is_odd = if parity_bytes[0] == 0x01 {
299            true // Odd
300        } else if parity_bytes[0] == 0x00 {
301            false // Even
302        } else {
303            return Err(PubKeyError::YCoordinateParity);
304        };
305        let public =
306            CurvePoint::get_point_from_x_unchecked(x, is_odd).ok_or(PubKeyError::XCoordinate)?;
307        if !public.is_on_curve() {
308            return Err(PubKeyError::NonCurvePoint);
309        }
310
311        // Safe now because we checked point is on the curve
312        Ok(Self { x, is_odd })
313    }
314
315    /// Deserialize compressed public key from hex
316    ///
317    /// # Errors
318    ///
319    /// Will give error if `hex` string does not match certain requirements.
320    pub fn from_hex(public_hex: &str) -> Result<Self> {
321        let bytes: Vec<u8> = hex::decode(public_hex).map_err(|_| PubKeyError::Hex)?;
322        Self::from_bytes(&bytes)
323    }
324
325    /// Create compressed public key from a secret key
326    #[must_use]
327    pub fn from_secret_key(sec_key: SecKey) -> Self {
328        // We do not need to check point is on the curve, since it's derived
329        // directly from the generator point
330        let public = PubKey::from_point_unsafe(
331            CurvePoint::generator()
332                .mul(sec_key.into_scalar())
333                .into_affine(),
334        );
335        public.into_compressed()
336    }
337
338    /// Deserialize Mina address into compressed public key (via an uncompressed
339    /// `PubKey`)
340    ///
341    /// # Errors
342    ///
343    /// Will give error if `PubKey::from_address()` returns error.
344    pub fn from_address(address: &str) -> Result<Self> {
345        Ok(PubKey::from_address(address)?.into_compressed())
346    }
347
348    /// The empty [`CompressedPubKey`] value that is used as `public_key` in
349    /// empty account and `None` value for calculating the hash of
350    /// `Option<CompressedPubKey>`, etc.
351    #[must_use]
352    pub fn empty() -> Self {
353        Self {
354            x: BaseField::zero(),
355            is_odd: false,
356        }
357    }
358
359    /// Deserialize compressed public key into bytes
360    #[must_use]
361    pub fn to_bytes(&self) -> Vec<u8> {
362        let x_bytes = self.x.to_bytes();
363        let is_odd_bytes = vec![u8::from(self.is_odd)];
364        [x_bytes, is_odd_bytes].concat()
365    }
366
367    /// Deserialize compressed public key into hex
368    #[must_use]
369    pub fn to_hex(&self) -> String {
370        hex::encode(self.to_bytes())
371    }
372}