verified_profile_conformance.rs (11093B)
1 #![cfg(feature = "json")] 2 3 use std::{borrow::Cow, fs, path::Path}; 4 5 use radroots_event::envelope::event_head::{ 6 CurrentEventHead, EventHeadCandidate, EventHeadCandidateResult, EventHeadDecision, 7 event_head_candidate_for_event, select_event_head, 8 }; 9 use radroots_event::{envelope::EventEnvelope, envelope::EventEnvelopeParts, wire::Nip01EventWire}; 10 use radroots_event::{id::ParseError, wire::EventWireError}; 11 use radroots_event_codec::admission::profile::{ 12 RadrootsAdmittedProfileEvent, RadrootsProfileAdmissionError, verify_and_admit_profile_event, 13 }; 14 use radroots_event_codec::decode::profile::RadrootsProfileMetadataParseError; 15 use radroots_event_codec::verify::{RadrootsNip01VerificationError, verify_nip01_event}; 16 use serde::Deserialize; 17 use serde_json::{Map, Value}; 18 19 const PACKAGED_VECTORS: &str = include_str!("fixtures/profile_verified_event.v1.json"); 20 const WORKSPACE_VECTOR_PATH: &str = 21 "../../contracts/conformance/vectors/profile/verified_event.v1.json"; 22 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; 23 24 #[derive(Debug, Deserialize)] 25 struct Suite { 26 suite: String, 27 contract_version: String, 28 vectors: Vec<Vector>, 29 } 30 31 #[derive(Debug, Deserialize)] 32 struct Vector { 33 id: String, 34 kind: String, 35 input: Value, 36 expected: Value, 37 } 38 39 #[derive(Debug, Deserialize)] 40 #[serde(deny_unknown_fields)] 41 struct RawEvent { 42 id: String, 43 pubkey: String, 44 created_at: u64, 45 kind: u32, 46 tags: Vec<Vec<String>>, 47 content: String, 48 sig: String, 49 } 50 51 #[test] 52 fn raw_signed_vectors_execute_against_verified_event_and_profile_boundaries() { 53 let vectors = conformance_vectors(); 54 let suite: Suite = serde_json::from_str(&vectors).expect("verified Profile vectors must parse"); 55 assert_eq!(suite.suite, "verified_profile_event"); 56 assert_eq!(suite.contract_version, "1.0.0"); 57 assert!(!suite.vectors.is_empty()); 58 59 for vector in &suite.vectors { 60 execute(vector); 61 } 62 } 63 64 fn conformance_vectors() -> Cow<'static, str> { 65 let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH); 66 match fs::read_to_string(&workspace_path) { 67 Ok(canonical) => { 68 assert_eq!( 69 canonical, 70 PACKAGED_VECTORS, 71 "packaged verified Profile vectors must match {}", 72 workspace_path.display() 73 ); 74 Cow::Owned(canonical) 75 } 76 Err(error) 77 if error.kind() == std::io::ErrorKind::NotFound 78 && !Path::new(env!("CARGO_MANIFEST_DIR")) 79 .join(WORKSPACE_CONTRACT_MARKER_PATH) 80 .is_file() => 81 { 82 Cow::Borrowed(PACKAGED_VECTORS) 83 } 84 Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), 85 } 86 } 87 88 fn execute(vector: &Vector) { 89 match vector.kind.as_str() { 90 "event.verify_nip01.valid" => verify_valid(vector), 91 "event.verify_nip01.invalid_id" => verify_invalid_id(vector), 92 "event.verify_nip01.invalid_signature" => verify_invalid_signature(vector), 93 "event.wire.invalid_public_key" => reject_invalid_wire_public_key(vector), 94 "event.verify_nip01.kind_overflow" => verify_kind_overflow(vector), 95 "profile.verify_and_admit.valid" => profile_admit_valid(vector), 96 "profile.verify_and_admit.invalid_kind" => profile_admit_invalid_kind(vector), 97 "profile.verify_and_admit.invalid_metadata" => profile_admit_invalid_metadata(vector), 98 "profile.select_equal_time_head" => profile_select_equal_time_head(vector), 99 kind => panic!("{} uses unsupported vector kind {kind}", vector.id), 100 } 101 } 102 103 fn verify_valid(vector: &Vector) { 104 let verified = verify_nip01_event(canonical_envelope(input_str(vector, "event_json"))) 105 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 106 assert_eq!( 107 verified.event().id_hex(), 108 expected_str(vector, "event_id"), 109 "{}", 110 vector.id 111 ); 112 assert_eq!( 113 u64::from(verified.event().kind_u32()), 114 vector.expected["kind"].as_u64().expect("expected.kind"), 115 "{}", 116 vector.id 117 ); 118 } 119 120 fn verify_invalid_id(vector: &Vector) { 121 let error = verify_nip01_event(unchecked_id_envelope(input_str(vector, "event_json"))) 122 .expect_err("mismatched event id must fail"); 123 assert_eq!(error.code(), expected_str(vector, "error")); 124 assert!(matches!( 125 error, 126 RadrootsNip01VerificationError::IdMismatch { .. } 127 )); 128 } 129 130 fn verify_invalid_signature(vector: &Vector) { 131 let error = verify_nip01_event(canonical_envelope(input_str(vector, "event_json"))) 132 .expect_err("invalid event signature must fail"); 133 assert_eq!(error.code(), expected_str(vector, "error")); 134 assert_eq!(error, RadrootsNip01VerificationError::SignatureInvalid); 135 } 136 137 fn reject_invalid_wire_public_key(vector: &Vector) { 138 let error = Nip01EventWire::parse_json(input_str(vector, "event_json")) 139 .expect_err("invalid secp256k1 public key must fail wire parsing"); 140 assert_eq!(expected_str(vector, "error"), "invalid_public_key"); 141 assert_eq!( 142 error, 143 EventWireError::InvalidIdentifier { 144 field: "pubkey", 145 error: ParseError::InvalidPublicKey, 146 } 147 ); 148 } 149 150 fn verify_kind_overflow(vector: &Vector) { 151 let error = verify_nip01_event(canonical_envelope(input_str(vector, "event_json"))) 152 .expect_err("out-of-range event kind must fail"); 153 assert_eq!(error.code(), expected_str(vector, "error")); 154 assert!(matches!( 155 error, 156 RadrootsNip01VerificationError::KindOutOfRange { kind } 157 if u64::from(kind) == vector.expected["kind"].as_u64().expect("expected.kind") 158 )); 159 } 160 161 fn profile_admit_valid(vector: &Vector) { 162 let admitted = admitted_event(input_str(vector, "event_json"), &vector.id); 163 assert_eq!(admitted.event().id_hex(), expected_str(vector, "event_id")); 164 assert_eq!( 165 admitted.verified_event().event().id_hex(), 166 expected_str(vector, "event_id") 167 ); 168 assert_eq!( 169 serde_json::to_value(admitted.event().tags_as_vec()).expect("tags"), 170 vector.expected["tags"] 171 ); 172 assert_eq!(projected_metadata(&admitted), vector.expected["projected"]); 173 assert_eq!( 174 serde_json::to_value(admitted.metadata().residual_fields()).expect("residual fields"), 175 vector.expected["residual_fields"] 176 ); 177 let (verified, metadata) = admitted.into_parts(); 178 assert_eq!(verified.event().id_hex(), expected_str(vector, "event_id")); 179 assert!(metadata.name().is_none()); 180 } 181 182 fn profile_admit_invalid_kind(vector: &Vector) { 183 let error = verify_and_admit_profile_event(canonical_envelope(input_str(vector, "event_json"))) 184 .expect_err("verified non-Profile kind must fail admission"); 185 assert_eq!(error.code(), expected_str(vector, "error")); 186 assert!(matches!( 187 error, 188 RadrootsProfileAdmissionError::InvalidKind { expected: 0, actual } 189 if u64::from(actual) == vector.expected["actual"].as_u64().expect("expected.actual") 190 )); 191 } 192 193 fn profile_admit_invalid_metadata(vector: &Vector) { 194 let error = verify_and_admit_profile_event(canonical_envelope(input_str(vector, "event_json"))) 195 .expect_err("verified Profile with non-object content must fail admission"); 196 assert_eq!(error.code(), expected_str(vector, "error")); 197 assert_eq!( 198 error, 199 RadrootsProfileAdmissionError::Metadata(RadrootsProfileMetadataParseError::RootNotObject) 200 ); 201 } 202 203 fn profile_select_equal_time_head(vector: &Vector) { 204 let first = admitted_event(input_str(vector, "first_event_json"), &vector.id); 205 let second = admitted_event(input_str(vector, "second_event_json"), &vector.id); 206 assert_eq!( 207 first.event().created_at_u64(), 208 second.event().created_at_u64() 209 ); 210 assert_eq!( 211 selected_event_id(&first, &second), 212 expected_str(vector, "event_id") 213 ); 214 assert_eq!( 215 selected_event_id(&second, &first), 216 expected_str(vector, "event_id") 217 ); 218 assert_eq!( 219 first.event().created_at_u64(), 220 vector.expected["created_at"] 221 .as_u64() 222 .expect("expected.created_at") 223 ); 224 } 225 226 fn selected_event_id( 227 current: &RadrootsAdmittedProfileEvent, 228 candidate: &RadrootsAdmittedProfileEvent, 229 ) -> String { 230 let current: CurrentEventHead = head_candidate(current).into(); 231 match select_event_head(head_candidate(candidate), Some(¤t)) { 232 EventHeadDecision::Applied(head) => head.event_id.into_string(), 233 EventHeadDecision::SkippedDuplicate 234 | EventHeadDecision::SkippedOlder 235 | EventHeadDecision::SkippedSameTimestampHigherEventId => current.event_id.into_string(), 236 EventHeadDecision::CoordinateMismatch => { 237 panic!("admitted Profile events from one author must share a coordinate") 238 } 239 } 240 } 241 242 fn head_candidate(admitted: &RadrootsAdmittedProfileEvent) -> EventHeadCandidate { 243 match event_head_candidate_for_event(admitted.event()).expect("Profile contract") { 244 EventHeadCandidateResult::Candidate(candidate) => candidate, 245 other => panic!("admitted Profile must be a replaceable head candidate: {other:?}"), 246 } 247 } 248 249 fn admitted_event(raw_json: &str, vector_id: &str) -> RadrootsAdmittedProfileEvent { 250 verify_and_admit_profile_event(canonical_envelope(raw_json)) 251 .unwrap_or_else(|error| panic!("{vector_id} failed: {error}")) 252 } 253 254 fn canonical_envelope(raw_json: &str) -> EventEnvelope { 255 Nip01EventWire::parse_json(raw_json) 256 .expect("canonical raw event") 257 .into_envelope() 258 .expect("event envelope") 259 } 260 261 fn unchecked_id_envelope(raw_json: &str) -> EventEnvelope { 262 let raw: RawEvent = serde_json::from_str(raw_json).expect("raw event"); 263 EventEnvelope::new(EventEnvelopeParts { 264 id: raw.id, 265 author: raw.pubkey, 266 created_at: raw.created_at, 267 kind: raw.kind, 268 tags: raw.tags, 269 content: raw.content, 270 sig: raw.sig, 271 }) 272 .expect("unchecked-id envelope") 273 } 274 275 fn projected_metadata(admitted: &RadrootsAdmittedProfileEvent) -> Value { 276 let metadata = admitted.metadata(); 277 let mut projected = Map::new(); 278 if let Some(value) = metadata.name() { 279 projected.insert("name".to_string(), Value::String(value.to_string())); 280 } 281 if let Some(value) = metadata.display_name() { 282 projected.insert("display_name".to_string(), Value::String(value.to_string())); 283 } 284 if let Some(value) = metadata.bot() { 285 projected.insert("bot".to_string(), Value::Bool(value)); 286 } 287 Value::Object(projected) 288 } 289 290 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 291 vector.input[field] 292 .as_str() 293 .unwrap_or_else(|| panic!("{} input.{field} must be a string", vector.id)) 294 } 295 296 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 297 vector.expected[field] 298 .as_str() 299 .unwrap_or_else(|| panic!("{} expected.{field} must be a string", vector.id)) 300 }