1use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CompactSummary {
16 pub checkpoint_count: u32,
17 pub total_chars: u64,
18 pub total_vdf_time_seconds: f64,
19 pub evidence_tier: u8,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub verdict: Option<String>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub confidence_score: Option<f32>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct CompactMetadata {
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub author_name: Option<String>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub created: Option<DateTime<Utc>>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub verifier_name: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub verified_at: Option<DateTime<Utc>>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct CompactEvidenceRef {
44 pub packet_id: Uuid,
45 pub chain_hash: String,
47 pub document_hash: String,
48 pub summary: CompactSummary,
49 pub evidence_uri: String,
51 pub signature: String,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub metadata: Option<CompactMetadata>,
55}
56
57impl CompactEvidenceRef {
58 pub fn new(
60 packet_id: Uuid,
61 chain_hash: String,
62 document_hash: String,
63 summary: CompactSummary,
64 evidence_uri: String,
65 signature: String,
66 ) -> Self {
67 Self {
68 packet_id,
69 chain_hash,
70 document_hash,
71 summary,
72 evidence_uri,
73 signature,
74 metadata: None,
75 }
76 }
77
78 pub fn with_metadata(mut self, metadata: CompactMetadata) -> Self {
80 self.metadata = Some(metadata);
81 self
82 }
83
84 pub fn signable_payload(&self) -> Vec<u8> {
86 let payload = serde_json::json!({
87 "packet_id": self.packet_id.to_string(),
88 "chain_hash": self.chain_hash,
89 "document_hash": self.document_hash,
90 "summary": {
91 "checkpoint_count": self.summary.checkpoint_count,
92 "total_chars": self.summary.total_chars,
93 "total_vdf_time_seconds": self.summary.total_vdf_time_seconds,
94 "evidence_tier": self.summary.evidence_tier,
95 },
96 "evidence_uri": self.evidence_uri,
97 });
98
99 payload.to_string().into_bytes()
100 }
101
102 pub fn to_base64_uri(&self) -> Result<String, serde_json::Error> {
104 let json = serde_json::to_vec(self)?;
105 let encoded =
106 base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, &json);
107 Ok(format!("pop-ref:{}", encoded))
108 }
109
110 pub fn from_base64_uri(uri: &str) -> Result<Self, CompactRefError> {
112 let encoded = uri
113 .strip_prefix("pop-ref:")
114 .ok_or(CompactRefError::InvalidPrefix)?;
115
116 let json =
117 base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, encoded)
118 .map_err(|_| CompactRefError::InvalidBase64)?;
119
120 serde_json::from_slice(&json).map_err(|_| CompactRefError::InvalidJson)
121 }
122
123 pub fn verification_uri(&self) -> String {
125 let encoded_evidence = urlencoding::encode(&self.evidence_uri);
126 format!(
127 "pop://verify?packet={}&uri={}",
128 self.packet_id, encoded_evidence
129 )
130 }
131
132 pub fn estimated_size(&self) -> usize {
134 let base = 16 + 64 + 64 + 50 + 100 + 88 + 100;
136 let uri_len = self.evidence_uri.len();
137 let metadata_len = self
138 .metadata
139 .as_ref()
140 .map(|m| {
141 m.author_name.as_ref().map(|s| s.len()).unwrap_or(0)
142 + m.verifier_name.as_ref().map(|s| s.len()).unwrap_or(0)
143 + 40 })
145 .unwrap_or(0);
146
147 base + uri_len + metadata_len
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum CompactRefError {
154 InvalidPrefix,
156 InvalidBase64,
158 InvalidJson,
160 InvalidSignature,
162 HashMismatch,
164}
165
166impl std::fmt::Display for CompactRefError {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 Self::InvalidPrefix => write!(f, "URI must start with 'pop-ref:'"),
170 Self::InvalidBase64 => write!(f, "Invalid base64 encoding"),
171 Self::InvalidJson => write!(f, "Invalid JSON structure"),
172 Self::InvalidSignature => write!(f, "Signature verification failed"),
173 Self::HashMismatch => write!(f, "Hash does not match Evidence"),
174 }
175 }
176}
177
178impl std::error::Error for CompactRefError {}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 fn sample_ref() -> CompactEvidenceRef {
185 CompactEvidenceRef::new(
186 Uuid::nil(),
187 "abcd1234".to_string(),
188 "efgh5678".to_string(),
189 CompactSummary {
190 checkpoint_count: 47,
191 total_chars: 12500,
192 total_vdf_time_seconds: 5400.0,
193 evidence_tier: 2,
194 verdict: Some("likely-human".to_string()),
195 confidence_score: Some(0.87),
196 },
197 "https://evidence.example.com/packets/abc.pop".to_string(),
198 "test_signature".to_string(),
199 )
200 }
201
202 #[test]
203 fn test_create_compact_ref() {
204 let compact = sample_ref();
205 assert_eq!(compact.summary.checkpoint_count, 47);
206 assert_eq!(compact.summary.evidence_tier, 2);
207 }
208
209 #[test]
210 fn test_base64_roundtrip() {
211 let original = sample_ref();
212 let encoded = original.to_base64_uri().unwrap();
213 assert!(encoded.starts_with("pop-ref:"));
214
215 let decoded = CompactEvidenceRef::from_base64_uri(&encoded).unwrap();
216 assert_eq!(decoded.packet_id, original.packet_id);
217 assert_eq!(decoded.chain_hash, original.chain_hash);
218 }
219
220 #[test]
221 fn test_invalid_prefix() {
222 let result = CompactEvidenceRef::from_base64_uri("invalid:data");
223 assert_eq!(result.unwrap_err(), CompactRefError::InvalidPrefix);
224 }
225
226 #[test]
227 fn test_new_constructor() {
228 let compact = CompactEvidenceRef::new(
229 Uuid::new_v4(),
230 "hash1".to_string(),
231 "hash2".to_string(),
232 CompactSummary {
233 checkpoint_count: 10,
234 total_chars: 1000,
235 total_vdf_time_seconds: 600.0,
236 evidence_tier: 1,
237 verdict: None,
238 confidence_score: None,
239 },
240 "https://example.com/evidence.pop".to_string(),
241 "signature".to_string(),
242 );
243
244 assert_eq!(compact.summary.checkpoint_count, 10);
245 }
246
247 #[test]
248 fn test_verification_uri() {
249 let compact = sample_ref();
250 let uri = compact.verification_uri();
251 assert!(uri.starts_with("pop://verify?"));
252 assert!(uri.contains("packet="));
253 }
254
255 #[test]
256 fn test_estimated_size() {
257 let compact = sample_ref();
258 let size = compact.estimated_size();
259 assert!(size < 1000);
260 }
261
262 #[test]
263 fn test_serialization() {
264 let original = sample_ref();
265 let json = serde_json::to_string(&original).unwrap();
266 let parsed: CompactEvidenceRef = serde_json::from_str(&json).unwrap();
267 assert_eq!(parsed.packet_id, original.packet_id);
268 }
269}