verified_deletion_conformance.rs (33604B)
1 #![cfg(feature = "json")] 2 3 mod support; 4 5 use std::{ 6 borrow::Cow, 7 collections::{BTreeMap, BTreeSet}, 8 fs, 9 path::Path, 10 }; 11 12 use radroots_event::{ 13 envelope::EventEnvelope, 14 envelope::EventEnvelopeLimits, 15 envelope::EventEnvelopeParts, 16 post::deletion::{ 17 AuthoredNip09DeletionRequest, Nip09DeletionAddressTarget, Nip09DeletionError, 18 Nip09DeletionEventTarget, RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES, 19 RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES, 20 RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES, RADROOTS_NIP09_DELETION_TAG_MAX_COUNT, 21 RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT, 22 RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES, 23 }, 24 }; 25 use radroots_event_codec::{ 26 admission::deletion::verify_and_admit_nip09_deletion_request_event, 27 decode::deletion::{ 28 RadrootsInboundNip09DeletionProjection, project_verified_nip09_deletion_request_event, 29 }, 30 encode::deletion::authored_nip09_deletion_request_to_wire_parts, 31 verify::verify_nip01_event, 32 }; 33 use serde::{Deserialize, Serialize}; 34 use serde_json::{Value, json}; 35 36 const PACKAGED_VECTORS: &str = include_str!("fixtures/deletion_verified_profile.v1.json"); 37 const WORKSPACE_VECTOR_PATH: &str = 38 "../../contracts/conformance/vectors/deletion/verified_profile.v1.json"; 39 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; 40 41 const AUTHORED_VALID: &str = "social.deletion_request.build_authored_draft.valid"; 42 const AUTHORED_INVALID: &str = "social.deletion_request.build_authored_draft.invalid"; 43 const PROJECT_VALID: &str = "social.deletion_request.project_verified_event.valid"; 44 const PROJECT_INVALID: &str = "social.deletion_request.project_verified_event.invalid"; 45 const ADMIT_VALID: &str = "social.deletion_request.verify_and_admit_event.valid"; 46 const ADMIT_INVALID: &str = "social.deletion_request.verify_and_admit_event.invalid"; 47 48 const VECTOR_EXPECTATIONS: [(&str, &str); 80] = [ 49 ( 50 "nip09_authored_event_target_min_kind_empty_content", 51 AUTHORED_VALID, 52 ), 53 ( 54 "nip09_authored_event_target_max_kind_unicode_content", 55 AUTHORED_VALID, 56 ), 57 ( 58 "nip09_authored_coordinate_kind_0_empty_identifier", 59 AUTHORED_VALID, 60 ), 61 ( 62 "nip09_authored_coordinate_kind_3_empty_identifier", 63 AUTHORED_VALID, 64 ), 65 ( 66 "nip09_authored_coordinate_kind_10000_empty_identifier", 67 AUTHORED_VALID, 68 ), 69 ( 70 "nip09_authored_coordinate_kind_19999_empty_identifier", 71 AUTHORED_VALID, 72 ), 73 ( 74 "nip09_authored_coordinate_kind_30000_empty_identifier", 75 AUTHORED_VALID, 76 ), 77 ( 78 "nip09_authored_coordinate_kind_39999_opaque_identifier", 79 AUTHORED_VALID, 80 ), 81 ( 82 "nip09_authored_mixed_targets_canonical_order", 83 AUTHORED_VALID, 84 ), 85 ("nip09_authored_content_bytes_exact", AUTHORED_VALID), 86 ("nip09_authored_tag_count_exact", AUTHORED_VALID), 87 ("nip09_authored_tag_element_bytes_exact", AUTHORED_VALID), 88 ("nip09_authored_tag_bytes_exact", AUTHORED_VALID), 89 ("nip09_authored_event_wire_bytes_exact", AUTHORED_VALID), 90 ("nip09_authored_event_target_invalid", AUTHORED_INVALID), 91 ( 92 "nip09_authored_event_target_kind_out_of_range", 93 AUTHORED_INVALID, 94 ), 95 ( 96 "nip09_authored_address_target_invalid_format", 97 AUTHORED_INVALID, 98 ), 99 ( 100 "nip09_authored_address_target_invalid_pubkey", 101 AUTHORED_INVALID, 102 ), 103 ( 104 "nip09_authored_address_target_unsupported_kind", 105 AUTHORED_INVALID, 106 ), 107 ( 108 "nip09_authored_replaceable_identifier_nonempty", 109 AUTHORED_INVALID, 110 ), 111 ( 112 "nip09_authored_event_target_duplicate_normalized", 113 AUTHORED_INVALID, 114 ), 115 ( 116 "nip09_authored_address_target_duplicate_normalized", 117 AUTHORED_INVALID, 118 ), 119 ("nip09_authored_target_missing", AUTHORED_INVALID), 120 ("nip09_authored_content_bytes_overflow", AUTHORED_INVALID), 121 ( 122 "nip09_authored_tag_count_overflow_precedes_duplicate", 123 AUTHORED_INVALID, 124 ), 125 ( 126 "nip09_authored_tag_element_bytes_overflow", 127 AUTHORED_INVALID, 128 ), 129 ( 130 "nip09_authored_tag_bytes_overflow_precedes_duplicate", 131 AUTHORED_INVALID, 132 ), 133 ( 134 "nip09_authored_event_wire_bytes_overflow_precedes_duplicate", 135 AUTHORED_INVALID, 136 ), 137 ("nip09_project_signed_event_target_without_k", PROJECT_VALID), 138 ( 139 "nip09_project_signed_address_replaceable_boundaries", 140 PROJECT_VALID, 141 ), 142 ("nip09_project_signed_addressable_boundaries", PROJECT_VALID), 143 ("nip09_project_signed_mixed_raw_retention", PROJECT_VALID), 144 ( 145 "nip09_project_signed_duplicate_targets_first_provenance", 146 PROJECT_VALID, 147 ), 148 ( 149 "nip09_project_signed_canonical_effect_sorting", 150 PROJECT_VALID, 151 ), 152 ( 153 "nip09_project_signed_kind_advisory_diagnostics", 154 PROJECT_VALID, 155 ), 156 ( 157 "nip09_project_signed_event_target_conflict_unprovable", 158 PROJECT_VALID, 159 ), 160 ( 161 "nip09_project_signed_trailing_kind_and_unknown_tags", 162 PROJECT_VALID, 163 ), 164 ( 165 "nip09_project_signed_unicode_whitespace_control_content", 166 PROJECT_VALID, 167 ), 168 ("nip09_project_signed_content_bytes_exact", PROJECT_VALID), 169 ("nip09_project_signed_tag_count_exact", PROJECT_VALID), 170 ( 171 "nip09_project_signed_tag_element_count_exact", 172 PROJECT_VALID, 173 ), 174 ( 175 "nip09_project_signed_tag_element_bytes_exact_multibyte", 176 PROJECT_VALID, 177 ), 178 ("nip09_project_signed_tag_bytes_exact", PROJECT_VALID), 179 ( 180 "nip09_project_signed_event_wire_bytes_exact_max_created_at", 181 PROJECT_VALID, 182 ), 183 ( 184 "nip09_project_signed_event_wire_short_created_at_width", 185 PROJECT_VALID, 186 ), 187 ("nip09_project_signed_kind_advisory_min_max", PROJECT_VALID), 188 ("nip09_project_signed_wrong_kind", PROJECT_INVALID), 189 ( 190 "nip09_project_signed_content_bytes_overflow", 191 PROJECT_INVALID, 192 ), 193 ("nip09_project_signed_tag_count_overflow", PROJECT_INVALID), 194 ( 195 "nip09_project_signed_tag_element_count_overflow", 196 PROJECT_INVALID, 197 ), 198 ( 199 "nip09_project_signed_tag_element_bytes_overflow", 200 PROJECT_INVALID, 201 ), 202 ("nip09_project_signed_tag_bytes_overflow", PROJECT_INVALID), 203 ( 204 "nip09_project_signed_event_wire_bytes_overflow", 205 PROJECT_INVALID, 206 ), 207 ("nip09_project_signed_event_target_shape", PROJECT_INVALID), 208 ("nip09_project_signed_event_target_empty", PROJECT_INVALID), 209 ("nip09_project_signed_event_target_invalid", PROJECT_INVALID), 210 ("nip09_project_signed_address_target_shape", PROJECT_INVALID), 211 ( 212 "nip09_project_signed_address_target_missing_colon", 213 PROJECT_INVALID, 214 ), 215 ( 216 "nip09_project_signed_address_target_invalid_pubkey", 217 PROJECT_INVALID, 218 ), 219 ( 220 "nip09_project_signed_address_target_unsupported_kind", 221 PROJECT_INVALID, 222 ), 223 ( 224 "nip09_project_signed_address_target_identifier_forbidden", 225 PROJECT_INVALID, 226 ), 227 ("nip09_project_signed_target_missing", PROJECT_INVALID), 228 ( 229 "nip09_project_signed_first_malformed_event_target", 230 PROJECT_INVALID, 231 ), 232 ( 233 "nip09_project_signed_first_malformed_address_target", 234 PROJECT_INVALID, 235 ), 236 ( 237 "nip09_project_signed_kind_precedes_content", 238 PROJECT_INVALID, 239 ), 240 ( 241 "nip09_project_signed_content_precedes_tag_count", 242 PROJECT_INVALID, 243 ), 244 ( 245 "nip09_project_signed_tag_count_precedes_element_count", 246 PROJECT_INVALID, 247 ), 248 ( 249 "nip09_project_signed_element_count_precedes_element_size", 250 PROJECT_INVALID, 251 ), 252 ( 253 "nip09_project_signed_element_size_precedes_tag_bytes", 254 PROJECT_INVALID, 255 ), 256 ( 257 "nip09_project_signed_tag_bytes_precedes_wire", 258 PROJECT_INVALID, 259 ), 260 ( 261 "nip09_project_signed_wire_precedes_target_parse", 262 PROJECT_INVALID, 263 ), 264 ( 265 "nip09_project_signed_target_parse_precedes_missing_union", 266 PROJECT_INVALID, 267 ), 268 ("nip09_admit_signed_event_target", ADMIT_VALID), 269 ("nip09_admit_signed_address_target", ADMIT_VALID), 270 ("nip09_admit_signed_mixed_tolerant_projection", ADMIT_VALID), 271 ("nip09_admit_invalid_signature", ADMIT_INVALID), 272 ("nip09_admit_id_mismatch", ADMIT_INVALID), 273 ("nip09_admit_wrong_kind", ADMIT_INVALID), 274 ("nip09_admit_invalid_target", ADMIT_INVALID), 275 ("nip09_admit_target_missing", ADMIT_INVALID), 276 ]; 277 278 #[derive(Debug, Deserialize)] 279 #[serde(deny_unknown_fields)] 280 struct Suite { 281 suite: String, 282 contract_version: String, 283 vectors: Vec<Vector>, 284 } 285 286 #[derive(Debug, Deserialize)] 287 #[serde(deny_unknown_fields)] 288 struct Vector { 289 id: String, 290 kind: String, 291 input: Value, 292 expected: Value, 293 } 294 295 #[derive(Clone, Debug, Deserialize, Serialize)] 296 #[serde(deny_unknown_fields)] 297 struct RawEvent { 298 id: String, 299 pubkey: String, 300 created_at: u64, 301 kind: u32, 302 tags: Vec<Vec<String>>, 303 content: String, 304 sig: String, 305 } 306 307 #[test] 308 fn deletion_operation_vectors_execute_the_declared_public_functions() { 309 let vectors = conformance_vectors(); 310 let raw_suite: Value = serde_json::from_str(&vectors).expect("NIP-09 JSON must parse"); 311 validate_no_forbidden_corpus_metadata(&raw_suite, "$") 312 .unwrap_or_else(|error| panic!("{error}")); 313 let suite: Suite = serde_json::from_str(&vectors).expect("NIP-09 vectors must parse"); 314 assert_eq!(suite.suite, "nip09_deletion_request_profile"); 315 assert_eq!(suite.contract_version, "1.0.0"); 316 assert_vector_inventory(&suite.vectors); 317 318 for vector in &suite.vectors { 319 execute(vector); 320 assert_boundary_case(vector); 321 } 322 } 323 324 fn validate_no_forbidden_corpus_metadata(value: &Value, path: &str) -> Result<(), String> { 325 match value { 326 Value::Object(object) => { 327 for (key, child) in object { 328 if is_forbidden_metadata_key(key) { 329 return Err(format!( 330 "frozen NIP-09 corpus contains forbidden key {path}.{key}" 331 )); 332 } 333 validate_no_forbidden_corpus_metadata(child, &format!("{path}.{key}"))?; 334 } 335 } 336 Value::Array(values) => { 337 for (index, child) in values.iter().enumerate() { 338 validate_no_forbidden_corpus_metadata(child, &format!("{path}[{index}]"))?; 339 } 340 } 341 Value::String(string) => { 342 if contains_nsec_material(string) { 343 return Err(format!( 344 "frozen NIP-09 corpus contains an nsec value at {path}" 345 )); 346 } 347 if contains_approved_fixture_secret(string) { 348 return Err(format!( 349 "frozen NIP-09 corpus contains an approved fixture secret at {path}" 350 )); 351 } 352 } 353 _ => {} 354 } 355 Ok(()) 356 } 357 358 fn is_forbidden_metadata_key(key: &str) -> bool { 359 let normalized = key.to_ascii_lowercase(); 360 matches!(normalized.as_str(), "base" | "mutation") 361 || normalized.contains("seed") 362 || normalized.contains("generator") 363 || normalized.contains("recipe") 364 || normalized.contains("secret_key") 365 || normalized.contains("private_key") 366 || normalized.contains("signing_key") 367 || normalized.contains("boundary") 368 || normalized.contains("authorization") 369 || normalized.contains("authorized") 370 || normalized.contains("cutoff") 371 || normalized.contains("evaluator") 372 || normalized.contains("store_mutation") 373 || normalized.contains("suppression") 374 || normalized.contains("suppressed") 375 || normalized == "effect" 376 || normalized.ends_with("_effect") 377 || normalized == "effects" 378 } 379 380 fn contains_nsec_material(value: &str) -> bool { 381 value.to_ascii_lowercase().contains("nsec1") 382 } 383 384 fn contains_approved_fixture_secret(value: &str) -> bool { 385 let normalized = value.to_ascii_lowercase(); 386 support::approved_fixture_identities() 387 .iter() 388 .any(|identity| normalized.contains(identity.secret_key_hex)) 389 } 390 391 #[test] 392 fn deletion_corpus_hygiene_recognizes_every_approved_fixture_secret() { 393 assert!(contains_nsec_material("NSEC1FORBIDDEN")); 394 for key in [ 395 "SeEd", 396 "GENERATOR", 397 "AuThOrIzAtIoN", 398 "CuToFf", 399 "Evaluator", 400 "SUPPRESSION", 401 "effects", 402 ] { 403 let value = Value::Object([(key.to_string(), Value::Null)].into_iter().collect()); 404 let error = validate_no_forbidden_corpus_metadata(&value, "$") 405 .expect_err("mixed-case generation and effect metadata must fail"); 406 assert!(error.contains(key), "{error}"); 407 } 408 409 for identity in support::approved_fixture_identities() { 410 assert!(contains_approved_fixture_secret(identity.secret_key_hex)); 411 assert!(contains_approved_fixture_secret( 412 format!("prefix{}suffix", identity.secret_key_hex).as_str() 413 )); 414 } 415 } 416 417 #[test] 418 fn deletion_corpus_shape_rejects_unknown_suite_and_vector_fields() { 419 let suite = json!({ 420 "suite": "nip09_deletion_request_profile", 421 "contract_version": "1.0.0", 422 "vectors": [], 423 "unexpected": null 424 }); 425 serde_json::from_value::<Suite>(suite).expect_err("unknown suite fields must fail"); 426 427 let vector = json!({ 428 "id": "nip09_unknown_shape", 429 "kind": AUTHORED_VALID, 430 "input": {}, 431 "expected": {}, 432 "unexpected": null 433 }); 434 serde_json::from_value::<Vector>(vector).expect_err("unknown vector fields must fail"); 435 } 436 437 fn conformance_vectors() -> Cow<'static, str> { 438 let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH); 439 match fs::read_to_string(&workspace_path) { 440 Ok(canonical) => { 441 assert_eq!( 442 canonical, 443 PACKAGED_VECTORS, 444 "packaged NIP-09 vectors must match {}", 445 workspace_path.display() 446 ); 447 Cow::Owned(canonical) 448 } 449 Err(error) 450 if error.kind() == std::io::ErrorKind::NotFound 451 && !Path::new(env!("CARGO_MANIFEST_DIR")) 452 .join(WORKSPACE_CONTRACT_MARKER_PATH) 453 .is_file() => 454 { 455 Cow::Borrowed(PACKAGED_VECTORS) 456 } 457 Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), 458 } 459 } 460 461 fn assert_vector_inventory(vectors: &[Vector]) { 462 let expected = VECTOR_EXPECTATIONS.into_iter().collect::<BTreeMap<_, _>>(); 463 let actual = vectors 464 .iter() 465 .map(|vector| (vector.id.as_str(), vector.kind.as_str())) 466 .collect::<BTreeMap<_, _>>(); 467 assert_eq!(vectors.len(), 80); 468 assert_eq!(actual, expected); 469 assert!(vectors.iter().all(|vector| vector.id.starts_with("nip09_"))); 470 471 let kinds = vectors.iter().fold(BTreeMap::new(), |mut counts, vector| { 472 *counts.entry(vector.kind.as_str()).or_insert(0usize) += 1; 473 counts 474 }); 475 assert_eq!( 476 kinds, 477 BTreeMap::from([ 478 (AUTHORED_INVALID, 14), 479 (AUTHORED_VALID, 14), 480 (PROJECT_INVALID, 26), 481 (PROJECT_VALID, 18), 482 (ADMIT_INVALID, 5), 483 (ADMIT_VALID, 3), 484 ]) 485 ); 486 487 assert_error_set( 488 vectors, 489 AUTHORED_INVALID, 490 &[ 491 "deletion_address_target_duplicate", 492 "deletion_address_target_invalid", 493 "deletion_content_too_large", 494 "deletion_event_target_duplicate", 495 "deletion_event_target_invalid", 496 "deletion_event_wire_too_large", 497 "deletion_tag_bytes_exceeded", 498 "deletion_tag_count_exceeded", 499 "deletion_tag_element_too_large", 500 "deletion_target_missing", 501 ], 502 ); 503 assert_error_set( 504 vectors, 505 PROJECT_INVALID, 506 &[ 507 "deletion_address_target_invalid", 508 "deletion_address_target_shape", 509 "deletion_content_too_large", 510 "deletion_event_target_invalid", 511 "deletion_event_target_shape", 512 "deletion_event_wire_too_large", 513 "deletion_tag_bytes_exceeded", 514 "deletion_tag_count_exceeded", 515 "deletion_tag_element_count_exceeded", 516 "deletion_tag_element_too_large", 517 "deletion_target_missing", 518 "unsupported_kind", 519 ], 520 ); 521 assert_error_set( 522 vectors, 523 ADMIT_INVALID, 524 &[ 525 "deletion_event_target_invalid", 526 "deletion_target_missing", 527 "id_mismatch", 528 "signature_invalid", 529 "unsupported_kind", 530 ], 531 ); 532 533 let diagnostics = vectors 534 .iter() 535 .filter(|vector| vector.kind == PROJECT_VALID || vector.kind == ADMIT_VALID) 536 .flat_map(|vector| { 537 vector.expected["diagnostics"] 538 .as_array() 539 .unwrap_or_else(|| panic!("{} expected.diagnostics must be an array", vector.id)) 540 }) 541 .map(|diagnostic| { 542 diagnostic["code"] 543 .as_str() 544 .expect("diagnostic.code must be a string") 545 }) 546 .collect::<BTreeSet<_>>(); 547 assert_eq!( 548 diagnostics, 549 BTreeSet::from([ 550 "deletion_kind_advisory_conflict_ignored", 551 "deletion_kind_advisory_duplicate_ignored", 552 "deletion_kind_advisory_invalid_ignored", 553 "deletion_kind_advisory_shape_ignored", 554 ]) 555 ); 556 557 for vector in vectors { 558 let input = vector 559 .input 560 .as_object() 561 .unwrap_or_else(|| panic!("{} input must be an object", vector.id)); 562 if vector.kind == AUTHORED_VALID || vector.kind == AUTHORED_INVALID { 563 assert_eq!( 564 input.keys().map(String::as_str).collect::<Vec<_>>(), 565 ["address_targets", "content", "event_targets"], 566 "{} authored input fields drifted", 567 vector.id 568 ); 569 } else { 570 assert_eq!( 571 input.keys().map(String::as_str).collect::<Vec<_>>(), 572 ["event_json"], 573 "{} signed input must contain only fixed event_json", 574 vector.id 575 ); 576 } 577 578 if vector.kind == PROJECT_VALID || vector.kind == ADMIT_VALID { 579 assert_eq!( 580 vector 581 .expected 582 .as_object() 583 .expect("valid expected object") 584 .keys() 585 .map(String::as_str) 586 .collect::<Vec<_>>(), 587 [ 588 "address_targets", 589 "contract_id", 590 "diagnostics", 591 "event_targets", 592 "kind_advisories", 593 "raw_tags", 594 ], 595 "{} projection output shape drifted", 596 vector.id 597 ); 598 } 599 } 600 } 601 602 fn assert_error_set(vectors: &[Vector], kind: &str, expected: &[&str]) { 603 let actual = vectors 604 .iter() 605 .filter(|vector| vector.kind == kind) 606 .map(|vector| expected_str(vector, "error")) 607 .collect::<BTreeSet<_>>(); 608 assert_eq!(actual, expected.iter().copied().collect(), "{kind}"); 609 } 610 611 fn execute(vector: &Vector) { 612 match vector.kind.as_str() { 613 AUTHORED_VALID => { 614 let request = 615 authored_request(vector).unwrap_or_else(|error| panic!("{}: {error}", vector.id)); 616 let first = authored_nip09_deletion_request_to_wire_parts(&request); 617 let second = authored_nip09_deletion_request_to_wire_parts(&request); 618 assert_eq!(first, second, "{} repeat encoding drifted", vector.id); 619 assert_eq!( 620 serde_json::to_value(first).expect("authored deletion result must serialize"), 621 vector.expected, 622 "{}", 623 vector.id 624 ); 625 } 626 AUTHORED_INVALID => { 627 let error = authored_request(vector).expect_err("invalid authored deletion must fail"); 628 assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id); 629 } 630 PROJECT_VALID => { 631 let verified = verify_nip01_event(fixture_envelope(vector)) 632 .unwrap_or_else(|error| panic!("{} verification failed: {error}", vector.id)); 633 let projection = project_verified_nip09_deletion_request_event(&verified) 634 .unwrap_or_else(|error| panic!("{} projection failed: {error}", vector.id)); 635 assert_eq!( 636 projection_snapshot(&projection), 637 vector.expected, 638 "{}", 639 vector.id 640 ); 641 } 642 PROJECT_INVALID => { 643 let verified = verify_nip01_event(fixture_envelope(vector)) 644 .unwrap_or_else(|error| panic!("{} verification failed: {error}", vector.id)); 645 let error = project_verified_nip09_deletion_request_event(&verified) 646 .expect_err("invalid verified deletion projection must fail"); 647 assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id); 648 } 649 ADMIT_VALID => { 650 let envelope = fixture_envelope(vector); 651 let expected_event = envelope.clone(); 652 let admitted = verify_and_admit_nip09_deletion_request_event(envelope) 653 .unwrap_or_else(|error| panic!("{} admission failed: {error}", vector.id)); 654 assert_eq!(admitted.event(), &expected_event, "{}", vector.id); 655 assert_eq!( 656 admitted.contract().id, 657 "radroots.social.deletion_request.v1" 658 ); 659 assert_eq!( 660 projection_snapshot(admitted.projection()), 661 vector.expected, 662 "{}", 663 vector.id 664 ); 665 let (verified, projection) = admitted.into_parts(); 666 assert_eq!(verified.event(), &expected_event, "{}", vector.id); 667 assert_eq!( 668 projection.contract_id(), 669 "radroots.social.deletion_request.v1" 670 ); 671 } 672 ADMIT_INVALID => { 673 let error = verify_and_admit_nip09_deletion_request_event(fixture_envelope(vector)) 674 .expect_err("invalid signed deletion must fail"); 675 assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id); 676 } 677 other => panic!("{} has unknown vector kind {other}", vector.id), 678 } 679 } 680 681 fn authored_request(vector: &Vector) -> Result<AuthoredNip09DeletionRequest, Nip09DeletionError> { 682 let event_targets = input_array(vector, "event_targets") 683 .iter() 684 .map(|target| { 685 let target = target 686 .as_object() 687 .unwrap_or_else(|| panic!("{} event target must be an object", vector.id)); 688 Nip09DeletionEventTarget::parse( 689 object_str(target, "event_id", &vector.id), 690 object_u32(target, "kind", &vector.id), 691 ) 692 }) 693 .collect::<Result<Vec<_>, _>>()?; 694 let address_targets = input_array(vector, "address_targets") 695 .iter() 696 .map(|target| { 697 Nip09DeletionAddressTarget::parse( 698 target 699 .as_str() 700 .unwrap_or_else(|| panic!("{} address target must be a string", vector.id)), 701 ) 702 }) 703 .collect::<Result<Vec<_>, _>>()?; 704 AuthoredNip09DeletionRequest::new(input_str(vector, "content"), event_targets, address_targets) 705 } 706 707 fn fixture_raw_event(vector: &Vector) -> RawEvent { 708 let event_json = input_str(vector, "event_json"); 709 let raw: RawEvent = serde_json::from_str(event_json) 710 .unwrap_or_else(|error| panic!("{} event_json failed to parse: {error}", vector.id)); 711 assert_eq!( 712 serde_json::to_string(&raw).expect("raw event must serialize"), 713 event_json, 714 "{} event_json must be compact and canonical", 715 vector.id 716 ); 717 raw 718 } 719 720 fn fixture_envelope(vector: &Vector) -> EventEnvelope { 721 let raw = fixture_raw_event(vector); 722 let mut limits = EventEnvelopeLimits::default(); 723 limits.max_content_bytes = limits.max_content_bytes.max(raw.content.len()); 724 limits.max_tag_count = limits.max_tag_count.max(raw.tags.len()); 725 limits.max_total_tag_elements = limits 726 .max_total_tag_elements 727 .max(raw.tags.iter().map(Vec::len).sum()); 728 limits.max_tag_element_bytes = limits.max_tag_element_bytes.max( 729 raw.tags 730 .iter() 731 .flat_map(|tag| tag.iter()) 732 .map(String::len) 733 .max() 734 .unwrap_or_default(), 735 ); 736 limits.max_total_tag_bytes = limits.max_total_tag_bytes.max(tag_bytes(&raw.tags)); 737 EventEnvelope::new_with_limits( 738 EventEnvelopeParts { 739 id: raw.id, 740 author: raw.pubkey, 741 created_at: raw.created_at, 742 kind: raw.kind, 743 tags: raw.tags, 744 content: raw.content, 745 sig: raw.sig, 746 }, 747 limits, 748 ) 749 .unwrap_or_else(|error| panic!("{} envelope failed: {error}", vector.id)) 750 } 751 752 fn projection_snapshot(projection: &RadrootsInboundNip09DeletionProjection) -> Value { 753 json!({ 754 "contract_id": projection.contract_id(), 755 "event_targets": projection.event_targets().iter().map(|target| json!({ 756 "tag_index": target.tag_index(), 757 "event_id": target.event_id().to_hex(), 758 "raw_tag": target.raw_tag(), 759 })).collect::<Vec<_>>(), 760 "address_targets": projection.address_targets().iter().map(|target| json!({ 761 "tag_index": target.tag_index(), 762 "coordinate": target.coordinate().as_str(), 763 "kind": target.coordinate().kind(), 764 "pubkey": target.coordinate().pubkey().to_hex(), 765 "identifier": target.coordinate().identifier(), 766 "raw_tag": target.raw_tag(), 767 })).collect::<Vec<_>>(), 768 "kind_advisories": projection.kind_advisories().iter().map(|advisory| json!({ 769 "tag_index": advisory.tag_index(), 770 "kind": advisory.kind(), 771 "raw_tag": advisory.raw_tag(), 772 })).collect::<Vec<_>>(), 773 "diagnostics": projection.diagnostics().iter().map(|diagnostic| json!({ 774 "code": diagnostic.code(), 775 "tag_index": diagnostic.tag_index(), 776 "raw_tag": diagnostic.raw_tag(), 777 })).collect::<Vec<_>>(), 778 "raw_tags": projection.raw_tags(), 779 }) 780 } 781 782 fn assert_boundary_case(vector: &Vector) { 783 match vector.id.as_str() { 784 "nip09_authored_content_bytes_exact" => { 785 assert_eq!( 786 input_str(vector, "content").len(), 787 RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES 788 ); 789 } 790 "nip09_authored_tag_count_exact" => { 791 assert_eq!( 792 vector.expected["tags"].as_array().expect("tags").len(), 793 RADROOTS_NIP09_DELETION_TAG_MAX_COUNT 794 ); 795 } 796 "nip09_authored_tag_element_bytes_exact" => { 797 assert_eq!( 798 input_array(vector, "address_targets")[0] 799 .as_str() 800 .expect("coordinate") 801 .len(), 802 RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES 803 ); 804 } 805 "nip09_authored_tag_bytes_exact" => { 806 let tags = vector.expected["tags"].as_array().expect("tags"); 807 assert_eq!( 808 value_tag_bytes(tags), 809 RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES 810 ); 811 } 812 "nip09_authored_event_wire_bytes_exact" => { 813 let request = authored_request(vector).expect("exact authored request"); 814 assert_eq!( 815 request.maximum_signed_event_wire_bytes(), 816 RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES 817 ); 818 } 819 "nip09_project_signed_content_bytes_exact" => { 820 assert_eq!( 821 fixture_raw_event(vector).content.len(), 822 RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES 823 ); 824 } 825 "nip09_project_signed_tag_count_exact" => { 826 assert_eq!( 827 fixture_raw_event(vector).tags.len(), 828 RADROOTS_NIP09_DELETION_TAG_MAX_COUNT 829 ); 830 } 831 "nip09_project_signed_tag_element_count_exact" => { 832 assert_eq!( 833 fixture_raw_event(vector) 834 .tags 835 .iter() 836 .map(Vec::len) 837 .sum::<usize>(), 838 RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT 839 ); 840 } 841 "nip09_project_signed_tag_element_bytes_exact_multibyte" => { 842 assert_eq!( 843 fixture_raw_event(vector) 844 .tags 845 .iter() 846 .flat_map(|tag| tag.iter()) 847 .map(String::len) 848 .max(), 849 Some(RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES) 850 ); 851 } 852 "nip09_project_signed_tag_bytes_exact" => { 853 assert_eq!( 854 tag_bytes(&fixture_raw_event(vector).tags), 855 RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES 856 ); 857 } 858 "nip09_project_signed_event_wire_bytes_exact_max_created_at" => { 859 let raw = fixture_raw_event(vector); 860 assert_eq!(raw.created_at, u64::MAX); 861 assert_eq!( 862 input_str(vector, "event_json").len(), 863 RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES 864 ); 865 } 866 "nip09_project_signed_event_wire_short_created_at_width" => { 867 let raw = fixture_raw_event(vector); 868 assert_eq!(raw.created_at, 1); 869 assert_eq!( 870 input_str(vector, "event_json").len(), 871 RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES 872 ); 873 } 874 "nip09_project_signed_content_bytes_overflow" => { 875 assert_eq!( 876 fixture_raw_event(vector).content.len(), 877 RADROOTS_NIP09_DELETION_CONTENT_MAX_BYTES + 1 878 ); 879 } 880 "nip09_project_signed_tag_count_overflow" => { 881 assert_eq!( 882 fixture_raw_event(vector).tags.len(), 883 RADROOTS_NIP09_DELETION_TAG_MAX_COUNT + 1 884 ); 885 } 886 "nip09_project_signed_tag_element_count_overflow" => { 887 assert_eq!( 888 fixture_raw_event(vector) 889 .tags 890 .iter() 891 .map(Vec::len) 892 .sum::<usize>(), 893 RADROOTS_NIP09_DELETION_TAG_TOTAL_ELEMENT_MAX_COUNT + 1 894 ); 895 } 896 "nip09_project_signed_tag_element_bytes_overflow" => { 897 assert_eq!( 898 fixture_raw_event(vector) 899 .tags 900 .iter() 901 .flat_map(|tag| tag.iter()) 902 .map(String::len) 903 .max(), 904 Some(RADROOTS_NIP09_DELETION_TAG_ELEMENT_MAX_BYTES + 1) 905 ); 906 } 907 "nip09_project_signed_tag_bytes_overflow" => { 908 assert_eq!( 909 tag_bytes(&fixture_raw_event(vector).tags), 910 RADROOTS_NIP09_DELETION_TAG_TOTAL_MAX_BYTES + 1 911 ); 912 } 913 "nip09_project_signed_event_wire_bytes_overflow" 914 | "nip09_project_signed_wire_precedes_target_parse" => { 915 assert_eq!( 916 input_str(vector, "event_json").len(), 917 RADROOTS_NIP09_DELETION_EVENT_WIRE_MAX_BYTES + 1 918 ); 919 } 920 _ => {} 921 } 922 } 923 924 fn input_array<'a>(vector: &'a Vector, field: &str) -> &'a [Value] { 925 vector.input[field] 926 .as_array() 927 .unwrap_or_else(|| panic!("{} input.{field} must be an array", vector.id)) 928 } 929 930 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 931 vector.input[field] 932 .as_str() 933 .unwrap_or_else(|| panic!("{} input.{field} must be a string", vector.id)) 934 } 935 936 fn object_str<'a>( 937 object: &'a serde_json::Map<String, Value>, 938 field: &str, 939 vector_id: &str, 940 ) -> &'a str { 941 object[field] 942 .as_str() 943 .unwrap_or_else(|| panic!("{vector_id} {field} must be a string")) 944 } 945 946 fn object_u32(object: &serde_json::Map<String, Value>, field: &str, vector_id: &str) -> u32 { 947 u32::try_from( 948 object[field] 949 .as_u64() 950 .unwrap_or_else(|| panic!("{vector_id} {field} must be an integer")), 951 ) 952 .unwrap_or_else(|_| panic!("{vector_id} {field} must fit u32")) 953 } 954 955 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 956 vector.expected[field] 957 .as_str() 958 .unwrap_or_else(|| panic!("{} expected.{field} must be a string", vector.id)) 959 } 960 961 fn tag_bytes(tags: &[Vec<String>]) -> usize { 962 tags.iter() 963 .flat_map(|tag| tag.iter()) 964 .map(String::len) 965 .sum() 966 } 967 968 fn value_tag_bytes(tags: &[Value]) -> usize { 969 tags.iter() 970 .flat_map(|tag| tag.as_array().expect("tag array")) 971 .map(|element| element.as_str().expect("tag element").len()) 972 .sum() 973 }