Skip to main content

nula_core/event/
id.rs

1//! 32-byte event identifier.
2//!
3//! Per [NIP-01], an event's `id` is the SHA-256 hash of its canonical
4//! serialization:
5//!
6//! ```json
7//! [0, pubkey, created_at, kind, tags, content]
8//! ```
9//!
10//! - the JSON has *no* whitespace,
11//! - `pubkey` is lowercase 64-char hex,
12//! - `tags` is an array of arrays of strings, and
13//! - control characters in `content` are escaped per the NIP-01 rules.
14//!
15//! [`EventId::compute_from_canonical`] consumes a serializer that produces this
16//! exact bytestream. [`crate::event::Event`] composes it for users so they
17//! never have to deal with the canonical form directly.
18//!
19//! [NIP-01]: https://github.com/nostr-protocol/nips/blob/master/01.md
20
21use std::fmt;
22use std::str::FromStr;
23
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use sha2::{Digest, Sha256};
26use thiserror::Error;
27
28use crate::util::hex::{self, HexError};
29
30/// Length of an [`EventId`] in bytes.
31pub const EVENT_ID_SIZE: usize = 32;
32
33/// Errors raised when constructing an [`EventId`].
34#[derive(Debug, Clone, Copy, Error)]
35#[non_exhaustive]
36pub enum EventIdError {
37    /// The hex representation could not be decoded.
38    #[error("invalid hex encoding: {0}")]
39    Hex(#[from] HexError),
40    /// The byte slice was not exactly [`EVENT_ID_SIZE`] long.
41    #[error("invalid length: expected {EVENT_ID_SIZE} bytes, got {0}")]
42    InvalidLength(usize),
43}
44
45/// 32-byte event identifier (SHA-256 of the canonical event serialization).
46///
47/// `Display` and `serde` use lowercase 64-char hex.
48#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct EventId([u8; EVENT_ID_SIZE]);
50
51impl EventId {
52    /// Construct from a fixed-size byte array.
53    #[must_use]
54    pub const fn from_byte_array(bytes: [u8; EVENT_ID_SIZE]) -> Self {
55        Self(bytes)
56    }
57
58    /// Construct from a byte slice.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`EventIdError::InvalidLength`] when the slice is not 32 bytes.
63    pub fn from_slice(bytes: &[u8]) -> Result<Self, EventIdError> {
64        let array: [u8; EVENT_ID_SIZE] = bytes
65            .try_into()
66            .map_err(|_| EventIdError::InvalidLength(bytes.len()))?;
67        Ok(Self(array))
68    }
69
70    /// Parse from a 64-char lowercase hex string.
71    ///
72    /// # Errors
73    ///
74    /// See [`EventIdError`].
75    pub fn parse<S>(input: S) -> Result<Self, EventIdError>
76    where
77        S: AsRef<str>,
78    {
79        let bytes = hex::decode(input.as_ref())?;
80        Self::from_slice(&bytes)
81    }
82
83    /// Compute an [`EventId`] from the canonical event serialization bytes.
84    ///
85    /// The caller must produce the exact byte sequence described by NIP-01.
86    /// This function does not validate the structure; it only hashes.
87    #[must_use]
88    pub fn compute_from_canonical(canonical: &[u8]) -> Self {
89        let mut hasher = Sha256::new();
90        hasher.update(canonical);
91        let digest = hasher.finalize();
92        let mut bytes = [0_u8; EVENT_ID_SIZE];
93        bytes.copy_from_slice(&digest);
94        Self(bytes)
95    }
96
97    /// Return the 32-byte representation.
98    #[must_use]
99    pub const fn to_byte_array(self) -> [u8; EVENT_ID_SIZE] {
100        self.0
101    }
102
103    /// Borrow the 32-byte representation.
104    #[must_use]
105    pub const fn as_bytes(&self) -> &[u8; EVENT_ID_SIZE] {
106        &self.0
107    }
108
109    /// Return a 64-char lowercase hex representation.
110    #[must_use]
111    pub fn to_hex(self) -> String {
112        hex::encode(self.0)
113    }
114}
115
116impl fmt::Debug for EventId {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.debug_tuple("EventId").field(&self.to_hex()).finish()
119    }
120}
121
122impl fmt::Display for EventId {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        hex::fmt_lower(self.0, f)
125    }
126}
127
128impl fmt::LowerHex for EventId {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        hex::fmt_lower(self.0, f)
131    }
132}
133
134impl FromStr for EventId {
135    type Err = EventIdError;
136
137    fn from_str(s: &str) -> Result<Self, Self::Err> {
138        Self::parse(s)
139    }
140}
141
142impl AsRef<[u8]> for EventId {
143    fn as_ref(&self) -> &[u8] {
144        &self.0
145    }
146}
147
148impl Serialize for EventId {
149    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150    where
151        S: Serializer,
152    {
153        serializer.collect_str(self)
154    }
155}
156
157impl<'de> Deserialize<'de> for EventId {
158    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
159    where
160        D: Deserializer<'de>,
161    {
162        let raw = <&str>::deserialize(deserializer)?;
163        Self::parse(raw).map_err(serde::de::Error::custom)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use hex_literal::hex;
170
171    use super::*;
172
173    /// SHA-256 of the empty input — a known constant.
174    const SHA256_EMPTY: [u8; 32] =
175        hex!("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
176
177    #[test]
178    fn from_byte_array_round_trip() {
179        let id = EventId::from_byte_array(SHA256_EMPTY);
180        assert_eq!(id.to_byte_array(), SHA256_EMPTY);
181    }
182
183    #[test]
184    fn from_slice_wrong_length() {
185        let err = EventId::from_slice(&[0_u8; 16]).unwrap_err();
186        assert!(matches!(err, EventIdError::InvalidLength(16)));
187    }
188
189    #[test]
190    fn parse_round_trip() {
191        let lower = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
192        let id = EventId::parse(lower).unwrap();
193        assert_eq!(id.to_hex(), lower);
194    }
195
196    #[test]
197    fn parse_rejects_bad_hex() {
198        let err = EventId::parse("zzzz").unwrap_err();
199        assert!(matches!(err, EventIdError::Hex(_)));
200    }
201
202    #[test]
203    fn compute_matches_known_sha256() {
204        let id = EventId::compute_from_canonical(b"");
205        assert_eq!(id.to_byte_array(), SHA256_EMPTY);
206    }
207
208    #[test]
209    fn compute_distinct_for_distinct_inputs() {
210        let lhs = EventId::compute_from_canonical(b"alice");
211        let rhs = EventId::compute_from_canonical(b"bob");
212        assert_ne!(lhs, rhs);
213    }
214
215    #[test]
216    fn display_lowercase() {
217        let id = EventId::from_byte_array(SHA256_EMPTY);
218        let s = format!("{id}");
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 id = EventId::from_byte_array(SHA256_EMPTY);
229        let dbg = format!("{id:?}");
230        assert!(dbg.contains(&id.to_hex()));
231    }
232
233    #[test]
234    fn ordering_is_lexicographic() {
235        let lhs = EventId::from_byte_array([0_u8; 32]);
236        let rhs = EventId::from_byte_array([1_u8; 32]);
237        assert!(lhs < rhs);
238    }
239
240    #[test]
241    fn serde_round_trip() {
242        let id = EventId::from_byte_array(SHA256_EMPTY);
243        let json = serde_json::to_string(&id).unwrap();
244        assert_eq!(
245            json,
246            r#""e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855""#
247        );
248        let parsed: EventId = serde_json::from_str(&json).unwrap();
249        assert_eq!(parsed, id);
250    }
251}