Skip to main content

world_id_primitives/
nullifier.rs

1use std::{fmt::Display, ops::Deref, str::FromStr};
2
3use ruint::aliases::U256;
4use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
5
6use crate::{FieldElement, PrimitiveError};
7
8/// A nullifier is a unique, one-time identifier derived from (user, rpId, action) that lets RPs detect
9/// duplicate actions without learning who the user is. Used with the contract's `verify()` function.
10///
11/// Internally, this is a thin wrapper to identify explicitly a single _nullifier_. This wrapper is
12/// used to expose explicit canonical serialization which is critical for uniqueness.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct Nullifier {
15    /// The `FieldElement` representing the nullifier.
16    pub inner: FieldElement,
17}
18
19impl Nullifier {
20    const PREFIX: &str = "nil_";
21    const ENCODING_LENGTH: usize = 64;
22
23    /// Initializes a new [`Nullifier`] from a [`FieldElement`]
24    pub const fn new(nullifier: FieldElement) -> Self {
25        Self { inner: nullifier }
26    }
27
28    /// Outputs the nullifier as a number. This is the **recommended way of enforcing nullifier uniqueness**.
29    ///
30    /// Store this number directly to enforce uniqueness.
31    pub fn as_number(&self) -> U256 {
32        self.inner.into()
33    }
34
35    /// Serializes a nullifier in a canonical string representation.
36    ///
37    /// It is generally safe to do uniqueness on nullifiers treating them as strings if you always serialize
38    /// them with this method. However, storing nullifiers as numbers instead is recommended.
39    ///
40    /// # Warning
41    /// Using a canonical representation is particularly important for nullifiers. Otherwise, different strings
42    /// may actually represent the same field elements, which could result in a compromise of uniqueness.
43    ///
44    /// # Details
45    /// In particular, this method adds an explicit prefix, serializes the field element to a 32-byte hex padded
46    /// string with only lowercase characters.
47    pub fn to_canonical_string(&self) -> String {
48        let value = self
49            .inner
50            .to_string()
51            .trim_start_matches("0x")
52            .to_lowercase();
53        // len is safe because for all the hex charset, each uses 1 byte
54        format!(
55            "{}{}{value}",
56            Self::PREFIX,
57            "0".repeat(Self::ENCODING_LENGTH - value.len())
58        )
59    }
60
61    /// Deserializes a nullifier from a canonical string representation. In particular,
62    /// this method will enforce all the required rules to ensure the value was canonically serialized.
63    ///
64    /// For example, the following string representations are equivalently the same field element: `0xa`, `0xA`, `0x0A`,
65    /// this method will ensure a single representation exists for each field element.
66    ///
67    /// # Errors
68    /// Will return an error if any of the encoding conditions failed (e.g. invalid characters, invalid length, etc.)
69    pub fn from_canonical_string(nullifier: String) -> Result<Self, PrimitiveError> {
70        let nullifier = nullifier.strip_prefix(Self::PREFIX).ok_or_else(|| {
71            PrimitiveError::Deserialization(format!(
72                "nullifier must start with the {}",
73                Self::PREFIX
74            ))
75        })?;
76
77        if nullifier
78            .chars()
79            .any(|c| !c.is_ascii_hexdigit() || c.is_ascii_uppercase())
80        {
81            return Err(PrimitiveError::Deserialization(
82                "nullifier has invalid characters. only lowercase hex characters allowed."
83                    .to_string(),
84            ));
85        }
86
87        if nullifier.len() != Self::ENCODING_LENGTH {
88            return Err(PrimitiveError::Deserialization(format!(
89                "nullifier does not have the right length. length: {}",
90                nullifier.len()
91            )));
92        }
93
94        let nullifier = FieldElement::from_str(nullifier)?;
95
96        Ok(Self { inner: nullifier })
97    }
98}
99
100impl Serialize for Nullifier {
101    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102    where
103        S: Serializer,
104    {
105        if serializer.is_human_readable() {
106            serializer.serialize_str(&self.to_canonical_string())
107        } else {
108            // `to_be_bytes()` is guaranteed to return 32 bytes
109            serializer.serialize_bytes(&self.inner.to_be_bytes())
110        }
111    }
112}
113
114impl<'de> Deserialize<'de> for Nullifier {
115    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
116    where
117        D: Deserializer<'de>,
118    {
119        if deserializer.is_human_readable() {
120            let value = String::deserialize(deserializer)?;
121            Self::from_canonical_string(value).map_err(|e| D::Error::custom(e.to_string()))
122        } else {
123            let bytes = Vec::deserialize(deserializer)?;
124            let bytes: [u8; 32] = bytes
125                .try_into()
126                .map_err(|_| D::Error::custom("expected 32 bytes"))?;
127            let nullifier = FieldElement::from_be_bytes(&bytes)
128                .map_err(|_| D::Error::custom("invalid field element"))?;
129            Ok(Self { inner: nullifier })
130        }
131    }
132}
133
134impl Display for Nullifier {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        self.to_canonical_string().fmt(f)
137    }
138}
139
140impl From<Nullifier> for FieldElement {
141    fn from(value: Nullifier) -> Self {
142        value.inner
143    }
144}
145
146impl From<FieldElement> for Nullifier {
147    fn from(value: FieldElement) -> Self {
148        Self { inner: value }
149    }
150}
151
152impl From<Nullifier> for U256 {
153    fn from(value: Nullifier) -> Self {
154        value.as_number()
155    }
156}
157
158impl Deref for Nullifier {
159    type Target = FieldElement;
160    fn deref(&self) -> &Self::Target {
161        &self.inner
162    }
163}
164
165/// A session nullifier for World ID Session proofs.
166///
167/// They are an adaptation that reuses the same proof system inputs for session flows:
168/// - the nullifier component lets RPs detect replayed submissions for the same proof context
169/// - the action component is randomized for session verification semantics
170///
171/// Together they include:
172/// - the nullifier used as the proof output
173/// - a random action bound to the same proof
174///
175/// The `WorldIDVerifier.sol` contract expects this as a `uint256[2]` array
176/// use `as_ethereum_representation()` for conversion.
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
178pub struct SessionNullifier {
179    /// The nullifier value for this proof.
180    nullifier: FieldElement,
181    /// The random action value bound to this session proof.
182    action: FieldElement,
183}
184
185impl SessionNullifier {
186    const JSON_PREFIX: &str = "snil_";
187
188    /// Creates a new session nullifier.
189    #[must_use]
190    pub const fn new(nullifier: FieldElement, action: FieldElement) -> Self {
191        Self { nullifier, action }
192    }
193
194    /// Returns the nullifier value.
195    #[must_use]
196    pub const fn nullifier(&self) -> FieldElement {
197        self.nullifier
198    }
199
200    /// Returns the action value.
201    #[must_use]
202    pub const fn action(&self) -> FieldElement {
203        self.action
204    }
205
206    /// Returns the session nullifier as an Ethereum-compatible array for `verifySession()`.
207    ///
208    /// Format: `[nullifier, action]` matching the contract's `uint256[2] sessionNullifier`.
209    #[must_use]
210    pub fn as_ethereum_representation(&self) -> [U256; 2] {
211        [self.nullifier.into(), self.action.into()]
212    }
213
214    /// Creates a session nullifier from an Ethereum representation.
215    ///
216    /// # Errors
217    /// Returns an error if the U256 values are not valid field elements.
218    pub fn from_ethereum_representation(value: [U256; 2]) -> Result<Self, String> {
219        let nullifier =
220            FieldElement::try_from(value[0]).map_err(|e| format!("invalid nullifier: {e}"))?;
221        let action =
222            FieldElement::try_from(value[1]).map_err(|e| format!("invalid action: {e}"))?;
223        Ok(Self { nullifier, action })
224    }
225
226    /// Returns the 64-byte big-endian representation (2 x 32-byte field elements).
227    #[must_use]
228    pub fn to_compressed_bytes(&self) -> [u8; 64] {
229        let mut bytes = [0u8; 64];
230        bytes[..32].copy_from_slice(&self.nullifier.to_be_bytes());
231        bytes[32..].copy_from_slice(&self.action.to_be_bytes());
232        bytes
233    }
234
235    /// Constructs from compressed bytes (must be exactly 64 bytes).
236    ///
237    /// # Errors
238    /// Returns an error if the input is not exactly 64 bytes or if values are not valid field elements.
239    pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
240        if bytes.len() != 64 {
241            return Err(format!(
242                "Invalid length: expected 64 bytes, got {}",
243                bytes.len()
244            ));
245        }
246
247        let nullifier = FieldElement::from_be_bytes(bytes[..32].try_into().unwrap())
248            .map_err(|e| format!("invalid nullifier: {e}"))?;
249        let action = FieldElement::from_be_bytes(bytes[32..].try_into().unwrap())
250            .map_err(|e| format!("invalid action: {e}"))?;
251
252        Ok(Self { nullifier, action })
253    }
254}
255
256impl Default for SessionNullifier {
257    fn default() -> Self {
258        Self {
259            nullifier: FieldElement::ZERO,
260            action: FieldElement::ZERO,
261        }
262    }
263}
264
265impl Serialize for SessionNullifier {
266    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
267    where
268        S: Serializer,
269    {
270        let bytes = self.to_compressed_bytes();
271        if serializer.is_human_readable() {
272            // JSON: prefixed hex-encoded compressed bytes for explicit typing.
273            serializer.serialize_str(&format!("{}{}", Self::JSON_PREFIX, hex::encode(bytes)))
274        } else {
275            // Binary: compressed bytes
276            serializer.serialize_bytes(&bytes)
277        }
278    }
279}
280
281impl<'de> Deserialize<'de> for SessionNullifier {
282    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
283    where
284        D: Deserializer<'de>,
285    {
286        let bytes = if deserializer.is_human_readable() {
287            let value = String::deserialize(deserializer)?;
288            let hex_str = value.strip_prefix(Self::JSON_PREFIX).ok_or_else(|| {
289                D::Error::custom(format!(
290                    "session nullifier must start with '{}'",
291                    Self::JSON_PREFIX
292                ))
293            })?;
294            hex::decode(hex_str).map_err(D::Error::custom)?
295        } else {
296            Vec::deserialize(deserializer)?
297        };
298
299        Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
300    }
301}
302
303impl From<SessionNullifier> for [U256; 2] {
304    fn from(value: SessionNullifier) -> Self {
305        value.as_ethereum_representation()
306    }
307}
308
309impl From<(FieldElement, FieldElement)> for SessionNullifier {
310    fn from((nullifier, action): (FieldElement, FieldElement)) -> Self {
311        Self::new(nullifier, action)
312    }
313}
314
315#[cfg(test)]
316mod nullifier_tests {
317    use super::*;
318    use ruint::uint;
319
320    fn nil(value: u64) -> Nullifier {
321        Nullifier::new(FieldElement::from(value))
322    }
323
324    #[test]
325    fn canonical_string_roundtrip() {
326        let nullifier = nil(42);
327        let canonical = nullifier.to_canonical_string();
328        let recovered = Nullifier::from_canonical_string(canonical.clone()).unwrap();
329        assert_eq!(nullifier, recovered);
330
331        let to_string_representation = nullifier.to_string();
332        assert_eq!(to_string_representation, canonical);
333    }
334
335    #[test]
336    fn canonical_string_roundtrip_zero() {
337        let nullifier = nil(0);
338        let canonical = nullifier.to_canonical_string();
339        assert_eq!(
340            canonical,
341            "nil_0000000000000000000000000000000000000000000000000000000000000000"
342        );
343        let recovered = Nullifier::from_canonical_string(canonical).unwrap();
344        assert_eq!(nullifier, recovered);
345    }
346
347    #[test]
348    fn canonical_string_roundtrip_large_value() {
349        let fe = FieldElement::try_from(uint!(
350            0x11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2_U256
351        ))
352        .unwrap();
353        let nullifier = Nullifier::new(fe);
354        let canonical = nullifier.to_canonical_string();
355        assert_eq!(
356            canonical,
357            "nil_11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2"
358        );
359        let recovered = Nullifier::from_canonical_string(canonical).unwrap();
360        assert_eq!(nullifier, recovered);
361    }
362
363    #[test]
364    fn canonical_string_is_lowercase_and_zero_padded() {
365        let canonical = nil(0xff).to_canonical_string();
366        let hex_part = canonical.strip_prefix("nil_").unwrap();
367        assert!(
368            hex_part
369                .chars()
370                .all(|c| c.is_ascii_digit() || matches!(c, 'a'..='f'))
371        );
372        assert_eq!(hex_part.len(), 64);
373        assert!(
374            hex_part.starts_with("000000000000000000000000000000000000000000000000000000000000")
375        );
376        assert!(hex_part.ends_with("ff"));
377    }
378
379    #[test]
380    fn rejects_missing_prefix() {
381        let s = "0000000000000000000000000000000000000000000000000000000000000001";
382        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
383        assert_eq!(
384            err.to_string(),
385            "Deserialization error: nullifier must start with the nil_".to_string()
386        );
387    }
388
389    #[test]
390    fn rejects_wrong_prefix() {
391        let s = "nul_0000000000000000000000000000000000000000000000000000000000000001";
392        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
393        assert_eq!(
394            err.to_string(),
395            "Deserialization error: nullifier must start with the nil_".to_string()
396        );
397    }
398
399    #[test]
400    fn rejects_uppercase_hex() {
401        let s = "nil_000000000000000000000000000000000000000000000000000000000000000A";
402        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
403        assert_eq!(
404            err.to_string(),
405            "Deserialization error: nullifier has invalid characters. only lowercase hex characters allowed.".to_string()
406        );
407    }
408
409    #[test]
410    fn rejects_mixed_case() {
411        let s = "nil_000000000000000000000000000000000000000000000000000000000000aAbB";
412        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
413        assert_eq!(
414            err.to_string(),
415            "Deserialization error: nullifier has invalid characters. only lowercase hex characters allowed.".to_string()
416        );
417    }
418
419    #[test]
420    fn rejects_unpadded_short() {
421        // Valid field element but not zero-padded to 64 chars
422        let s = "nil_a";
423        let err = Nullifier::from_canonical_string(s.to_string()).unwrap_err();
424        assert_eq!(
425            err.to_string(),
426            "Deserialization error: nullifier does not have the right length. length: 1"
427                .to_string()
428        );
429    }
430
431    #[test]
432    fn rejects_too_long() {
433        let s = "nil_00000000000000000000000000000000000000000000000000000000000000001";
434        assert!(Nullifier::from_canonical_string(s.to_string()).is_err());
435    }
436
437    #[test]
438    fn rejects_non_hex_characters() {
439        let s = "nil_000000000000000000000000000000000000000000000000000000000000gggg";
440        assert!(Nullifier::from_canonical_string(s.to_string()).is_err());
441    }
442
443    #[test]
444    fn rejects_0x_prefix_inside_canonical() {
445        let s = "nil_0x0000000000000000000000000000000000000000000000000000000000000a";
446        assert!(Nullifier::from_canonical_string(s.to_string()).is_err());
447    }
448
449    #[test]
450    fn non_canonical_representations_of_same_value_rejected() {
451        // All of these represent field element 10, but only the canonical form is accepted.
452        let non_canonical = [
453            "nil_000000000000000000000000000000000000000000000000000000000000000A", // uppercase
454            "nil_a",                                                                // unpadded
455            "nil_0a", // partially padded
456            "nil_0A", // uppercase + unpadded
457            "nil_00000000000000000000000000000000000000000000000000000000000000a", // 63 chars
458            "nil_0000000000000000000000000000000000000000000000000000000000000000a", // 65 chars
459        ];
460
461        for s in non_canonical {
462            assert!(
463                Nullifier::from_canonical_string(s.to_string()).is_err(),
464                "should reject non-canonical: {s}"
465            );
466        }
467    }
468
469    #[test]
470    fn as_number_returns_inner_u256() {
471        let nullifier = nil(12345);
472        assert_eq!(nullifier.as_number(), U256::from(12345));
473    }
474
475    #[test]
476    fn json_roundtrip() {
477        let nullifier = nil(42);
478        let json = serde_json::to_string(&nullifier).unwrap();
479        let recovered: Nullifier = serde_json::from_str(&json).unwrap();
480        assert_eq!(nullifier, recovered);
481    }
482
483    #[test]
484    fn json_uses_canonical_format() {
485        let nullifier = nil(255);
486        let json = serde_json::to_string(&nullifier).unwrap();
487        let expected = format!("\"{}\"", nullifier.to_canonical_string());
488        assert_eq!(json, expected);
489    }
490
491    #[test]
492    fn json_rejects_non_canonical_input() {
493        // Valid field element, but uppercase hex
494        let json = "\"nil_000000000000000000000000000000000000000000000000000000000000000A\"";
495        assert!(serde_json::from_str::<Nullifier>(json).is_err());
496
497        // Valid field element, but no prefix
498        let json = "\"0000000000000000000000000000000000000000000000000000000000000001\"";
499        assert!(serde_json::from_str::<Nullifier>(json).is_err());
500    }
501
502    #[test]
503    fn cbor_roundtrip() {
504        let nullifier = nil(42);
505        let mut buf = Vec::new();
506        ciborium::into_writer(&nullifier, &mut buf).unwrap();
507        let recovered: Nullifier = ciborium::from_reader(&buf[..]).unwrap();
508        assert_eq!(nullifier, recovered);
509    }
510
511    #[test]
512    fn json_and_cbor_decode_to_same_value() {
513        let nullifier = nil(999);
514
515        let json = serde_json::to_string(&nullifier).unwrap();
516        let from_json: Nullifier = serde_json::from_str(&json).unwrap();
517
518        let mut cbor_buf = Vec::new();
519        ciborium::into_writer(&nullifier, &mut cbor_buf).unwrap();
520        let from_cbor: Nullifier = ciborium::from_reader(&cbor_buf[..]).unwrap();
521
522        assert_eq!(from_json, from_cbor);
523    }
524}
525
526#[cfg(test)]
527mod session_nullifier_tests {
528    use super::*;
529
530    fn test_field_element(value: u64) -> FieldElement {
531        FieldElement::from(value)
532    }
533
534    #[test]
535    fn test_new_and_accessors() {
536        let nullifier = test_field_element(1001);
537        let action = test_field_element(42);
538        let session = SessionNullifier::new(nullifier, action);
539
540        assert_eq!(session.nullifier(), nullifier);
541        assert_eq!(session.action(), action);
542    }
543
544    #[test]
545    fn test_as_ethereum_representation() {
546        let nullifier = test_field_element(100);
547        let action = test_field_element(200);
548        let session = SessionNullifier::new(nullifier, action);
549
550        let repr = session.as_ethereum_representation();
551        assert_eq!(repr[0], U256::from(100));
552        assert_eq!(repr[1], U256::from(200));
553    }
554
555    #[test]
556    fn test_from_ethereum_representation() {
557        let repr = [U256::from(100), U256::from(200)];
558        let session = SessionNullifier::from_ethereum_representation(repr).unwrap();
559
560        assert_eq!(session.nullifier(), test_field_element(100));
561        assert_eq!(session.action(), test_field_element(200));
562    }
563
564    #[test]
565    fn test_json_roundtrip() {
566        let session = SessionNullifier::new(test_field_element(1001), test_field_element(42));
567        let json = serde_json::to_string(&session).unwrap();
568
569        // Verify JSON uses the prefixed compact representation
570        assert!(json.starts_with("\"snil_"));
571        assert!(json.ends_with('"'));
572
573        // Verify roundtrip
574        let decoded: SessionNullifier = serde_json::from_str(&json).unwrap();
575        assert_eq!(session, decoded);
576    }
577
578    #[test]
579    fn test_json_format() {
580        let session = SessionNullifier::new(test_field_element(1), test_field_element(2));
581        let json = serde_json::to_string(&session).unwrap();
582
583        // Should be a prefixed compact string
584        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
585        assert!(parsed.is_string());
586        let value = parsed.as_str().unwrap();
587        assert!(value.starts_with("snil_"));
588    }
589
590    #[test]
591    fn test_bytes_roundtrip() {
592        let session = SessionNullifier::new(test_field_element(1001), test_field_element(42));
593        let bytes = session.to_compressed_bytes();
594
595        assert_eq!(bytes.len(), 64); // 32 + 32 bytes
596
597        let decoded = SessionNullifier::from_compressed_bytes(&bytes).unwrap();
598        assert_eq!(session, decoded);
599    }
600
601    #[test]
602    fn test_bytes_use_field_element_encoding() {
603        let session = SessionNullifier::new(test_field_element(1001), test_field_element(42));
604        let bytes = session.to_compressed_bytes();
605
606        let mut expected = [0u8; 64];
607        expected[..32].copy_from_slice(&session.nullifier().to_be_bytes());
608        expected[32..].copy_from_slice(&session.action().to_be_bytes());
609        assert_eq!(bytes, expected);
610    }
611
612    #[test]
613    fn test_invalid_bytes_length() {
614        let too_short = vec![0u8; 63];
615        let result = SessionNullifier::from_compressed_bytes(&too_short);
616        assert!(result.is_err());
617        assert!(result.unwrap_err().contains("Invalid length"));
618
619        let too_long = vec![0u8; 65];
620        let result = SessionNullifier::from_compressed_bytes(&too_long);
621        assert!(result.is_err());
622        assert!(result.unwrap_err().contains("Invalid length"));
623    }
624
625    #[test]
626    fn test_default() {
627        let session = SessionNullifier::default();
628        assert_eq!(session.nullifier(), FieldElement::ZERO);
629        assert_eq!(session.action(), FieldElement::ZERO);
630    }
631
632    #[test]
633    fn test_from_tuple() {
634        let nullifier = test_field_element(100);
635        let action = test_field_element(200);
636        let session: SessionNullifier = (nullifier, action).into();
637
638        assert_eq!(session.nullifier(), nullifier);
639        assert_eq!(session.action(), action);
640    }
641
642    #[test]
643    fn test_into_u256_array() {
644        let session = SessionNullifier::new(test_field_element(100), test_field_element(200));
645        let arr: [U256; 2] = session.into();
646
647        assert_eq!(arr[0], U256::from(100));
648        assert_eq!(arr[1], U256::from(200));
649    }
650}