world_id_primitives/
nullifier.rs1use ruint::aliases::U256;
2use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
3
4use crate::FieldElement;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct SessionNullifier {
20 nullifier: FieldElement,
22 action: FieldElement,
24}
25
26impl SessionNullifier {
27 const JSON_PREFIX: &str = "snil_";
28
29 #[must_use]
31 pub const fn new(nullifier: FieldElement, action: FieldElement) -> Self {
32 Self { nullifier, action }
33 }
34
35 #[must_use]
37 pub const fn nullifier(&self) -> FieldElement {
38 self.nullifier
39 }
40
41 #[must_use]
43 pub const fn action(&self) -> FieldElement {
44 self.action
45 }
46
47 #[must_use]
51 pub fn as_ethereum_representation(&self) -> [U256; 2] {
52 [self.nullifier.into(), self.action.into()]
53 }
54
55 pub fn from_ethereum_representation(value: [U256; 2]) -> Result<Self, String> {
60 let nullifier =
61 FieldElement::try_from(value[0]).map_err(|e| format!("invalid nullifier: {e}"))?;
62 let action =
63 FieldElement::try_from(value[1]).map_err(|e| format!("invalid action: {e}"))?;
64 Ok(Self { nullifier, action })
65 }
66
67 #[must_use]
69 pub fn to_compressed_bytes(&self) -> [u8; 64] {
70 let mut bytes = [0u8; 64];
71 bytes[..32].copy_from_slice(&self.nullifier.to_be_bytes());
72 bytes[32..].copy_from_slice(&self.action.to_be_bytes());
73 bytes
74 }
75
76 pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
81 if bytes.len() != 64 {
82 return Err(format!(
83 "Invalid length: expected 64 bytes, got {}",
84 bytes.len()
85 ));
86 }
87
88 let nullifier = FieldElement::from_be_bytes(bytes[..32].try_into().unwrap())
89 .map_err(|e| format!("invalid nullifier: {e}"))?;
90 let action = FieldElement::from_be_bytes(bytes[32..].try_into().unwrap())
91 .map_err(|e| format!("invalid action: {e}"))?;
92
93 Ok(Self { nullifier, action })
94 }
95}
96
97impl Default for SessionNullifier {
98 fn default() -> Self {
99 Self {
100 nullifier: FieldElement::ZERO,
101 action: FieldElement::ZERO,
102 }
103 }
104}
105
106impl Serialize for SessionNullifier {
107 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108 where
109 S: Serializer,
110 {
111 let bytes = self.to_compressed_bytes();
112 if serializer.is_human_readable() {
113 serializer.serialize_str(&format!("{}{}", Self::JSON_PREFIX, hex::encode(bytes)))
115 } else {
116 serializer.serialize_bytes(&bytes)
118 }
119 }
120}
121
122impl<'de> Deserialize<'de> for SessionNullifier {
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: Deserializer<'de>,
126 {
127 let bytes = if deserializer.is_human_readable() {
128 let value = String::deserialize(deserializer)?;
129 let hex_str = value.strip_prefix(Self::JSON_PREFIX).ok_or_else(|| {
130 D::Error::custom(format!(
131 "session nullifier must start with '{}'",
132 Self::JSON_PREFIX
133 ))
134 })?;
135 hex::decode(hex_str).map_err(D::Error::custom)?
136 } else {
137 Vec::deserialize(deserializer)?
138 };
139
140 Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
141 }
142}
143
144impl From<SessionNullifier> for [U256; 2] {
145 fn from(value: SessionNullifier) -> Self {
146 value.as_ethereum_representation()
147 }
148}
149
150impl From<(FieldElement, FieldElement)> for SessionNullifier {
151 fn from((nullifier, action): (FieldElement, FieldElement)) -> Self {
152 Self::new(nullifier, action)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 fn test_field_element(value: u64) -> FieldElement {
161 FieldElement::from(value)
162 }
163
164 #[test]
165 fn test_new_and_accessors() {
166 let nullifier = test_field_element(1001);
167 let action = test_field_element(42);
168 let session = SessionNullifier::new(nullifier, action);
169
170 assert_eq!(session.nullifier(), nullifier);
171 assert_eq!(session.action(), action);
172 }
173
174 #[test]
175 fn test_as_ethereum_representation() {
176 let nullifier = test_field_element(100);
177 let action = test_field_element(200);
178 let session = SessionNullifier::new(nullifier, action);
179
180 let repr = session.as_ethereum_representation();
181 assert_eq!(repr[0], U256::from(100));
182 assert_eq!(repr[1], U256::from(200));
183 }
184
185 #[test]
186 fn test_from_ethereum_representation() {
187 let repr = [U256::from(100), U256::from(200)];
188 let session = SessionNullifier::from_ethereum_representation(repr).unwrap();
189
190 assert_eq!(session.nullifier(), test_field_element(100));
191 assert_eq!(session.action(), test_field_element(200));
192 }
193
194 #[test]
195 fn test_json_roundtrip() {
196 let session = SessionNullifier::new(test_field_element(1001), test_field_element(42));
197 let json = serde_json::to_string(&session).unwrap();
198
199 assert!(json.starts_with("\"snil_"));
201 assert!(json.ends_with('"'));
202
203 let decoded: SessionNullifier = serde_json::from_str(&json).unwrap();
205 assert_eq!(session, decoded);
206 }
207
208 #[test]
209 fn test_json_format() {
210 let session = SessionNullifier::new(test_field_element(1), test_field_element(2));
211 let json = serde_json::to_string(&session).unwrap();
212
213 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
215 assert!(parsed.is_string());
216 let value = parsed.as_str().unwrap();
217 assert!(value.starts_with("snil_"));
218 }
219
220 #[test]
221 fn test_bytes_roundtrip() {
222 let session = SessionNullifier::new(test_field_element(1001), test_field_element(42));
223 let bytes = session.to_compressed_bytes();
224
225 assert_eq!(bytes.len(), 64); let decoded = SessionNullifier::from_compressed_bytes(&bytes).unwrap();
228 assert_eq!(session, decoded);
229 }
230
231 #[test]
232 fn test_bytes_use_field_element_encoding() {
233 let session = SessionNullifier::new(test_field_element(1001), test_field_element(42));
234 let bytes = session.to_compressed_bytes();
235
236 let mut expected = [0u8; 64];
237 expected[..32].copy_from_slice(&session.nullifier().to_be_bytes());
238 expected[32..].copy_from_slice(&session.action().to_be_bytes());
239 assert_eq!(bytes, expected);
240 }
241
242 #[test]
243 fn test_invalid_bytes_length() {
244 let too_short = vec![0u8; 63];
245 let result = SessionNullifier::from_compressed_bytes(&too_short);
246 assert!(result.is_err());
247 assert!(result.unwrap_err().contains("Invalid length"));
248
249 let too_long = vec![0u8; 65];
250 let result = SessionNullifier::from_compressed_bytes(&too_long);
251 assert!(result.is_err());
252 assert!(result.unwrap_err().contains("Invalid length"));
253 }
254
255 #[test]
256 fn test_default() {
257 let session = SessionNullifier::default();
258 assert_eq!(session.nullifier(), FieldElement::ZERO);
259 assert_eq!(session.action(), FieldElement::ZERO);
260 }
261
262 #[test]
263 fn test_from_tuple() {
264 let nullifier = test_field_element(100);
265 let action = test_field_element(200);
266 let session: SessionNullifier = (nullifier, action).into();
267
268 assert_eq!(session.nullifier(), nullifier);
269 assert_eq!(session.action(), action);
270 }
271
272 #[test]
273 fn test_into_u256_array() {
274 let session = SessionNullifier::new(test_field_element(100), test_field_element(200));
275 let arr: [U256; 2] = session.into();
276
277 assert_eq!(arr[0], U256::from(100));
278 assert_eq!(arr[1], U256::from(200));
279 }
280}