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