Skip to main content

nula_core/key/
public_key.rs

1//! 32-byte BIP-340 x-only public key.
2//!
3//! NIP-01 carries public keys as a 32-byte x-only encoding (the `pubkey` field
4//! and every `p` tag). [`PublicKey`] wraps [`secp256k1::XOnlyPublicKey`] with
5//! Nostr-friendly construction, hex/serde representations, and clear errors.
6
7use std::fmt;
8use std::str::FromStr;
9
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use thiserror::Error;
12
13use crate::util::hex::{self, HexError};
14
15/// Length of a serialized public key in bytes.
16pub const PUBLIC_KEY_SIZE: usize = 32;
17
18/// Errors raised when constructing a [`PublicKey`].
19#[derive(Debug, Clone, Copy, Error)]
20#[non_exhaustive]
21pub enum PublicKeyError {
22    /// The hex representation could not be decoded.
23    #[error("invalid hex encoding: {0}")]
24    Hex(#[from] HexError),
25    /// The byte slice was not exactly [`PUBLIC_KEY_SIZE`] long.
26    #[error("invalid length: expected {PUBLIC_KEY_SIZE} bytes, got {0}")]
27    InvalidLength(usize),
28    /// The bytes did not encode a valid x-only point on secp256k1.
29    #[error("not a valid x-only public key")]
30    InvalidPoint,
31}
32
33/// 32-byte BIP-340 x-only public key.
34///
35/// `Display` and `serde` use lowercase 64-char hex.
36///
37/// # Example
38///
39/// ```
40/// use nula_core::PublicKey;
41///
42/// let pk = PublicKey::parse(
43///     "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
44/// )
45/// .unwrap();
46/// assert_eq!(pk.to_hex().len(), 64);
47/// ```
48#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct PublicKey(secp256k1::XOnlyPublicKey);
50
51impl PublicKey {
52    /// Construct from a fixed-size byte array.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`PublicKeyError::InvalidPoint`] when the bytes do not encode
57    /// a valid x-coordinate.
58    pub fn from_byte_array(bytes: [u8; PUBLIC_KEY_SIZE]) -> Result<Self, PublicKeyError> {
59        secp256k1::XOnlyPublicKey::from_byte_array(bytes)
60            .map(Self)
61            .map_err(|_| PublicKeyError::InvalidPoint)
62    }
63
64    /// Construct from a byte slice.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`PublicKeyError::InvalidLength`] when the slice is not 32
69    /// bytes long, or [`PublicKeyError::InvalidPoint`] otherwise.
70    pub fn from_slice(bytes: &[u8]) -> Result<Self, PublicKeyError> {
71        let array: [u8; PUBLIC_KEY_SIZE] = bytes
72            .try_into()
73            .map_err(|_| PublicKeyError::InvalidLength(bytes.len()))?;
74        Self::from_byte_array(array)
75    }
76
77    /// Parse from a 64-char lowercase hex string.
78    ///
79    /// # Errors
80    ///
81    /// See [`PublicKeyError`].
82    pub fn parse<S>(input: S) -> Result<Self, PublicKeyError>
83    where
84        S: AsRef<str>,
85    {
86        let bytes = hex::decode(input.as_ref())?;
87        Self::from_slice(&bytes)
88    }
89
90    /// Return the public key as raw bytes.
91    #[must_use]
92    pub fn to_byte_array(self) -> [u8; PUBLIC_KEY_SIZE] {
93        self.0.serialize()
94    }
95
96    /// Return the public key as a 64-char lowercase hex string.
97    #[must_use]
98    pub fn to_hex(self) -> String {
99        hex::encode(self.0.serialize())
100    }
101
102    /// Borrow the inner [`secp256k1::XOnlyPublicKey`].
103    ///
104    /// Use this only at the boundary with the cryptography backend.
105    #[must_use]
106    pub const fn as_inner(&self) -> &secp256k1::XOnlyPublicKey {
107        &self.0
108    }
109
110    /// Verify a BIP-340 Schnorr signature against this public key.
111    ///
112    /// `message` is the 32-byte digest the signer signed (typically the
113    /// canonical NIP-01 event id). The function uses the global
114    /// `secp256k1` context and is therefore allocation-free.
115    ///
116    /// Returns `true` when the signature is valid for this public key
117    /// over `message`, `false` on every other path (invalid signature,
118    /// wrong key, malformed point at construction time is impossible
119    /// because [`PublicKey`] only holds curve-valid points).
120    #[must_use]
121    pub fn verify_schnorr(&self, message: &[u8; 32], sig: &secp256k1::schnorr::Signature) -> bool {
122        secp256k1::SECP256K1
123            .verify_schnorr(sig, message, &self.0)
124            .is_ok()
125    }
126}
127
128impl fmt::Debug for PublicKey {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.debug_tuple("PublicKey").field(&self.to_hex()).finish()
131    }
132}
133
134impl fmt::Display for PublicKey {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        hex::fmt_lower(self.0.serialize(), f)
137    }
138}
139
140impl fmt::LowerHex for PublicKey {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        hex::fmt_lower(self.0.serialize(), f)
143    }
144}
145
146impl FromStr for PublicKey {
147    type Err = PublicKeyError;
148
149    fn from_str(s: &str) -> Result<Self, Self::Err> {
150        Self::parse(s)
151    }
152}
153
154impl From<secp256k1::XOnlyPublicKey> for PublicKey {
155    fn from(value: secp256k1::XOnlyPublicKey) -> Self {
156        Self(value)
157    }
158}
159
160impl Serialize for PublicKey {
161    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162    where
163        S: Serializer,
164    {
165        serializer.collect_str(self)
166    }
167}
168
169impl<'de> Deserialize<'de> for PublicKey {
170    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171    where
172        D: Deserializer<'de>,
173    {
174        let raw = <&str>::deserialize(deserializer)?;
175        Self::parse(raw).map_err(serde::de::Error::custom)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use hex_literal::hex;
182
183    use super::*;
184
185    /// Generator point `G`'s x-coordinate (BIP-340 ยง Test Vectors).
186    const G_X: [u8; 32] = hex!("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798");
187
188    #[test]
189    fn parse_lowercase_hex() {
190        let lower = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
191        let pk = PublicKey::parse(lower).unwrap();
192        assert_eq!(pk.to_hex(), lower);
193    }
194
195    #[test]
196    fn from_byte_array_round_trip() {
197        let pk = PublicKey::from_byte_array(G_X).unwrap();
198        assert_eq!(pk.to_byte_array(), G_X);
199    }
200
201    #[test]
202    fn from_slice_wrong_length() {
203        let err = PublicKey::from_slice(&[0_u8; 16]).unwrap_err();
204        assert!(matches!(err, PublicKeyError::InvalidLength(16)));
205    }
206
207    #[test]
208    fn invalid_point_rejected() {
209        // The point is rejected because the curve does not contain it.
210        let bytes = hex!("0100000000000000000000000000000000000000000000000000000000000000");
211        let err = PublicKey::from_byte_array(bytes).unwrap_err();
212        assert!(matches!(err, PublicKeyError::InvalidPoint));
213    }
214
215    #[test]
216    fn display_lowercase() {
217        let pk = PublicKey::from_byte_array(G_X).unwrap();
218        let s = format!("{pk}");
219        assert_eq!(s.len(), 64);
220        assert!(
221            s.chars()
222                .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
223        );
224    }
225
226    #[test]
227    fn debug_includes_hex() {
228        let pk = PublicKey::from_byte_array(G_X).unwrap();
229        let dbg = format!("{pk:?}");
230        assert!(dbg.contains(&pk.to_hex()));
231    }
232
233    #[test]
234    fn serde_round_trip() {
235        let pk = PublicKey::from_byte_array(G_X).unwrap();
236        let json = serde_json::to_string(&pk).unwrap();
237        let parsed: PublicKey = serde_json::from_str(&json).unwrap();
238        assert_eq!(parsed, pk);
239    }
240
241    #[test]
242    fn ordering_is_lexicographic() {
243        let lhs = PublicKey::from_byte_array(hex!(
244            "0000000000000000000000000000000000000000000000000000000000000002"
245        ))
246        .unwrap();
247        let rhs = PublicKey::from_byte_array(hex!(
248            "0000000000000000000000000000000000000000000000000000000000000003"
249        ))
250        .unwrap();
251        assert!(lhs < rhs);
252    }
253
254    #[test]
255    fn verify_schnorr_round_trip() {
256        use crate::Keys;
257        let keys = Keys::parse("0000000000000000000000000000000000000000000000000000000000000003")
258            .unwrap();
259        let message = hex!("0202020202020202020202020202020202020202020202020202020202020202");
260        let sig = keys.sign_schnorr(&message);
261        // Correct key + correct message: success.
262        assert!(keys.public_key().verify_schnorr(&message, &sig));
263        // Tamper with the message: must reject.
264        let mut bad = message;
265        bad[0] ^= 0xff;
266        assert!(!keys.public_key().verify_schnorr(&bad, &sig));
267    }
268}