1use crate::{
2 FieldElement, PrimitiveError,
3 oprf::{OprfPrefix, OprfPrefixedFieldElement as _},
4 poseidon::{self, ds},
5};
6use embed_doc_image::embed_doc_image;
7use ruint::aliases::U256;
8use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
9
10#[embed_doc_image("session-proofs.png", "assets/session-proofs.png")]
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct SessionId {
26 pub commitment: FieldElement,
31 pub oprf_seed: FieldElement,
53}
54
55impl SessionId {
56 const JSON_PREFIX: &str = "session_";
57
58 pub fn new(commitment: FieldElement, oprf_seed: FieldElement) -> Result<Self, PrimitiveError> {
63 if !oprf_seed.has_prefix(OprfPrefix::SessionOprfSeed) {
67 return Err(PrimitiveError::InvalidInput {
68 attribute: "session_id".to_string(),
69 reason: "inner oprf_seed is not valid".to_string(),
70 });
71 }
72 Ok(Self {
73 commitment,
74 oprf_seed,
75 })
76 }
77
78 pub fn from_r_seed(
91 leaf_index: u64,
92 session_id_r_seed: FieldElement,
93 oprf_seed: FieldElement,
94 ) -> Result<Self, PrimitiveError> {
95 if !oprf_seed.has_prefix(OprfPrefix::SessionOprfSeed) {
96 return Err(PrimitiveError::InvalidInput {
97 attribute: "session_id".to_string(),
98 reason: "inner oprf_seed is not valid".to_string(),
99 });
100 }
101
102 let commitment = poseidon::hash(
103 ds::SESSION_COMMITMENT,
104 [leaf_index.into(), session_id_r_seed],
105 );
106 Ok(Self {
107 commitment,
108 oprf_seed,
109 })
110 }
111
112 pub fn generate_oprf_seed<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> FieldElement {
114 FieldElement::random_with_prefix(rng, OprfPrefix::SessionOprfSeed)
115 }
116
117 pub fn verify_commitment(
126 &self,
127 leaf_index: u64,
128 session_id_r_seed: FieldElement,
129 ) -> Result<(), PrimitiveError> {
130 let computed = Self::from_r_seed(leaf_index, session_id_r_seed, self.oprf_seed)?;
131 if computed.commitment != self.commitment {
132 return Err(PrimitiveError::SessionIdCommitmentMismatch);
133 }
134 Ok(())
135 }
136
137 #[must_use]
139 pub fn to_compressed_bytes(&self) -> [u8; 64] {
140 let mut bytes = [0u8; 64];
141 bytes[..32].copy_from_slice(&self.commitment.to_be_bytes());
142 bytes[32..].copy_from_slice(&self.oprf_seed.to_be_bytes());
143 bytes
144 }
145
146 pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
151 if bytes.len() != 64 {
152 return Err(format!(
153 "Invalid length: expected 64 bytes, got {}",
154 bytes.len()
155 ));
156 }
157
158 let commitment = FieldElement::from_be_bytes(bytes[..32].try_into().unwrap())
159 .map_err(|e| format!("invalid commitment: {e}"))?;
160 let oprf_seed = FieldElement::from_be_bytes(bytes[32..].try_into().unwrap())
161 .map_err(|e| format!("invalid oprf_seed: {e}"))?;
162
163 if bytes[32] != OprfPrefix::SessionOprfSeed as u8 {
164 return Err("invalid prefix for oprf_seed".to_string());
165 }
166
167 Ok(Self {
168 commitment,
169 oprf_seed,
170 })
171 }
172}
173
174impl Default for SessionId {
175 fn default() -> Self {
176 let mut oprf_seed = [0u8; 32];
177 oprf_seed[0] = OprfPrefix::SessionOprfSeed as u8;
178 let oprf_seed = U256::from_be_bytes(oprf_seed)
179 .try_into()
180 .expect("always fits in the field");
181 Self {
182 commitment: FieldElement::ZERO,
183 oprf_seed,
184 }
185 }
186}
187
188impl Serialize for SessionId {
189 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
190 where
191 S: Serializer,
192 {
193 let bytes = self.to_compressed_bytes();
194 if serializer.is_human_readable() {
195 serializer.serialize_str(&format!("{}{}", Self::JSON_PREFIX, hex::encode(bytes)))
197 } else {
198 serializer.serialize_bytes(&bytes)
200 }
201 }
202}
203
204impl<'de> Deserialize<'de> for SessionId {
205 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
206 where
207 D: Deserializer<'de>,
208 {
209 let bytes = if deserializer.is_human_readable() {
210 let value = String::deserialize(deserializer)?;
211 let hex_str = value.strip_prefix(Self::JSON_PREFIX).ok_or_else(|| {
212 D::Error::custom(format!(
213 "session id must start with '{}'",
214 Self::JSON_PREFIX
215 ))
216 })?;
217 hex::decode(hex_str).map_err(D::Error::custom)?
218 } else {
219 Vec::deserialize(deserializer)?
220 };
221
222 Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
231pub enum SessionRef {
232 #[default]
234 None,
235 Create,
239 Existing(SessionId),
241}
242
243impl SessionRef {
244 const CREATE_TOKEN: &str = "create";
245
246 #[must_use]
248 pub const fn is_none(&self) -> bool {
249 matches!(self, Self::None)
250 }
251
252 #[must_use]
254 pub const fn is_create(&self) -> bool {
255 matches!(self, Self::Create)
256 }
257
258 #[must_use]
260 pub const fn existing(&self) -> Option<SessionId> {
261 match self {
262 Self::Existing(id) => Some(*id),
263 _ => None,
264 }
265 }
266}
267
268impl From<SessionId> for SessionRef {
269 fn from(id: SessionId) -> Self {
270 Self::Existing(id)
271 }
272}
273
274impl Serialize for SessionRef {
275 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
276 where
277 S: Serializer,
278 {
279 match self {
280 Self::None => serializer.serialize_none(),
281 Self::Create => {
282 if serializer.is_human_readable() {
283 serializer.serialize_str(Self::CREATE_TOKEN)
284 } else {
285 serializer.serialize_bytes(Self::CREATE_TOKEN.as_bytes())
287 }
288 }
289 Self::Existing(id) => id.serialize(serializer),
290 }
291 }
292}
293
294impl<'de> Deserialize<'de> for SessionRef {
295 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
296 where
297 D: Deserializer<'de>,
298 {
299 struct SessionRefVisitor;
300
301 impl<'de> serde::de::Visitor<'de> for SessionRefVisitor {
302 type Value = SessionRef;
303
304 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
305 write!(
306 formatter,
307 "null, \"{}\", or a '{}'-prefixed session id",
308 SessionRef::CREATE_TOKEN,
309 SessionId::JSON_PREFIX
310 )
311 }
312
313 fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
314 Ok(SessionRef::None)
315 }
316
317 fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
318 Ok(SessionRef::None)
319 }
320
321 fn visit_some<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error>
322 where
323 D2: Deserializer<'de>,
324 {
325 if deserializer.is_human_readable() {
326 let value = String::deserialize(deserializer)?;
327 if value == SessionRef::CREATE_TOKEN {
328 return Ok(SessionRef::Create);
329 }
330 let hex_str = value.strip_prefix(SessionId::JSON_PREFIX).ok_or_else(|| {
331 D2::Error::custom(format!(
332 "session_id must be \"{}\" or start with '{}'",
333 SessionRef::CREATE_TOKEN,
334 SessionId::JSON_PREFIX
335 ))
336 })?;
337 let bytes = hex::decode(hex_str).map_err(D2::Error::custom)?;
338 SessionId::from_compressed_bytes(&bytes)
339 .map(SessionRef::Existing)
340 .map_err(D2::Error::custom)
341 } else {
342 let bytes = Vec::<u8>::deserialize(deserializer)?;
343 if bytes == SessionRef::CREATE_TOKEN.as_bytes() {
344 return Ok(SessionRef::Create);
345 }
346 SessionId::from_compressed_bytes(&bytes)
347 .map(SessionRef::Existing)
348 .map_err(D2::Error::custom)
349 }
350 }
351 }
352
353 deserializer.deserialize_option(SessionRefVisitor)
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
377pub struct SessionNullifier {
378 nullifier: FieldElement,
380 action: FieldElement,
382}
383
384impl SessionNullifier {
385 const JSON_PREFIX: &str = "snil_";
386
387 pub fn new(nullifier: FieldElement, action: FieldElement) -> Result<Self, PrimitiveError> {
389 if !action.has_prefix(OprfPrefix::SessionAction) {
390 return Err(PrimitiveError::InvalidInput {
391 attribute: "session_nullifier".to_string(),
392 reason: "inner action is not valid".to_string(),
393 });
394 }
395 Ok(Self { nullifier, action })
396 }
397
398 #[must_use]
400 pub const fn nullifier(&self) -> FieldElement {
401 self.nullifier
402 }
403
404 #[must_use]
406 pub const fn action(&self) -> FieldElement {
407 self.action
408 }
409
410 #[must_use]
414 pub fn as_ethereum_representation(&self) -> [U256; 2] {
415 [self.nullifier.into(), self.action.into()]
416 }
417
418 pub fn from_ethereum_representation(value: [U256; 2]) -> Result<Self, String> {
423 let nullifier =
424 FieldElement::try_from(value[0]).map_err(|e| format!("invalid nullifier: {e}"))?;
425 let action =
426 FieldElement::try_from(value[1]).map_err(|e| format!("invalid action: {e}"))?;
427
428 if !action.has_prefix(OprfPrefix::SessionAction) {
429 return Err("inner action is not valid".to_string());
430 }
431 Ok(Self { nullifier, action })
432 }
433
434 #[must_use]
436 pub fn to_compressed_bytes(&self) -> [u8; 64] {
437 let mut bytes = [0u8; 64];
438 bytes[..32].copy_from_slice(&self.nullifier.to_be_bytes());
439 bytes[32..].copy_from_slice(&self.action.to_be_bytes());
440 bytes
441 }
442
443 pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
448 if bytes.len() != 64 {
449 return Err(format!(
450 "Invalid length: expected 64 bytes, got {}",
451 bytes.len()
452 ));
453 }
454
455 let nullifier = FieldElement::from_be_bytes(bytes[..32].try_into().unwrap())
456 .map_err(|e| format!("invalid nullifier: {e}"))?;
457 let action = FieldElement::from_be_bytes(bytes[32..].try_into().unwrap())
458 .map_err(|e| format!("invalid action: {e}"))?;
459
460 if bytes[32] != OprfPrefix::SessionAction as u8 {
461 return Err("invalid action. missing expected prefix.".to_string());
462 }
463
464 Ok(Self { nullifier, action })
465 }
466}
467
468impl Default for SessionNullifier {
469 fn default() -> Self {
470 let mut action = [0u8; 32];
471 action[0] = OprfPrefix::SessionAction as u8;
472 let action = U256::from_be_bytes(action)
473 .try_into()
474 .expect("always fits in the field");
475 Self {
476 nullifier: FieldElement::ZERO,
477 action,
478 }
479 }
480}
481
482impl Serialize for SessionNullifier {
483 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
484 where
485 S: Serializer,
486 {
487 let bytes = self.to_compressed_bytes();
488 if serializer.is_human_readable() {
489 serializer.serialize_str(&format!("{}{}", Self::JSON_PREFIX, hex::encode(bytes)))
491 } else {
492 serializer.serialize_bytes(&bytes)
494 }
495 }
496}
497
498impl<'de> Deserialize<'de> for SessionNullifier {
499 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
500 where
501 D: Deserializer<'de>,
502 {
503 let bytes = if deserializer.is_human_readable() {
504 let value = String::deserialize(deserializer)?;
505 let hex_str = value.strip_prefix(Self::JSON_PREFIX).ok_or_else(|| {
506 D::Error::custom(format!(
507 "session nullifier must start with '{}'",
508 Self::JSON_PREFIX
509 ))
510 })?;
511 hex::decode(hex_str).map_err(D::Error::custom)?
512 } else {
513 Vec::deserialize(deserializer)?
514 };
515
516 Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
517 }
518}
519
520impl From<SessionNullifier> for [U256; 2] {
521 fn from(value: SessionNullifier) -> Self {
522 value.as_ethereum_representation()
523 }
524}
525
526#[cfg(test)]
527mod session_id_tests {
528 use super::*;
529 use ruint::uint;
530
531 fn test_field_element(value: u64) -> FieldElement {
532 FieldElement::from(value)
533 }
534
535 fn test_oprf_seed(value: u64) -> FieldElement {
537 let n = U256::from(value)
539 | uint!(0x0100000000000000000000000000000000000000000000000000000000000000_U256);
540 FieldElement::try_from(n).expect("test value fits in field")
541 }
542
543 #[test]
544 fn test_new_and_accessors() {
545 let commitment = test_field_element(1001);
546 let seed = test_oprf_seed(42);
547 let id = SessionId::new(commitment, seed).unwrap();
548
549 assert_eq!(id.commitment, commitment);
550 assert_eq!(id.oprf_seed, seed);
551 }
552
553 #[test]
554 fn test_default() {
555 let id = SessionId::default();
556 assert_eq!(id.commitment, FieldElement::ZERO);
557 assert_eq!(
558 id.oprf_seed,
559 uint!(0x0100000000000000000000000000000000000000000000000000000000000000_U256)
560 .try_into()
561 .unwrap()
562 );
563 }
564
565 #[test]
566 fn test_bytes_roundtrip() {
567 let id = SessionId::new(test_field_element(1001), test_oprf_seed(42)).unwrap();
568 let bytes = id.to_compressed_bytes();
569
570 assert_eq!(bytes.len(), 64);
571
572 let decoded = SessionId::from_compressed_bytes(&bytes).unwrap();
573 assert_eq!(id, decoded);
574 }
575
576 #[test]
577 fn test_bytes_use_field_element_encoding() {
578 let id = SessionId::new(test_field_element(1001), test_oprf_seed(42)).unwrap();
579 let bytes = id.to_compressed_bytes();
580
581 let mut expected = [0u8; 64];
582 expected[..32].copy_from_slice(&id.commitment.to_be_bytes());
583 expected[32..].copy_from_slice(&id.oprf_seed.to_be_bytes());
584 assert_eq!(bytes, expected);
585 }
586
587 #[test]
588 fn test_invalid_bytes_length() {
589 let too_short = vec![0u8; 63];
590 let result = SessionId::from_compressed_bytes(&too_short);
591 assert!(result.is_err());
592 assert!(result.unwrap_err().contains("Invalid length"));
593
594 let too_long = vec![0u8; 65];
595 let result = SessionId::from_compressed_bytes(&too_long);
596 assert!(result.is_err());
597 assert!(result.unwrap_err().contains("Invalid length"));
598 }
599
600 #[test]
601 fn test_from_compressed_bytes_rejects_wrong_oprf_seed_prefix() {
602 let mut bytes = [0u8; 64];
603 bytes[32] = 0x00;
606 let result = SessionId::from_compressed_bytes(&bytes);
607 assert!(result.is_err());
608 assert!(
609 result.unwrap_err().contains("invalid prefix"),
610 "should reject oprf_seed without 0x01 prefix"
611 );
612 }
613
614 #[test]
615 fn test_json_roundtrip() {
616 let id = SessionId::new(test_field_element(1001), test_oprf_seed(42)).unwrap();
617 let json = serde_json::to_string(&id).unwrap();
618
619 assert!(json.starts_with("\"session_"));
620 assert!(json.ends_with('"'));
621
622 let decoded: SessionId = serde_json::from_str(&json).unwrap();
623 assert_eq!(id, decoded);
624 }
625
626 #[test]
627 fn test_json_format() {
628 let id = SessionId::new(test_field_element(1), test_oprf_seed(2)).unwrap();
629 let json = serde_json::to_string(&id).unwrap();
630
631 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
632 assert!(parsed.is_string());
633 let value = parsed.as_str().unwrap();
634 assert!(value.starts_with("session_"));
635 }
636
637 #[test]
638 fn test_json_wrong_prefix_rejected() {
639 let result = serde_json::from_str::<SessionId>("\"snil_00\"");
640 assert!(result.is_err());
641 }
642
643 #[test]
644 fn test_generates_random_oprf_seed() {
645 let mut rng = rand::rngs::OsRng;
646
647 let seed_1 = SessionId::generate_oprf_seed(&mut rng);
648 let seed_2 = SessionId::generate_oprf_seed(&mut rng);
649
650 assert_ne!(seed_1, seed_2);
651 }
652
653 #[test]
654 fn test_from_r_seed_generated_seed_has_session_prefix() {
655 let mut rng = rand::rngs::OsRng;
656
657 for _ in 0..50 {
658 let seed = SessionId::generate_oprf_seed(&mut rng);
659 assert_eq!(seed.to_u256() >> 248, U256::from(1));
661 }
662 }
663
664 #[test]
665 fn test_from_r_seed_commitment_snapshot() {
666 let leaf_index = 42u64;
667 let r_seed = test_field_element(123);
668 let oprf_seed = test_oprf_seed(456);
669
670 let session_id = SessionId::from_r_seed(leaf_index, r_seed, oprf_seed).unwrap();
671
672 let expected = "0x1e7853ebd4fc9d9f0232fdcfae116023610bdf66a22e2700445d7a2e0e7e6152"
673 .parse::<U256>()
674 .unwrap();
675 assert_eq!(
676 session_id.commitment.to_u256(),
677 expected,
678 "commitment snapashot for session commitment changed"
679 );
680 }
681
682 #[test]
683 fn test_verify_commitment() {
684 let leaf_index = 42u64;
685 let r_seed = test_field_element(123);
686 let session_id = SessionId::from_r_seed(leaf_index, r_seed, test_oprf_seed(456)).unwrap();
687
688 session_id.verify_commitment(leaf_index, r_seed).unwrap();
689 assert_eq!(
690 session_id
691 .verify_commitment(leaf_index, test_field_element(124))
692 .unwrap_err(),
693 PrimitiveError::SessionIdCommitmentMismatch
694 );
695 }
696}
697
698#[cfg(test)]
699mod session_ref_tests {
700 use super::*;
701 use ruint::uint;
702
703 fn test_session_id() -> SessionId {
704 let oprf_seed = U256::from(42u64)
705 | uint!(0x0100000000000000000000000000000000000000000000000000000000000000_U256);
706 SessionId::new(
707 FieldElement::from(1001u64),
708 FieldElement::try_from(oprf_seed).expect("test value fits in field"),
709 )
710 .expect("valid session id")
711 }
712
713 #[test]
714 fn test_default_is_none() {
715 assert_eq!(SessionRef::default(), SessionRef::None);
716 assert!(SessionRef::None.is_none());
717 assert!(SessionRef::Create.is_create());
718 assert_eq!(
719 SessionRef::Existing(test_session_id()).existing(),
720 Some(test_session_id())
721 );
722 assert_eq!(
723 SessionRef::from(test_session_id()).existing(),
724 Some(test_session_id())
725 );
726 }
727
728 #[test]
729 fn test_deserialize_create_token() {
730 let parsed: SessionRef = serde_json::from_str("\"create\"").unwrap();
731 assert_eq!(parsed, SessionRef::Create);
732 }
733
734 #[test]
735 fn test_deserialize_existing_matches_session_id_parse() {
736 let id = test_session_id();
737 let json = serde_json::to_string(&id).unwrap();
738 let parsed: SessionRef = serde_json::from_str(&json).unwrap();
739 assert_eq!(parsed, SessionRef::Existing(id));
740 }
741
742 #[test]
743 fn test_deserialize_null_is_none() {
744 let parsed: SessionRef = serde_json::from_str("null").unwrap();
745 assert_eq!(parsed, SessionRef::None);
746 }
747
748 #[test]
749 fn test_rejects_unknown_strings() {
750 for input in ["\"Create\"", "\"creat\"", "\"snil_00\"", "\"\""] {
751 let result = serde_json::from_str::<SessionRef>(input);
752 let err = result.expect_err(input).to_string();
753 assert!(
754 err.contains("create") || err.contains("session_"),
755 "error for {input} should name the accepted forms: {err}"
756 );
757 }
758 }
759
760 #[test]
761 fn test_json_roundtrip_all_states() {
762 let cases = [
763 (SessionRef::None, "null"),
764 (SessionRef::Create, "\"create\""),
765 ];
766 for (state, expected_json) in cases {
767 let json = serde_json::to_string(&state).unwrap();
768 assert_eq!(json, expected_json);
769 let parsed: SessionRef = serde_json::from_str(&json).unwrap();
770 assert_eq!(parsed, state);
771 }
772
773 let existing = SessionRef::Existing(test_session_id());
774 let json = serde_json::to_string(&existing).unwrap();
775 assert!(json.starts_with("\"session_"));
776 let parsed: SessionRef = serde_json::from_str(&json).unwrap();
777 assert_eq!(parsed, existing);
778 }
779
780 #[test]
781 fn test_cbor_roundtrip_all_states() {
782 for state in [
783 SessionRef::None,
784 SessionRef::Create,
785 SessionRef::Existing(test_session_id()),
786 ] {
787 let mut buffer = Vec::new();
788 ciborium::into_writer(&state, &mut buffer).unwrap();
789 let decoded: SessionRef = ciborium::from_reader(&buffer[..]).unwrap();
790 assert_eq!(state, decoded);
791 }
792 }
793}
794
795#[cfg(test)]
796mod session_nullifier_tests {
797 use super::*;
798 use ruint::uint;
799
800 fn test_field_element(value: u64) -> FieldElement {
801 FieldElement::from(value)
802 }
803
804 fn test_action(value: u64) -> FieldElement {
806 let n = U256::from(value)
807 | uint!(0x0200000000000000000000000000000000000000000000000000000000000000_U256);
808 FieldElement::try_from(n).expect("test value fits in field")
809 }
810
811 #[test]
812 fn test_new_and_accessors() {
813 let nullifier = test_field_element(1001);
814 let action = test_action(42);
815 let session = SessionNullifier::new(nullifier, action).unwrap();
816
817 assert_eq!(session.nullifier(), nullifier);
818 assert_eq!(session.action(), action);
819 }
820
821 #[test]
822 fn test_as_ethereum_representation() {
823 let nullifier = test_field_element(100);
824 let action = test_action(200);
825 let session = SessionNullifier::new(nullifier, action).unwrap();
826
827 let repr = session.as_ethereum_representation();
828 assert_eq!(repr[0], U256::from(100));
829 assert_eq!(repr[1], action.to_u256());
830 }
831
832 #[test]
833 fn test_from_ethereum_representation() {
834 let action = test_action(200);
835 let repr = [U256::from(100), action.to_u256()];
836 let session = SessionNullifier::from_ethereum_representation(repr).unwrap();
837
838 assert_eq!(session.nullifier(), test_field_element(100));
839 assert_eq!(session.action(), action);
840 }
841
842 #[test]
843 fn test_json_roundtrip() {
844 let session = SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap();
845 let json = serde_json::to_string(&session).unwrap();
846
847 assert!(json.starts_with("\"snil_"));
849 assert!(json.ends_with('"'));
850
851 let decoded: SessionNullifier = serde_json::from_str(&json).unwrap();
853 assert_eq!(session, decoded);
854 }
855
856 #[test]
857 fn test_json_format() {
858 let session = SessionNullifier::new(test_field_element(1), test_action(2)).unwrap();
859 let json = serde_json::to_string(&session).unwrap();
860
861 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
863 assert!(parsed.is_string());
864 let value = parsed.as_str().unwrap();
865 assert!(value.starts_with("snil_"));
866 }
867
868 #[test]
869 fn test_bytes_roundtrip() {
870 let session = SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap();
871 let bytes = session.to_compressed_bytes();
872
873 assert_eq!(bytes.len(), 64); let decoded = SessionNullifier::from_compressed_bytes(&bytes).unwrap();
876 assert_eq!(session, decoded);
877 }
878
879 #[test]
880 fn test_bytes_use_field_element_encoding() {
881 let session = SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap();
882 let bytes = session.to_compressed_bytes();
883
884 let mut expected = [0u8; 64];
885 expected[..32].copy_from_slice(&session.nullifier().to_be_bytes());
886 expected[32..].copy_from_slice(&session.action().to_be_bytes());
887 assert_eq!(bytes, expected);
888 }
889
890 #[test]
891 fn test_invalid_bytes_length() {
892 let too_short = vec![0u8; 63];
893 let result = SessionNullifier::from_compressed_bytes(&too_short);
894 assert!(result.is_err());
895 assert!(result.unwrap_err().contains("Invalid length"));
896
897 let too_long = vec![0u8; 65];
898 let result = SessionNullifier::from_compressed_bytes(&too_long);
899 assert!(result.is_err());
900 assert!(result.unwrap_err().contains("Invalid length"));
901 }
902
903 #[test]
904 fn test_default() {
905 let session = SessionNullifier::default();
906 assert_eq!(session.nullifier(), FieldElement::ZERO);
907 let expected_action: FieldElement =
908 uint!(0x0200000000000000000000000000000000000000000000000000000000000000_U256)
909 .try_into()
910 .unwrap();
911 assert_eq!(session.action(), expected_action);
912 }
913
914 #[test]
915 fn test_into_u256_array() {
916 let action = test_action(200);
917 let session = SessionNullifier::new(test_field_element(100), action).unwrap();
918 let arr: [U256; 2] = session.into();
919
920 assert_eq!(arr[0], U256::from(100));
921 assert_eq!(arr[1], action.to_u256());
922 }
923
924 #[test]
925 fn test_new_rejects_invalid_action_prefix() {
926 let nullifier = test_field_element(1);
927 let bad_action = test_field_element(42); let result = SessionNullifier::new(nullifier, bad_action);
929 assert!(result.is_err());
930
931 let err = result.unwrap_err();
932 assert!(
933 matches!(err, PrimitiveError::InvalidInput { .. }),
934 "expected InvalidInput, got {err:?}"
935 );
936 }
937
938 #[test]
939 fn test_new_rejects_oprf_seed_prefix_as_action() {
940 let nullifier = test_field_element(1);
941 let oprf_prefixed = U256::from(42u64)
943 | uint!(0x0100000000000000000000000000000000000000000000000000000000000000_U256);
944 let bad_action = FieldElement::try_from(oprf_prefixed).unwrap();
945 assert!(SessionNullifier::new(nullifier, bad_action).is_err());
946 }
947
948 #[test]
949 fn test_from_ethereum_representation_rejects_invalid_action() {
950 let repr = [U256::from(100), U256::from(200)]; let result = SessionNullifier::from_ethereum_representation(repr);
952 assert!(result.is_err());
953 assert!(
954 result.unwrap_err().contains("action"),
955 "error should mention the action"
956 );
957 }
958
959 #[test]
960 fn test_from_compressed_bytes_rejects_invalid_action_prefix() {
961 let mut bytes = [0u8; 64];
962 bytes[32] = 0x00;
964 let result = SessionNullifier::from_compressed_bytes(&bytes);
965 assert!(result.is_err());
966 assert!(
967 result.unwrap_err().contains("action"),
968 "error should mention the action"
969 );
970 }
971
972 #[test]
973 fn test_json_rejects_invalid_action_prefix() {
974 let nullifier = test_field_element(1);
976 let bad_action = test_field_element(2); let mut bytes = [0u8; 64];
978 bytes[..32].copy_from_slice(&nullifier.to_be_bytes());
979 bytes[32..].copy_from_slice(&bad_action.to_be_bytes());
980 let json = format!("\"snil_{}\"", hex::encode(bytes));
981
982 let result = serde_json::from_str::<SessionNullifier>(&json);
983 assert!(result.is_err());
984 }
985}