lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }