admission_authority.rs (16156B)
1 use super::{ 2 ConformanceVectorEntry, OperationsContractManifest, collect_non_empty_set, 3 validate_conformance_vector_file, validate_operation_case_kinds, 4 }; 5 use serde_json::{Map, Value}; 6 use std::{ 7 collections::{BTreeMap, BTreeSet}, 8 fs, 9 path::Path, 10 }; 11 12 pub(super) const ADMISSION_CONFORMANCE_VECTOR_RELATIVE: &str = 13 "contracts/conformance/vectors/event/verified_admission.v1.json"; 14 15 const REQUIRED_ADMISSION_PUBLIC_TYPES: [&str; 4] = [ 16 "RadrootsSignatureVerifiedEvent", 17 "RadrootsContractValidatedEvent", 18 "RadrootsAdmittedEvent", 19 "RadrootsEventAdmissionError", 20 ]; 21 22 const ADMISSION_CASE_KINDS: [&str; 2] = 23 ["event.admit_verified.valid", "event.admit_verified.invalid"]; 24 25 #[derive(Clone, Copy)] 26 enum ExpectedAdmission { 27 Valid { 28 variant: &'static str, 29 contract_id: &'static str, 30 }, 31 Invalid { 32 error_variant: &'static str, 33 error_code: &'static str, 34 }, 35 } 36 37 #[derive(Clone, Copy)] 38 struct AdmissionVectorExpectation { 39 id: &'static str, 40 outcome: ExpectedAdmission, 41 } 42 43 const ADMISSION_VECTOR_EXPECTATIONS: [AdmissionVectorExpectation; 13] = [ 44 AdmissionVectorExpectation { 45 id: "event_admit_verified_profile_001", 46 outcome: ExpectedAdmission::Valid { 47 variant: "profile", 48 contract_id: "radroots.profile.metadata.v1", 49 }, 50 }, 51 AdmissionVectorExpectation { 52 id: "event_admit_verified_root_update_002", 53 outcome: ExpectedAdmission::Valid { 54 variant: "root_post", 55 contract_id: "radroots.social.update.v1", 56 }, 57 }, 58 AdmissionVectorExpectation { 59 id: "event_admit_verified_root_photo_update_003", 60 outcome: ExpectedAdmission::Valid { 61 variant: "root_post", 62 contract_id: "radroots.social.photo_update.v1", 63 }, 64 }, 65 AdmissionVectorExpectation { 66 id: "event_admit_verified_root_ask_004", 67 outcome: ExpectedAdmission::Valid { 68 variant: "root_post", 69 contract_id: "radroots.social.ask.v1", 70 }, 71 }, 72 AdmissionVectorExpectation { 73 id: "event_admit_verified_post_to_reply_005", 74 outcome: ExpectedAdmission::Valid { 75 variant: "reply", 76 contract_id: "radroots.social.reply.v1", 77 }, 78 }, 79 AdmissionVectorExpectation { 80 id: "event_admit_verified_comment_006", 81 outcome: ExpectedAdmission::Valid { 82 variant: "comment", 83 contract_id: "radroots.social.comment.v1", 84 }, 85 }, 86 AdmissionVectorExpectation { 87 id: "event_admit_verified_deletion_request_007", 88 outcome: ExpectedAdmission::Valid { 89 variant: "deletion_request", 90 contract_id: "radroots.social.deletion_request.v1", 91 }, 92 }, 93 AdmissionVectorExpectation { 94 id: "event_admit_verified_food_availability_008", 95 outcome: ExpectedAdmission::Valid { 96 variant: "food_availability", 97 contract_id: "radroots.food.availability.v1", 98 }, 99 }, 100 AdmissionVectorExpectation { 101 id: "event_admit_verified_operational_fallback_009", 102 outcome: ExpectedAdmission::Valid { 103 variant: "contract_validated", 104 contract_id: "radroots.operational_listing.published.v1", 105 }, 106 }, 107 AdmissionVectorExpectation { 108 id: "event_admit_verified_unsupported_kind_010", 109 outcome: ExpectedAdmission::Invalid { 110 error_variant: "contract_match", 111 error_code: "unsupported_kind", 112 }, 113 }, 114 AdmissionVectorExpectation { 115 id: "event_admit_verified_generic_nip99_excluded_011", 116 outcome: ExpectedAdmission::Invalid { 117 error_variant: "contract_match", 118 error_code: "unsupported_shape", 119 }, 120 }, 121 AdmissionVectorExpectation { 122 id: "event_admit_verified_operational_invalid_shape_012", 123 outcome: ExpectedAdmission::Invalid { 124 error_variant: "contract_validation", 125 error_code: "missing_tag", 126 }, 127 }, 128 AdmissionVectorExpectation { 129 id: "event_admit_verified_ambiguous_food_markers_013", 130 outcome: ExpectedAdmission::Invalid { 131 error_variant: "food_availability", 132 error_code: "food_profile_ambiguous", 133 }, 134 }, 135 ]; 136 137 pub(super) fn validate_admission_operation_authority( 138 manifest: &OperationsContractManifest, 139 workspace_root: &Path, 140 ) -> Result<(), String> { 141 let vector = validate_conformance_vector_file( 142 &workspace_root.join(ADMISSION_CONFORMANCE_VECTOR_RELATIVE), 143 &manifest.contract.version, 144 )?; 145 validate_admission_operation_inventory(manifest, &vector)?; 146 validate_source_witnesses(workspace_root)?; 147 Ok(()) 148 } 149 150 pub(super) fn validate_admission_operation_inventory( 151 manifest: &OperationsContractManifest, 152 vector: &super::ConformanceVectorFile, 153 ) -> Result<(), String> { 154 validate_manifest_authority(manifest, vector)?; 155 validate_vector_inventory(&vector.vectors) 156 } 157 158 fn validate_manifest_authority( 159 manifest: &OperationsContractManifest, 160 vector: &super::ConformanceVectorFile, 161 ) -> Result<(), String> { 162 let shared_types = collect_non_empty_set( 163 &manifest.shared_types.public, 164 "verified admission shared_types.public", 165 )?; 166 for required in REQUIRED_ADMISSION_PUBLIC_TYPES { 167 if !shared_types.contains(required) { 168 return Err(format!( 169 "verified admission authority requires shared public type {required}" 170 )); 171 } 172 } 173 174 let actual_keys = manifest 175 .operations 176 .iter() 177 .filter(|(key, operation)| { 178 key.starts_with("event_admit_verified") 179 || operation.id.starts_with("event.admit_verified") 180 || operation.conformance.vector == ADMISSION_CONFORMANCE_VECTOR_RELATIVE 181 }) 182 .map(|(key, _)| key.as_str()) 183 .collect::<BTreeSet<_>>(); 184 let expected_keys = BTreeSet::from(["event_admit_verified"]); 185 if actual_keys != expected_keys { 186 return Err(format!( 187 "verified admission operation authority drift: expected {expected_keys:?}, got {actual_keys:?}" 188 )); 189 } 190 191 let operation = manifest 192 .operations 193 .get("event_admit_verified") 194 .ok_or_else(|| { 195 "verified admission operation event_admit_verified is required".to_string() 196 })?; 197 require_scalar("domain", &operation.domain, "event")?; 198 require_scalar("id", &operation.id, "event.admit_verified")?; 199 require_scalar("stability", &operation.stability, "beta")?; 200 require_scalar("error_class", &operation.error_class, "admission_error")?; 201 require_scalar("signing", &operation.signing, "none")?; 202 require_scalar("transport", &operation.transport, "none")?; 203 if !operation.deterministic { 204 return Err( 205 "verified admission operation deterministic drift: expected true, got false" 206 .to_string(), 207 ); 208 } 209 require_sequence( 210 "inputs", 211 &operation.inputs, 212 &["RadrootsSignatureVerifiedEvent"], 213 )?; 214 require_sequence("outputs", &operation.outputs, &["RadrootsAdmittedEvent"])?; 215 require_sequence( 216 "implementation.rust_modules", 217 &operation.implementation.rust_modules, 218 &[ 219 "crates/event_codec/src/admission.rs", 220 "crates/event_codec/src/verification.rs", 221 ], 222 )?; 223 require_sequence( 224 "implementation.rust_types", 225 &operation.implementation.rust_types, 226 &[ 227 "radroots_event_codec::admission::RadrootsAdmittedEvent", 228 "radroots_event_codec::admission::RadrootsEventAdmissionError", 229 "radroots_event_codec::verify::RadrootsContractValidatedEvent", 230 "radroots_event_codec::verify::RadrootsSignatureVerifiedEvent", 231 ], 232 )?; 233 require_scalar( 234 "conformance.vector", 235 &operation.conformance.vector, 236 ADMISSION_CONFORMANCE_VECTOR_RELATIVE, 237 )?; 238 validate_operation_case_kinds(operation, vector)?; 239 require_sequence( 240 "conformance.case_kinds", 241 &operation.conformance.case_kinds, 242 &ADMISSION_CASE_KINDS, 243 ) 244 } 245 246 fn validate_source_witnesses(workspace_root: &Path) -> Result<(), String> { 247 for (relative, fragments) in [ 248 ( 249 "crates/event_codec/src/admission.rs", 250 &[ 251 "pub enum RadrootsAdmittedEvent", 252 "pub enum RadrootsEventAdmissionError", 253 "pub fn admit_verified_event(", 254 ][..], 255 ), 256 ( 257 "crates/event_codec/src/verification/v1.rs", 258 &[ 259 "pub struct RadrootsSignatureVerifiedEvent", 260 "pub struct RadrootsContractValidatedEvent", 261 ][..], 262 ), 263 ] { 264 let source = fs::read_to_string(workspace_root.join(relative)).map_err(|error| { 265 format!("failed to read verified admission witness {relative}: {error}") 266 })?; 267 for fragment in fragments { 268 if !source.contains(fragment) { 269 return Err(format!( 270 "verified admission witness {relative} is missing `{fragment}`" 271 )); 272 } 273 } 274 } 275 Ok(()) 276 } 277 278 fn validate_vector_inventory(vectors: &[ConformanceVectorEntry]) -> Result<(), String> { 279 let expected = ADMISSION_VECTOR_EXPECTATIONS 280 .iter() 281 .map(|entry| (entry.id, entry.outcome)) 282 .collect::<BTreeMap<_, _>>(); 283 let mut actual = BTreeMap::new(); 284 let mut event_ids = BTreeSet::new(); 285 286 for vector in vectors { 287 let expectation = expected.get(vector.id.as_str()).ok_or_else(|| { 288 format!( 289 "verified admission conformance vector has unexpected id {}", 290 vector.id 291 ) 292 })?; 293 if actual 294 .insert(vector.id.as_str(), vector.kind.as_str()) 295 .is_some() 296 { 297 return Err(format!( 298 "verified admission conformance vector has duplicate id {}", 299 vector.id 300 )); 301 } 302 validate_vector(vector, *expectation, &mut event_ids)?; 303 } 304 305 let expected_inventory = ADMISSION_VECTOR_EXPECTATIONS 306 .iter() 307 .map(|entry| { 308 let kind = match entry.outcome { 309 ExpectedAdmission::Valid { .. } => "event.admit_verified.valid", 310 ExpectedAdmission::Invalid { .. } => "event.admit_verified.invalid", 311 }; 312 (entry.id, kind) 313 }) 314 .collect::<BTreeMap<_, _>>(); 315 if actual != expected_inventory { 316 return Err(format!( 317 "verified admission conformance inventory drift: expected {expected_inventory:?}, got {actual:?}" 318 )); 319 } 320 Ok(()) 321 } 322 323 fn validate_vector( 324 vector: &ConformanceVectorEntry, 325 expectation: ExpectedAdmission, 326 event_ids: &mut BTreeSet<String>, 327 ) -> Result<(), String> { 328 let input = exact_object(&vector.input, &["event"], &format!("{}.input", vector.id))?; 329 let event = exact_object( 330 &input["event"], 331 &[ 332 "content", 333 "created_at", 334 "id", 335 "kind", 336 "pubkey", 337 "sig", 338 "tags", 339 ], 340 &format!("{}.input.event", vector.id), 341 )?; 342 let event_id = required_string(event, "id", &vector.id)?; 343 let pubkey = required_string(event, "pubkey", &vector.id)?; 344 let signature = required_string(event, "sig", &vector.id)?; 345 require_lower_hex(event_id, 64, "event id", &vector.id)?; 346 require_lower_hex(pubkey, 64, "pubkey", &vector.id)?; 347 require_lower_hex(signature, 128, "signature", &vector.id)?; 348 if !event_ids.insert(event_id.to_string()) { 349 return Err(format!( 350 "verified admission vector {} reuses event id {event_id}", 351 vector.id 352 )); 353 } 354 if !event["created_at"].is_u64() 355 || !event["kind"].is_u64() 356 || !event["tags"].is_array() 357 || !event["content"].is_string() 358 { 359 return Err(format!( 360 "verified admission vector {} must contain a complete typed signed event", 361 vector.id 362 )); 363 } 364 365 match expectation { 366 ExpectedAdmission::Valid { 367 variant, 368 contract_id, 369 } => { 370 if vector.kind != "event.admit_verified.valid" { 371 return Err(format!( 372 "verified admission vector {} kind drift", 373 vector.id 374 )); 375 } 376 let expected = exact_object( 377 vector.expected_value()?, 378 &["contract_id", "event_id", "variant"], 379 &format!("{}.expected", vector.id), 380 )?; 381 require_expected(expected, "variant", variant, &vector.id)?; 382 require_expected(expected, "contract_id", contract_id, &vector.id)?; 383 require_expected(expected, "event_id", event_id, &vector.id)?; 384 } 385 ExpectedAdmission::Invalid { 386 error_variant, 387 error_code, 388 } => { 389 if vector.kind != "event.admit_verified.invalid" { 390 return Err(format!( 391 "verified admission vector {} kind drift", 392 vector.id 393 )); 394 } 395 let expected = exact_object( 396 vector.expected_value()?, 397 &["error_code", "error_variant", "event_id"], 398 &format!("{}.expected", vector.id), 399 )?; 400 require_expected(expected, "error_variant", error_variant, &vector.id)?; 401 require_expected(expected, "error_code", error_code, &vector.id)?; 402 require_expected(expected, "event_id", event_id, &vector.id)?; 403 } 404 } 405 Ok(()) 406 } 407 408 fn exact_object<'a>( 409 value: &'a Value, 410 fields: &[&str], 411 label: &str, 412 ) -> Result<&'a Map<String, Value>, String> { 413 let object = value 414 .as_object() 415 .ok_or_else(|| format!("verified admission {label} must be an object"))?; 416 let actual = object.keys().map(String::as_str).collect::<BTreeSet<_>>(); 417 let expected = fields.iter().copied().collect::<BTreeSet<_>>(); 418 if actual != expected { 419 return Err(format!( 420 "verified admission {label} field inventory drift: expected {expected:?}, got {actual:?}" 421 )); 422 } 423 Ok(object) 424 } 425 426 fn required_string<'a>( 427 object: &'a Map<String, Value>, 428 field: &str, 429 vector_id: &str, 430 ) -> Result<&'a str, String> { 431 object[field].as_str().ok_or_else(|| { 432 format!("verified admission vector {vector_id} field {field} must be a string") 433 }) 434 } 435 436 fn require_lower_hex( 437 value: &str, 438 length: usize, 439 label: &str, 440 vector_id: &str, 441 ) -> Result<(), String> { 442 if value.len() != length 443 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) 444 || value.bytes().any(|byte| byte.is_ascii_uppercase()) 445 { 446 return Err(format!( 447 "verified admission vector {vector_id} {label} must be {length} lowercase hex characters" 448 )); 449 } 450 Ok(()) 451 } 452 453 fn require_expected( 454 object: &Map<String, Value>, 455 field: &str, 456 expected: &str, 457 vector_id: &str, 458 ) -> Result<(), String> { 459 let actual = required_string(object, field, vector_id)?; 460 if actual != expected { 461 return Err(format!( 462 "verified admission vector {vector_id} expected.{field} drift: expected {expected}, got {actual}" 463 )); 464 } 465 Ok(()) 466 } 467 468 fn require_scalar(field: &str, actual: &str, expected: &str) -> Result<(), String> { 469 if actual != expected { 470 return Err(format!( 471 "verified admission operation {field} drift: expected {expected}, got {actual}" 472 )); 473 } 474 Ok(()) 475 } 476 477 fn require_sequence(field: &str, actual: &[String], expected: &[&str]) -> Result<(), String> { 478 if !actual 479 .iter() 480 .map(String::as_str) 481 .eq(expected.iter().copied()) 482 { 483 return Err(format!( 484 "verified admission operation {field} drift: expected {expected:?}, got {actual:?}" 485 )); 486 } 487 Ok(()) 488 }