Skip to main content

world_id_primitives/
nullifier.rs

1use std::{fmt::Display, ops::Deref, str::FromStr};
2
3use ark_babyjubjub::Fq;
4use ruint::aliases::U256;
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
6
7use crate::{FieldElement, PrimitiveError};
8
9/// A nullifier is a unique, one-time identifier derived from (user, rpId, action) that lets RPs detect
10/// duplicate actions without learning who the user is. Used with the contract's `verify()` function.
11///
12/// Internally, this is a thin wrapper to identify explicitly a single _nullifier_. This wrapper is
13/// used to expose explicit canonical serialization which is critical for uniqueness.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct Nullifier {
16    /// The `FieldElement` representing the nullifier.
17    pub inner: FieldElement,
18}
19
20impl Nullifier {
21    const PREFIX: &str = "nil_";
22    const ENCODING_LENGTH: usize = 64;
23
24    /// Initializes a new [`Nullifier`] from a [`FieldElement`]
25    pub const fn new(nullifier: FieldElement) -> Self {
26        Self { inner: nullifier }
27    }
28
29    /// Outputs the nullifier as a number. This is the **recommended way of enforcing nullifier uniqueness**.
30    ///
31    /// Store this number directly to enforce uniqueness.
32    pub fn as_number(&self) -> U256 {
33        self.inner.into()
34    }
35
36    /// Serializes a nullifier in a canonical string representation.
37    ///
38    /// It is generally safe to do uniqueness on nullifiers treating them as strings if you always serialize
39    /// them with this method. However, storing nullifiers as numbers instead is recommended.
40    ///
41    /// # Warning
42    /// Using a canonical representation is particularly important for nullifiers. Otherwise, different strings
43    /// may actually represent the same field elements, which could result in a compromise of uniqueness.
44    ///
45    /// # Details
46    /// In particular, this method adds an explicit prefix, serializes the field element to a 32-byte hex padded
47    /// string with only lowercase characters.
48    pub fn to_canonical_string(&self) -> String {
49        let value = self
50            .inner
51            .to_string()
52            .trim_start_matches("0x")
53            .to_lowercase();
54        // len is safe because for all the hex charset, each uses 1 byte
55        format!(
56            "{}{}{value}",
57            Self::PREFIX,
58            "0".repeat(Self::ENCODING_LENGTH - value.len())
59        )
60    }
61
62    /// Deserializes a nullifier from a canonical string representation. In particular,
63    /// this method will enforce all the required rules to ensure the value was canonically serialized.
64    ///
65    /// For example, the following string representations are equivalently the same field element: `0xa`, `0xA`, `0x0A`,
66    /// this method will ensure a single representation exists for each field element.
67    ///
68    /// # Errors
69    /// Will return an error if any of the encoding conditions failed (e.g. invalid characters, invalid length, etc.)
70    pub fn from_canonical_string(nullifier: String) -> Result<Self, PrimitiveError> {
71        let nullifier = nullifier.strip_prefix(Self::PREFIX).ok_or_else(|| {
72            PrimitiveError::Deserialization(format!(
73                "nullifier must start with the {}",
74                Self::PREFIX
75            ))
76        })?;
77
78        if nullifier
79            .chars()
80            .any(|c| !c.is_ascii_hexdigit() || c.is_ascii_uppercase())
81        {
82            return Err(PrimitiveError::Deserialization(
83                "nullifier has invalid characters. only lowercase hex characters allowed."
84                    .to_string(),
85            ));
86        }
87
88        if nullifier.len() != Self::ENCODING_LENGTH {
89            return Err(PrimitiveError::Deserialization(format!(
90                "nullifier does not have the right length. length: {}",
91                nullifier.len()
92            )));
93        }
94
95        let nullifier = FieldElement::from_str(nullifier)?;
96
97        Ok(Self { inner: nullifier })
98    }
99}
100
101impl Serialize for Nullifier {
102    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103    where
104        S: Serializer,
105    {
106        if serializer.is_human_readable() {
107            serializer.serialize_str(&self.to_canonical_string())
108        } else {
109            // `to_be_bytes()` is guaranteed to return 32 bytes
110            serializer.serialize_bytes(&self.inner.to_be_bytes())
111        }
112    }
113}
114
115impl<'de> Deserialize<'de> for Nullifier {
116    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117    where
118        D: Deserializer<'de>,
119    {
120        if deserializer.is_human_readable() {
121            let value = String::deserialize(deserializer)?;
122            Self::from_canonical_string(value).map_err(|e| D::Error::custom(e.to_string()))
123        } else {
124            let bytes = Vec::deserialize(deserializer)?;
125            let bytes: [u8; 32] = bytes
126                .try_into()
127                .map_err(|_| D::Error::custom("expected 32 bytes"))?;
128            let nullifier = FieldElement::from_be_bytes(&bytes)
129                .map_err(|_| D::Error::custom("invalid field element"))?;
130            Ok(Self { inner: nullifier })
131        }
132    }
133}
134
135impl Display for Nullifier {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        self.to_canonical_string().fmt(f)
138    }
139}
140
141impl From<Nullifier> for FieldElement {
142    fn from(value: Nullifier) -> Self {
143        value.inner
144    }
145}
146
147impl From<FieldElement> for Nullifier {
148    fn from(value: FieldElement) -> Self {
149        Self { inner: value }
150    }
151}
152
153impl From<Fq> for Nullifier {
154    fn from(value: Fq) -> Self {
155        Self {
156            inner: FieldElement::from(value),
157        }
158    }
159}
160
161impl From<Nullifier> for U256 {
162    fn from(value: Nullifier) -> Self {
163        value.as_number()
164    }
165}
166
167impl Deref for Nullifier {
168    type Target = FieldElement;
169    fn deref(&self) -> &Self::Target {
170        &self.inner
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use ruint::uint;
178
179    fn nil(value: u64) -> Nullifier {
180        Nullifier::new(FieldElement::from(value))
181    }
182
183    #[test]
184    fn canonical_string_roundtrip() {
185        let nullifier = nil(42);
186        let canonical = nullifier.to_canonical_string();
187        let recovered = Nullifier::from_canonical_string(canonical.clone()).unwrap();
188        assert_eq!(nullifier, recovered);
189
190        let to_string_representation = nullifier.to_string();
191        assert_eq!(to_string_representation, canonical);
192    }
193
194    #[test]
195    fn canonical_string_roundtrip_zero() {
196        let nullifier = nil(0);
197        let canonical = nullifier.to_canonical_string();
198        assert_eq!(
199            canonical,
200            "nil_0000000000000000000000000000000000000000000000000000000000000000"
201        );
202        let recovered = Nullifier::from_canonical_string(canonical).unwrap();
203        assert_eq!(nullifier, recovered);
204    }
205
206    #[test]
207    fn canonical_string_roundtrip_large_value() {
208        let fe = FieldElement::try_from(uint!(
209            0x11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2_U256
210        ))
211        .unwrap();
212        let nullifier = Nullifier::new(fe);
213        let canonical = nullifier.to_canonical_string();
214        assert_eq!(
215            canonical,
216            "nil_11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2"
217        );
218        let recovered = Nullifier::from_canonical_string(canonical).unwrap();
219        assert_eq!(nullifier, recovered);
220    }
221
222    #[test]
223    fn canonical_string_is_lowercase_and_zero_padded() {
224        let canonical = nil(0xff).to_canonical_string();
225        let hex_part = canonical.strip_prefix("nil_").unwrap();
226        assert!(
227            hex_part
228                .chars()
229                .all(|c| c.is_ascii_digit() || matches!(c, 'a'..='f'))
230        );
231        assert_eq!(hex_part.len(), 64);
232        assert!(
233            hex_part.starts_with("000000000000000000000000000000000000000000000000000000000000")
234        );
235        assert!(hex_part.ends_with("ff"));
236    }
237
238    #[test]
239    fn rejects_missing_prefix() {
240        let s = "0000000000000000000000000000000000000000000000000000000000000001";
241        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
242        assert_eq!(
243            err.to_string(),
244            "Deserialization error: nullifier must start with the nil_".to_string()
245        );
246    }
247
248    #[test]
249    fn rejects_wrong_prefix() {
250        let s = "nul_0000000000000000000000000000000000000000000000000000000000000001";
251        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
252        assert_eq!(
253            err.to_string(),
254            "Deserialization error: nullifier must start with the nil_".to_string()
255        );
256    }
257
258    #[test]
259    fn rejects_uppercase_hex() {
260        let s = "nil_000000000000000000000000000000000000000000000000000000000000000A";
261        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
262        assert_eq!(
263            err.to_string(),
264            "Deserialization error: nullifier has invalid characters. only lowercase hex characters allowed.".to_string()
265        );
266    }
267
268    #[test]
269    fn rejects_mixed_case() {
270        let s = "nil_000000000000000000000000000000000000000000000000000000000000aAbB";
271        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
272        assert_eq!(
273            err.to_string(),
274            "Deserialization error: nullifier has invalid characters. only lowercase hex characters allowed.".to_string()
275        );
276    }
277
278    #[test]
279    fn rejects_unpadded_short() {
280        // Valid field element but not zero-padded to 64 chars
281        let s = "nil_a";
282        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
283        assert_eq!(
284            err.to_string(),
285            "Deserialization error: nullifier does not have the right length. length: 1"
286                .to_string()
287        );
288    }
289
290    #[test]
291    fn rejects_too_long() {
292        let s = "nil_00000000000000000000000000000000000000000000000000000000000000001";
293        assert!(Nullifier::from_canonical_string(s.to_string()).is_err());
294    }
295
296    #[test]
297    fn rejects_non_hex_characters() {
298        let s = "nil_000000000000000000000000000000000000000000000000000000000000gggg";
299        assert!(Nullifier::from_canonical_string(s.to_string()).is_err());
300    }
301
302    #[test]
303    fn rejects_0x_prefix_inside_canonical() {
304        let s = "nil_0x0000000000000000000000000000000000000000000000000000000000000a";
305        assert!(Nullifier::from_canonical_string(s.to_string()).is_err());
306    }
307
308    #[test]
309    fn non_canonical_representations_of_same_value_rejected() {
310        // All of these represent field element 10, but only the canonical form is accepted.
311        let non_canonical = [
312            "nil_000000000000000000000000000000000000000000000000000000000000000A", // uppercase
313            "nil_a",                                                                // unpadded
314            "nil_0a", // partially padded
315            "nil_0A", // uppercase + unpadded
316            "nil_00000000000000000000000000000000000000000000000000000000000000a", // 63 chars
317            "nil_0000000000000000000000000000000000000000000000000000000000000000a", // 65 chars
318        ];
319
320        for s in non_canonical {
321            assert!(
322                Nullifier::from_canonical_string(s.to_string()).is_err(),
323                "should reject non-canonical: {s}"
324            );
325        }
326    }
327
328    #[test]
329    fn as_number_returns_inner_u256() {
330        let nullifier = nil(12345);
331        assert_eq!(nullifier.as_number(), U256::from(12345));
332    }
333
334    #[test]
335    fn json_roundtrip() {
336        let nullifier = nil(42);
337        let json = serde_json::to_string(&nullifier).unwrap();
338        let recovered: Nullifier = serde_json::from_str(&json).unwrap();
339        assert_eq!(nullifier, recovered);
340    }
341
342    #[test]
343    fn json_uses_canonical_format() {
344        let nullifier = nil(255);
345        let json = serde_json::to_string(&nullifier).unwrap();
346        let expected = format!("\"{}\"", nullifier.to_canonical_string());
347        assert_eq!(json, expected);
348    }
349
350    #[test]
351    fn json_rejects_non_canonical_input() {
352        // Valid field element, but uppercase hex
353        let json = "\"nil_000000000000000000000000000000000000000000000000000000000000000A\"";
354        assert!(serde_json::from_str::<Nullifier>(json).is_err());
355
356        // Valid field element, but no prefix
357        let json = "\"0000000000000000000000000000000000000000000000000000000000000001\"";
358        assert!(serde_json::from_str::<Nullifier>(json).is_err());
359    }
360
361    #[test]
362    fn cbor_roundtrip() {
363        let nullifier = nil(42);
364        let mut buf = Vec::new();
365        ciborium::into_writer(&nullifier, &mut buf).unwrap();
366        let recovered: Nullifier = ciborium::from_reader(&buf[..]).unwrap();
367        assert_eq!(nullifier, recovered);
368    }
369
370    #[test]
371    fn json_and_cbor_decode_to_same_value() {
372        let nullifier = nil(999);
373
374        let json = serde_json::to_string(&nullifier).unwrap();
375        let from_json: Nullifier = serde_json::from_str(&json).unwrap();
376
377        let mut cbor_buf = Vec::new();
378        ciborium::into_writer(&nullifier, &mut cbor_buf).unwrap();
379        let from_cbor: Nullifier = ciborium::from_reader(&cbor_buf[..]).unwrap();
380
381        assert_eq!(from_json, from_cbor);
382    }
383}