world_id_primitives/
rp.rs1#![allow(clippy::unreadable_literal)]
2
3use std::{fmt, str::FromStr};
4
5use ark_ff::{BigInteger as _, PrimeField as _};
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
7
8use crate::FieldElement;
9
10const RP_SIGNATURE_MSG_VERSION: u8 = 0x01;
11
12#[expect(unused_imports, reason = "used in doc comments")]
13use crate::ProofRequest;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct RpId(u64);
18
19impl RpId {
20 #[must_use]
22 pub const fn into_inner(self) -> u64 {
23 self.0
24 }
25
26 #[must_use]
28 pub const fn new(value: u64) -> Self {
29 Self(value)
30 }
31}
32
33impl fmt::Display for RpId {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 write!(f, "rp_{:016x}", self.0)
36 }
37}
38
39impl FromStr for RpId {
40 type Err = String;
41
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 if let Some(id) = s.strip_prefix("rp_") {
44 Ok(Self(u64::from_str_radix(id, 16).map_err(|_| {
45 "Invalid RP ID format: expected hex string".to_string()
46 })?))
47 } else {
48 Err("A valid RP ID must start with 'rp_'".to_string())
49 }
50 }
51}
52
53impl From<u64> for RpId {
54 fn from(value: u64) -> Self {
55 Self(value)
56 }
57}
58
59impl From<RpId> for FieldElement {
60 fn from(value: RpId) -> Self {
61 Self::from(value.0)
62 }
63}
64
65impl Serialize for RpId {
66 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
67 where
68 S: Serializer,
69 {
70 if serializer.is_human_readable() {
71 serializer.serialize_str(&self.to_string())
72 } else {
73 u64::serialize(&self.0, serializer)
74 }
75 }
76}
77
78impl<'de> Deserialize<'de> for RpId {
79 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80 where
81 D: Deserializer<'de>,
82 {
83 if deserializer.is_human_readable() {
84 let s = String::deserialize(deserializer)?;
85 Self::from_str(&s).map_err(D::Error::custom)
86 } else {
87 let value = u64::deserialize(deserializer)?;
88 Ok(Self(value))
89 }
90 }
91}
92
93#[must_use]
107pub fn compute_rp_signature_msg(
108 nonce: ark_babyjubjub::Fq,
109 created_at: u64,
110 expires_at: u64,
111 action: Option<ark_babyjubjub::Fq>,
112) -> Vec<u8> {
113 let mut msg = Vec::with_capacity(81);
114 msg.push(RP_SIGNATURE_MSG_VERSION);
115 msg.extend(nonce.into_bigint().to_bytes_be());
116 msg.extend(created_at.to_be_bytes());
117 msg.extend(expires_at.to_be_bytes());
118
119 if let Some(action) = action {
120 msg.extend(action.into_bigint().to_bytes_be());
121 }
122
123 msg
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_rpid_display() {
132 let rp_id = RpId::new(0x123456789abcdef0);
133 assert_eq!(rp_id.to_string(), "rp_123456789abcdef0");
134
135 let rp_id = RpId::new(u64::MAX);
136 assert_eq!(rp_id.to_string(), "rp_ffffffffffffffff");
137
138 let rp_id = RpId::new(0);
139 assert_eq!(rp_id.to_string(), "rp_0000000000000000");
140 }
141
142 #[test]
143 fn test_rpid_from_str() {
144 let rp_id = "rp_123456789abcdef0".parse::<RpId>().unwrap();
145 assert_eq!(rp_id.0, 0x123456789abcdef0);
146
147 let rp_id = "rp_ffffffffffffffff".parse::<RpId>().unwrap();
148 assert_eq!(rp_id.0, u64::MAX);
149
150 let rp_id = "rp_0000000000000000".parse::<RpId>().unwrap();
151 assert_eq!(rp_id.0, 0);
152
153 let rp_id = "rp_123456789ABCDEF0".parse::<RpId>().unwrap();
154 assert_eq!(rp_id.0, 0x123456789abcdef0);
155 }
156
157 #[test]
158 fn test_rpid_from_str_errors() {
159 assert!("123456789abcdef0".parse::<RpId>().is_err());
160 assert!("rp_invalid".parse::<RpId>().is_err());
161 assert!("rp_".parse::<RpId>().is_err());
162 }
163
164 #[test]
165 fn test_rpid_roundtrip() {
166 let original = RpId::new(0x123456789abcdef0);
167 let s = original.to_string();
168 let parsed = s.parse::<RpId>().unwrap();
169 assert_eq!(original, parsed);
170 }
171
172 #[test]
173 fn test_rpid_json_serialization() {
174 let rp_id = RpId::new(0x123456789abcdef0);
175 let json = serde_json::to_string(&rp_id).unwrap();
176 assert_eq!(json, "\"rp_123456789abcdef0\"");
177
178 let deserialized: RpId = serde_json::from_str(&json).unwrap();
179 assert_eq!(rp_id, deserialized);
180 }
181
182 #[test]
183 fn test_rpid_binary_serialization() {
184 let rp_id = RpId::new(0x123456789abcdef0);
185
186 let mut buffer = Vec::new();
187 ciborium::into_writer(&rp_id, &mut buffer).unwrap();
188
189 let decoded: RpId = ciborium::from_reader(&buffer[..]).unwrap();
190
191 assert_eq!(rp_id, decoded);
192 }
193
194 #[test]
195 fn test_compute_rp_signature_msg_fixed_length() {
196 let nonce = ark_babyjubjub::Fq::from(1u64);
199 let created_at = 1000u64;
200 let expires_at = 2000u64;
201
202 let msg = compute_rp_signature_msg(nonce, created_at, expires_at, None);
203
204 assert_eq!(
207 msg.len(),
208 49,
209 "RP signature message must be exactly 49 bytes"
210 );
211 assert_eq!(
212 msg[0], RP_SIGNATURE_MSG_VERSION,
213 "RP signature message version must be 0x01"
214 );
215 }
216
217 #[test]
218 fn test_compute_rp_signature_msg_with_action() {
219 let nonce = ark_babyjubjub::Fq::from(1u64);
222 let created_at = 1000u64;
223 let expires_at = 2000u64;
224 let action = ark_babyjubjub::Fq::from(2u64);
225
226 let msg = compute_rp_signature_msg(nonce, created_at, expires_at, Some(action));
227
228 assert_eq!(
231 msg.len(),
232 81,
233 "RP signature message must be exactly 81 bytes"
234 );
235 assert_eq!(
236 msg[0], RP_SIGNATURE_MSG_VERSION,
237 "RP signature message version must be 0x01"
238 );
239 }
240}