world_id_primitives/
nullifier.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct Nullifier {
16 pub inner: FieldElement,
18}
19
20impl Nullifier {
21 const PREFIX: &str = "nil_";
22 const ENCODING_LENGTH: usize = 64;
23
24 pub const fn new(nullifier: FieldElement) -> Self {
26 Self { inner: nullifier }
27 }
28
29 pub fn as_number(&self) -> U256 {
33 self.inner.into()
34 }
35
36 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 format!(
56 "{}{}{value}",
57 Self::PREFIX,
58 "0".repeat(Self::ENCODING_LENGTH - value.len())
59 )
60 }
61
62 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 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 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 let non_canonical = [
312 "nil_000000000000000000000000000000000000000000000000000000000000000A", "nil_a", "nil_0a", "nil_0A", "nil_00000000000000000000000000000000000000000000000000000000000000a", "nil_0000000000000000000000000000000000000000000000000000000000000000a", ];
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 let json = "\"nil_000000000000000000000000000000000000000000000000000000000000000A\"";
354 assert!(serde_json::from_str::<Nullifier>(json).is_err());
355
356 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}