1use crate::{FieldElement, PrimitiveError};
2use embed_doc_image::embed_doc_image;
3use ruint::aliases::U256;
4use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
5
6#[expect(unused_imports, reason = "used in doc comments")]
7use crate::circuit_inputs::QueryProofCircuitInput;
8
9#[repr(u8)]
11pub enum SessionFeType {
12 OprfSeed = 0x01,
14 Action = 0x02,
16}
17
18pub trait SessionFieldElement {
20 fn random_for_session<R: rand::CryptoRng + rand::RngCore>(
23 rng: &mut R,
24 element_type: SessionFeType,
25 ) -> FieldElement;
26 fn is_valid_for_session(&self, element_type: SessionFeType) -> bool;
29}
30
31impl SessionFieldElement for FieldElement {
32 fn random_for_session<R: rand::CryptoRng + rand::RngCore>(
33 rng: &mut R,
34 element_type: SessionFeType,
35 ) -> FieldElement {
36 let mut bytes = [0u8; 32];
37 rng.fill_bytes(&mut bytes);
38 bytes[0] = element_type as u8;
39 let seed = U256::from_be_bytes(bytes);
40 Self::try_from(seed).expect(
41 "should always fit in the field because with 0x01 as the MSB, the field element < babyjubjub modulus",
42 )
43 }
44
45 fn is_valid_for_session(&self, element_type: SessionFeType) -> bool {
46 self.to_be_bytes()[0] == element_type as u8
47 }
48}
49
50#[embed_doc_image("session-proofs.png", "assets/session-proofs.png")]
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub struct SessionId {
66 pub commitment: FieldElement,
71 pub oprf_seed: FieldElement,
93}
94
95impl SessionId {
96 const JSON_PREFIX: &str = "session_";
97 const DS_C: &[u8] = b"H(id, r)";
101
102 pub fn new(commitment: FieldElement, oprf_seed: FieldElement) -> Result<Self, PrimitiveError> {
107 if !oprf_seed.is_valid_for_session(SessionFeType::OprfSeed) {
111 return Err(PrimitiveError::InvalidInput {
112 attribute: "session_id".to_string(),
113 reason: "inner oprf_seed is not valid".to_string(),
114 });
115 }
116 Ok(Self {
117 commitment,
118 oprf_seed,
119 })
120 }
121
122 pub fn from_r_seed(
135 leaf_index: u64,
136 session_id_r_seed: FieldElement,
137 oprf_seed: FieldElement,
138 ) -> Result<Self, PrimitiveError> {
139 let sub_ds = FieldElement::from_be_bytes_mod_order(Self::DS_C);
140
141 if !oprf_seed.is_valid_for_session(SessionFeType::OprfSeed) {
142 return Err(PrimitiveError::InvalidInput {
143 attribute: "session_id".to_string(),
144 reason: "inner oprf_seed is not valid".to_string(),
145 });
146 }
147
148 let mut input = [*sub_ds, leaf_index.into(), *session_id_r_seed];
149 poseidon2::bn254::t3::permutation_in_place(&mut input);
150 let commitment = input[1].into();
151 Ok(Self {
152 commitment,
153 oprf_seed,
154 })
155 }
156
157 pub fn generate_oprf_seed<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> FieldElement {
159 FieldElement::random_for_session(rng, SessionFeType::OprfSeed)
160 }
161
162 #[must_use]
164 pub fn to_compressed_bytes(&self) -> [u8; 64] {
165 let mut bytes = [0u8; 64];
166 bytes[..32].copy_from_slice(&self.commitment.to_be_bytes());
167 bytes[32..].copy_from_slice(&self.oprf_seed.to_be_bytes());
168 bytes
169 }
170
171 pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
176 if bytes.len() != 64 {
177 return Err(format!(
178 "Invalid length: expected 64 bytes, got {}",
179 bytes.len()
180 ));
181 }
182
183 let commitment = FieldElement::from_be_bytes(bytes[..32].try_into().unwrap())
184 .map_err(|e| format!("invalid commitment: {e}"))?;
185 let oprf_seed = FieldElement::from_be_bytes(bytes[32..].try_into().unwrap())
186 .map_err(|e| format!("invalid oprf_seed: {e}"))?;
187
188 if bytes[32] != SessionFeType::OprfSeed as u8 {
189 return Err("invalid prefix for oprf_seed".to_string());
190 }
191
192 Ok(Self {
193 commitment,
194 oprf_seed,
195 })
196 }
197}
198
199impl Default for SessionId {
200 fn default() -> Self {
201 let mut oprf_seed = [0u8; 32];
202 oprf_seed[0] = SessionFeType::OprfSeed as u8;
203 let oprf_seed = U256::from_be_bytes(oprf_seed)
204 .try_into()
205 .expect("always fits in the field");
206 Self {
207 commitment: FieldElement::ZERO,
208 oprf_seed,
209 }
210 }
211}
212
213impl Serialize for SessionId {
214 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
215 where
216 S: Serializer,
217 {
218 let bytes = self.to_compressed_bytes();
219 if serializer.is_human_readable() {
220 serializer.serialize_str(&format!("{}{}", Self::JSON_PREFIX, hex::encode(bytes)))
222 } else {
223 serializer.serialize_bytes(&bytes)
225 }
226 }
227}
228
229impl<'de> Deserialize<'de> for SessionId {
230 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231 where
232 D: Deserializer<'de>,
233 {
234 let bytes = if deserializer.is_human_readable() {
235 let value = String::deserialize(deserializer)?;
236 let hex_str = value.strip_prefix(Self::JSON_PREFIX).ok_or_else(|| {
237 D::Error::custom(format!(
238 "session id must start with '{}'",
239 Self::JSON_PREFIX
240 ))
241 })?;
242 hex::decode(hex_str).map_err(D::Error::custom)?
243 } else {
244 Vec::deserialize(deserializer)?
245 };
246
247 Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
271pub struct SessionNullifier {
272 nullifier: FieldElement,
274 action: FieldElement,
276}
277
278impl SessionNullifier {
279 const JSON_PREFIX: &str = "snil_";
280
281 pub fn new(nullifier: FieldElement, action: FieldElement) -> Result<Self, PrimitiveError> {
283 if !action.is_valid_for_session(SessionFeType::Action) {
284 return Err(PrimitiveError::InvalidInput {
285 attribute: "session_nullifier".to_string(),
286 reason: "inner action is not valid".to_string(),
287 });
288 }
289 Ok(Self { nullifier, action })
290 }
291
292 #[must_use]
294 pub const fn nullifier(&self) -> FieldElement {
295 self.nullifier
296 }
297
298 #[must_use]
300 pub const fn action(&self) -> FieldElement {
301 self.action
302 }
303
304 #[must_use]
308 pub fn as_ethereum_representation(&self) -> [U256; 2] {
309 [self.nullifier.into(), self.action.into()]
310 }
311
312 pub fn from_ethereum_representation(value: [U256; 2]) -> Result<Self, String> {
317 let nullifier =
318 FieldElement::try_from(value[0]).map_err(|e| format!("invalid nullifier: {e}"))?;
319 let action =
320 FieldElement::try_from(value[1]).map_err(|e| format!("invalid action: {e}"))?;
321
322 if !action.is_valid_for_session(SessionFeType::Action) {
323 return Err("inner action is not valid".to_string());
324 }
325 Ok(Self { nullifier, action })
326 }
327
328 #[must_use]
330 pub fn to_compressed_bytes(&self) -> [u8; 64] {
331 let mut bytes = [0u8; 64];
332 bytes[..32].copy_from_slice(&self.nullifier.to_be_bytes());
333 bytes[32..].copy_from_slice(&self.action.to_be_bytes());
334 bytes
335 }
336
337 pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
342 if bytes.len() != 64 {
343 return Err(format!(
344 "Invalid length: expected 64 bytes, got {}",
345 bytes.len()
346 ));
347 }
348
349 let nullifier = FieldElement::from_be_bytes(bytes[..32].try_into().unwrap())
350 .map_err(|e| format!("invalid nullifier: {e}"))?;
351 let action = FieldElement::from_be_bytes(bytes[32..].try_into().unwrap())
352 .map_err(|e| format!("invalid action: {e}"))?;
353
354 if bytes[32] != SessionFeType::Action as u8 {
355 return Err("invalid action. missing expected prefix.".to_string());
356 }
357
358 Ok(Self { nullifier, action })
359 }
360}
361
362impl Default for SessionNullifier {
363 fn default() -> Self {
364 let mut action = [0u8; 32];
365 action[0] = SessionFeType::Action as u8;
366 let action = U256::from_be_bytes(action)
367 .try_into()
368 .expect("always fits in the field");
369 Self {
370 nullifier: FieldElement::ZERO,
371 action,
372 }
373 }
374}
375
376impl Serialize for SessionNullifier {
377 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
378 where
379 S: Serializer,
380 {
381 let bytes = self.to_compressed_bytes();
382 if serializer.is_human_readable() {
383 serializer.serialize_str(&format!("{}{}", Self::JSON_PREFIX, hex::encode(bytes)))
385 } else {
386 serializer.serialize_bytes(&bytes)
388 }
389 }
390}
391
392impl<'de> Deserialize<'de> for SessionNullifier {
393 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
394 where
395 D: Deserializer<'de>,
396 {
397 let bytes = if deserializer.is_human_readable() {
398 let value = String::deserialize(deserializer)?;
399 let hex_str = value.strip_prefix(Self::JSON_PREFIX).ok_or_else(|| {
400 D::Error::custom(format!(
401 "session nullifier must start with '{}'",
402 Self::JSON_PREFIX
403 ))
404 })?;
405 hex::decode(hex_str).map_err(D::Error::custom)?
406 } else {
407 Vec::deserialize(deserializer)?
408 };
409
410 Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
411 }
412}
413
414impl From<SessionNullifier> for [U256; 2] {
415 fn from(value: SessionNullifier) -> Self {
416 value.as_ethereum_representation()
417 }
418}
419
420#[cfg(test)]
421mod session_id_tests {
422 use super::*;
423 use ruint::uint;
424
425 fn test_field_element(value: u64) -> FieldElement {
426 FieldElement::from(value)
427 }
428
429 fn test_oprf_seed(value: u64) -> FieldElement {
431 let n = U256::from(value)
433 | uint!(0x0100000000000000000000000000000000000000000000000000000000000000_U256);
434 FieldElement::try_from(n).expect("test value fits in field")
435 }
436
437 #[test]
438 fn test_new_and_accessors() {
439 let commitment = test_field_element(1001);
440 let seed = test_oprf_seed(42);
441 let id = SessionId::new(commitment, seed).unwrap();
442
443 assert_eq!(id.commitment, commitment);
444 assert_eq!(id.oprf_seed, seed);
445 }
446
447 #[test]
448 fn test_default() {
449 let id = SessionId::default();
450 assert_eq!(id.commitment, FieldElement::ZERO);
451 assert_eq!(
452 id.oprf_seed,
453 uint!(0x0100000000000000000000000000000000000000000000000000000000000000_U256)
454 .try_into()
455 .unwrap()
456 );
457 }
458
459 #[test]
460 fn test_bytes_roundtrip() {
461 let id = SessionId::new(test_field_element(1001), test_oprf_seed(42)).unwrap();
462 let bytes = id.to_compressed_bytes();
463
464 assert_eq!(bytes.len(), 64);
465
466 let decoded = SessionId::from_compressed_bytes(&bytes).unwrap();
467 assert_eq!(id, decoded);
468 }
469
470 #[test]
471 fn test_bytes_use_field_element_encoding() {
472 let id = SessionId::new(test_field_element(1001), test_oprf_seed(42)).unwrap();
473 let bytes = id.to_compressed_bytes();
474
475 let mut expected = [0u8; 64];
476 expected[..32].copy_from_slice(&id.commitment.to_be_bytes());
477 expected[32..].copy_from_slice(&id.oprf_seed.to_be_bytes());
478 assert_eq!(bytes, expected);
479 }
480
481 #[test]
482 fn test_invalid_bytes_length() {
483 let too_short = vec![0u8; 63];
484 let result = SessionId::from_compressed_bytes(&too_short);
485 assert!(result.is_err());
486 assert!(result.unwrap_err().contains("Invalid length"));
487
488 let too_long = vec![0u8; 65];
489 let result = SessionId::from_compressed_bytes(&too_long);
490 assert!(result.is_err());
491 assert!(result.unwrap_err().contains("Invalid length"));
492 }
493
494 #[test]
495 fn test_from_compressed_bytes_rejects_wrong_oprf_seed_prefix() {
496 let mut bytes = [0u8; 64];
497 bytes[32] = 0x00;
500 let result = SessionId::from_compressed_bytes(&bytes);
501 assert!(result.is_err());
502 assert!(
503 result.unwrap_err().contains("invalid prefix"),
504 "should reject oprf_seed without 0x01 prefix"
505 );
506 }
507
508 #[test]
509 fn test_json_roundtrip() {
510 let id = SessionId::new(test_field_element(1001), test_oprf_seed(42)).unwrap();
511 let json = serde_json::to_string(&id).unwrap();
512
513 assert!(json.starts_with("\"session_"));
514 assert!(json.ends_with('"'));
515
516 let decoded: SessionId = serde_json::from_str(&json).unwrap();
517 assert_eq!(id, decoded);
518 }
519
520 #[test]
521 fn test_json_format() {
522 let id = SessionId::new(test_field_element(1), test_oprf_seed(2)).unwrap();
523 let json = serde_json::to_string(&id).unwrap();
524
525 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
526 assert!(parsed.is_string());
527 let value = parsed.as_str().unwrap();
528 assert!(value.starts_with("session_"));
529 }
530
531 #[test]
532 fn test_json_wrong_prefix_rejected() {
533 let result = serde_json::from_str::<SessionId>("\"snil_00\"");
534 assert!(result.is_err());
535 }
536
537 #[test]
538 fn test_generates_random_oprf_seed() {
539 let mut rng = rand::rngs::OsRng;
540
541 let seed_1 = SessionId::generate_oprf_seed(&mut rng);
542 let seed_2 = SessionId::generate_oprf_seed(&mut rng);
543
544 assert_ne!(seed_1, seed_2);
545 }
546
547 #[test]
548 fn test_from_r_seed_generated_seed_has_session_prefix() {
549 let mut rng = rand::rngs::OsRng;
550
551 for _ in 0..50 {
552 let seed = SessionId::generate_oprf_seed(&mut rng);
553 assert_eq!(seed.to_u256() >> 248, U256::from(1));
555 }
556 }
557
558 #[test]
559 fn test_from_r_seed_commitment_snapshot() {
560 let leaf_index = 42u64;
561 let r_seed = test_field_element(123);
562 let oprf_seed = test_oprf_seed(456);
563
564 let session_id = SessionId::from_r_seed(leaf_index, r_seed, oprf_seed).unwrap();
565
566 let expected = "0x1e7853ebd4fc9d9f0232fdcfae116023610bdf66a22e2700445d7a2e0e7e6152"
567 .parse::<U256>()
568 .unwrap();
569 assert_eq!(
570 session_id.commitment.to_u256(),
571 expected,
572 "commitment snapashot for session commitment changed"
573 );
574 }
575}
576
577#[cfg(test)]
578mod session_nullifier_tests {
579 use super::*;
580 use ruint::uint;
581
582 fn test_field_element(value: u64) -> FieldElement {
583 FieldElement::from(value)
584 }
585
586 fn test_action(value: u64) -> FieldElement {
588 let n = U256::from(value)
589 | uint!(0x0200000000000000000000000000000000000000000000000000000000000000_U256);
590 FieldElement::try_from(n).expect("test value fits in field")
591 }
592
593 #[test]
594 fn test_new_and_accessors() {
595 let nullifier = test_field_element(1001);
596 let action = test_action(42);
597 let session = SessionNullifier::new(nullifier, action).unwrap();
598
599 assert_eq!(session.nullifier(), nullifier);
600 assert_eq!(session.action(), action);
601 }
602
603 #[test]
604 fn test_as_ethereum_representation() {
605 let nullifier = test_field_element(100);
606 let action = test_action(200);
607 let session = SessionNullifier::new(nullifier, action).unwrap();
608
609 let repr = session.as_ethereum_representation();
610 assert_eq!(repr[0], U256::from(100));
611 assert_eq!(repr[1], action.to_u256());
612 }
613
614 #[test]
615 fn test_from_ethereum_representation() {
616 let action = test_action(200);
617 let repr = [U256::from(100), action.to_u256()];
618 let session = SessionNullifier::from_ethereum_representation(repr).unwrap();
619
620 assert_eq!(session.nullifier(), test_field_element(100));
621 assert_eq!(session.action(), action);
622 }
623
624 #[test]
625 fn test_json_roundtrip() {
626 let session = SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap();
627 let json = serde_json::to_string(&session).unwrap();
628
629 assert!(json.starts_with("\"snil_"));
631 assert!(json.ends_with('"'));
632
633 let decoded: SessionNullifier = serde_json::from_str(&json).unwrap();
635 assert_eq!(session, decoded);
636 }
637
638 #[test]
639 fn test_json_format() {
640 let session = SessionNullifier::new(test_field_element(1), test_action(2)).unwrap();
641 let json = serde_json::to_string(&session).unwrap();
642
643 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
645 assert!(parsed.is_string());
646 let value = parsed.as_str().unwrap();
647 assert!(value.starts_with("snil_"));
648 }
649
650 #[test]
651 fn test_bytes_roundtrip() {
652 let session = SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap();
653 let bytes = session.to_compressed_bytes();
654
655 assert_eq!(bytes.len(), 64); let decoded = SessionNullifier::from_compressed_bytes(&bytes).unwrap();
658 assert_eq!(session, decoded);
659 }
660
661 #[test]
662 fn test_bytes_use_field_element_encoding() {
663 let session = SessionNullifier::new(test_field_element(1001), test_action(42)).unwrap();
664 let bytes = session.to_compressed_bytes();
665
666 let mut expected = [0u8; 64];
667 expected[..32].copy_from_slice(&session.nullifier().to_be_bytes());
668 expected[32..].copy_from_slice(&session.action().to_be_bytes());
669 assert_eq!(bytes, expected);
670 }
671
672 #[test]
673 fn test_invalid_bytes_length() {
674 let too_short = vec![0u8; 63];
675 let result = SessionNullifier::from_compressed_bytes(&too_short);
676 assert!(result.is_err());
677 assert!(result.unwrap_err().contains("Invalid length"));
678
679 let too_long = vec![0u8; 65];
680 let result = SessionNullifier::from_compressed_bytes(&too_long);
681 assert!(result.is_err());
682 assert!(result.unwrap_err().contains("Invalid length"));
683 }
684
685 #[test]
686 fn test_default() {
687 let session = SessionNullifier::default();
688 assert_eq!(session.nullifier(), FieldElement::ZERO);
689 let expected_action: FieldElement =
690 uint!(0x0200000000000000000000000000000000000000000000000000000000000000_U256)
691 .try_into()
692 .unwrap();
693 assert_eq!(session.action(), expected_action);
694 }
695
696 #[test]
697 fn test_into_u256_array() {
698 let action = test_action(200);
699 let session = SessionNullifier::new(test_field_element(100), action).unwrap();
700 let arr: [U256; 2] = session.into();
701
702 assert_eq!(arr[0], U256::from(100));
703 assert_eq!(arr[1], action.to_u256());
704 }
705
706 #[test]
707 fn test_new_rejects_invalid_action_prefix() {
708 let nullifier = test_field_element(1);
709 let bad_action = test_field_element(42); let result = SessionNullifier::new(nullifier, bad_action);
711 assert!(result.is_err());
712
713 let err = result.unwrap_err();
714 assert!(
715 matches!(err, PrimitiveError::InvalidInput { .. }),
716 "expected InvalidInput, got {err:?}"
717 );
718 }
719
720 #[test]
721 fn test_new_rejects_oprf_seed_prefix_as_action() {
722 let nullifier = test_field_element(1);
723 let oprf_prefixed = U256::from(42u64)
725 | uint!(0x0100000000000000000000000000000000000000000000000000000000000000_U256);
726 let bad_action = FieldElement::try_from(oprf_prefixed).unwrap();
727 assert!(SessionNullifier::new(nullifier, bad_action).is_err());
728 }
729
730 #[test]
731 fn test_from_ethereum_representation_rejects_invalid_action() {
732 let repr = [U256::from(100), U256::from(200)]; let result = SessionNullifier::from_ethereum_representation(repr);
734 assert!(result.is_err());
735 assert!(
736 result.unwrap_err().contains("action"),
737 "error should mention the action"
738 );
739 }
740
741 #[test]
742 fn test_from_compressed_bytes_rejects_invalid_action_prefix() {
743 let mut bytes = [0u8; 64];
744 bytes[32] = 0x00;
746 let result = SessionNullifier::from_compressed_bytes(&bytes);
747 assert!(result.is_err());
748 assert!(
749 result.unwrap_err().contains("action"),
750 "error should mention the action"
751 );
752 }
753
754 #[test]
755 fn test_json_rejects_invalid_action_prefix() {
756 let nullifier = test_field_element(1);
758 let bad_action = test_field_element(2); let mut bytes = [0u8; 64];
760 bytes[..32].copy_from_slice(&nullifier.to_be_bytes());
761 bytes[32..].copy_from_slice(&bad_action.to_be_bytes());
762 let json = format!("\"snil_{}\"", hex::encode(bytes));
763
764 let result = serde_json::from_str::<SessionNullifier>(&json);
765 assert!(result.is_err());
766 }
767}