app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

focused_availability_lifecycle.rs (24085B)


      1 use nostr::{Event, EventBuilder, JsonUtil, Keys, Kind, Tag, Timestamp};
      2 use radroots_event::envelope::event_head::{
      3     CurrentEventHead, EventHeadCandidate, EventHeadCandidateResult, EventHeadCoordinate,
      4     EventHeadDecision, event_head_candidate_for_nip01_event, select_event_head,
      5 };
      6 use radroots_event::food::availability::FoodAvailabilityError;
      7 use radroots_event::listing::classified::ClassifiedListingPartition;
      8 use radroots_event::wire::Nip01EventWire;
      9 use radroots_event_codec::admission::deletion::{
     10     RadrootsAdmittedNip09DeletionRequestEvent, RadrootsNip09DeletionAdmissionError,
     11     RadrootsNip09SuppressionOutcome, RadrootsNip09SuppressionReason, evaluate_nip09_suppression,
     12     verify_and_admit_nip09_deletion_request_event,
     13 };
     14 use radroots_event_codec::admission::food_availability::{
     15     RadrootsFoodAvailabilityAdmissionError, RadrootsFoodAvailabilityAdmissionOutcome,
     16     admit_verified_food_availability_event,
     17 };
     18 use radroots_event_codec::decode::food_availability::RadrootsFoodAvailabilityProjectionError;
     19 use radroots_event_codec::verify::{
     20     RadrootsNip01VerificationError, RadrootsSignatureVerifiedEvent, verify_nip01_event,
     21 };
     22 
     23 #[path = "fixtures/availability_lifecycle_v1.rs"]
     24 mod corpus;
     25 
     26 const MAX_EVENT_BYTES: usize = 4 * 1024;
     27 const MAX_CORPUS_BYTES: usize = 64 * 1024;
     28 const MAX_RETAINED_SOURCE_BYTES: usize = 16 * 1024;
     29 const RETAINED_SOURCE: &str = include_str!("fixtures/availability_lifecycle_v1.rs");
     30 
     31 struct PublicEvent {
     32     name: &'static str,
     33     json: String,
     34 }
     35 
     36 struct PublicCorpus {
     37     author: String,
     38     other_author: String,
     39     events: Vec<PublicEvent>,
     40 }
     41 
     42 fn sign_event(
     43     keys: &Keys,
     44     kind: u16,
     45     created_at: u64,
     46     content: &str,
     47     tags: Vec<Vec<String>>,
     48 ) -> Event {
     49     assert!(tags.len() <= 16);
     50     assert!(
     51         content.len() + tags.iter().flatten().map(String::len).sum::<usize>() <= MAX_EVENT_BYTES
     52     );
     53     let tags = tags
     54         .into_iter()
     55         .map(|tag| Tag::parse(tag).expect("bounded public raw tag"));
     56     EventBuilder::new(Kind::from(kind), content)
     57         .tags(tags)
     58         .custom_created_at(Timestamp::from(created_at))
     59         .sign_with_keys(keys)
     60         .expect("isolated public lifecycle signing")
     61 }
     62 
     63 /// Both signers are dropped before any retained public JSON leaves the helper.
     64 fn public_corpus() -> PublicCorpus {
     65     let owner = Keys::generate();
     66     let other = Keys::generate();
     67     let author = owner.public_key().to_hex();
     68     let other_author = other.public_key().to_hex();
     69     assert_ne!(author, other_author);
     70     let mut signed = Vec::<(&'static str, Event)>::new();
     71     for version in &corpus::VERSIONS {
     72         let keys = if version.other_author { &other } else { &owner };
     73         signed.push((
     74             version.name,
     75             sign_event(
     76                 keys,
     77                 30402,
     78                 version.created_at,
     79                 version.content,
     80                 corpus::listing_tags(version),
     81             ),
     82         ));
     83     }
     84     let original = &corpus::VERSIONS[0];
     85     assert_eq!(original.name, "old_active");
     86     signed.push((
     87         "other_kind",
     88         sign_event(
     89             &owner,
     90             corpus::OTHER_ADDRESSABLE_KIND,
     91             original.created_at,
     92             original.content,
     93             corpus::listing_tags(original),
     94         ),
     95     ));
     96     let address = format!("30402:{author}:{}", corpus::IDENTIFIER);
     97     for deletion in &corpus::DELETIONS {
     98         let mut tags = Vec::new();
     99         match deletion.target {
    100             corpus::Target::Event(name) | corpus::Target::EventAndAddress(name) => {
    101                 let target = signed
    102                     .iter()
    103                     .find(|(target_name, _)| *target_name == name)
    104                     .expect("public target recipe exists");
    105                 tags.push(vec!["e".into(), target.1.id.to_hex()]);
    106             }
    107             corpus::Target::Address => {}
    108         }
    109         if matches!(
    110             deletion.target,
    111             corpus::Target::Address | corpus::Target::EventAndAddress(_)
    112         ) {
    113             tags.push(vec!["a".into(), address.clone()]);
    114         }
    115         tags.push(vec!["k".into(), "30402".into()]);
    116         let keys = if deletion.other_author {
    117             &other
    118         } else {
    119             &owner
    120         };
    121         signed.push((
    122             deletion.name,
    123             sign_event(
    124                 keys,
    125                 5,
    126                 deletion.created_at,
    127                 "Public deletion conformance.",
    128                 tags,
    129             ),
    130         ));
    131     }
    132     drop(owner);
    133     drop(other);
    134     let events = signed
    135         .into_iter()
    136         .map(|(name, event)| {
    137             let json = event.as_json();
    138             assert!(json.len() <= MAX_EVENT_BYTES);
    139             PublicEvent { name, json }
    140         })
    141         .collect::<Vec<_>>();
    142     assert_eq!(events.len(), 18);
    143     assert!(events.iter().map(|event| event.json.len()).sum::<usize>() <= MAX_CORPUS_BYTES);
    144     PublicCorpus {
    145         author,
    146         other_author,
    147         events,
    148     }
    149 }
    150 
    151 fn public_json<'a>(corpus: &'a PublicCorpus, name: &str) -> &'a str {
    152     &corpus
    153         .events
    154         .iter()
    155         .find(|event| event.name == name)
    156         .expect("named public event")
    157         .json
    158 }
    159 
    160 fn verified(corpus: &PublicCorpus, name: &str) -> RadrootsSignatureVerifiedEvent {
    161     let wire = Nip01EventWire::parse_json(public_json(corpus, name))
    162         .expect("bounded public ID-verified wire");
    163     assert!(wire.extra.is_empty());
    164     verify_nip01_event(wire.into_envelope().expect("ID-verified envelope"))
    165         .expect("shared ID and signature verification")
    166 }
    167 
    168 fn deletion(corpus: &PublicCorpus, name: &str) -> RadrootsAdmittedNip09DeletionRequestEvent {
    169     let wire = Nip01EventWire::parse_json(public_json(corpus, name)).expect("public deletion wire");
    170     verify_and_admit_nip09_deletion_request_event(wire.into_envelope().expect("deletion envelope"))
    171         .expect("shared NIP-09 request admission without a target lookup")
    172 }
    173 
    174 fn candidate(event: &RadrootsSignatureVerifiedEvent) -> EventHeadCandidate {
    175     match event_head_candidate_for_nip01_event(event.event()) {
    176         EventHeadCandidateResult::Candidate(candidate) => candidate,
    177         _ => panic!("verified addressable fixture must produce a raw protocol head candidate"),
    178     }
    179 }
    180 
    181 /// Every supplied verified version enters shared ordering before product projection or suppression.
    182 fn select_all_versions(versions: &[&RadrootsSignatureVerifiedEvent]) -> CurrentEventHead {
    183     assert!(!versions.is_empty());
    184     assert!(versions.len() <= 3);
    185     let mut head = None;
    186     for version in versions {
    187         match select_event_head(candidate(version), head.as_ref()) {
    188             EventHeadDecision::Applied(next) => head = Some(next),
    189             EventHeadDecision::SkippedDuplicate
    190             | EventHeadDecision::SkippedOlder
    191             | EventHeadDecision::SkippedSameTimestampHigherEventId => {}
    192             EventHeadDecision::CoordinateMismatch => {
    193                 panic!("single-coordinate permutation fixture")
    194             }
    195         }
    196     }
    197     head.expect("shared selected head")
    198 }
    199 
    200 fn assert_focused_status(event: &RadrootsSignatureVerifiedEvent, status: &str) {
    201     match admit_verified_food_availability_event(event.clone()).expect("focused head admission") {
    202         RadrootsFoodAvailabilityAdmissionOutcome::Admitted(admitted) => {
    203             assert_eq!(admitted.projection().status().as_str(), status);
    204         }
    205         _ => panic!("selected focused version must be admitted"),
    206     }
    207 }
    208 
    209 #[test]
    210 fn raw_head_selection_preserves_reverse_arrival_and_exact_duplicates() {
    211     let corpus = public_corpus();
    212     let old = verified(&corpus, "old_active");
    213     let new = verified(&corpus, "newer_sold");
    214     let mut cases = 0;
    215     for order in [[&old, &new, &new], [&new, &old, &new], [&new, &new, &old]] {
    216         let head = select_all_versions(&order);
    217         assert_eq!(head.event_id, *new.event().id());
    218         assert_eq!(head.created_at, 1_800_000_200);
    219         assert_eq!(
    220             select_event_head(candidate(&new), Some(&head)),
    221             EventHeadDecision::SkippedDuplicate
    222         );
    223         assert_eq!(
    224             select_event_head(candidate(&old), Some(&head)),
    225             EventHeadDecision::SkippedOlder
    226         );
    227         assert_focused_status(&new, "sold");
    228         cases += 1;
    229     }
    230     assert_eq!(cases, 3);
    231 }
    232 
    233 #[test]
    234 fn equal_timestamp_heads_select_lowest_event_id_in_every_order() {
    235     let corpus = public_corpus();
    236     let left = verified(&corpus, "tie_left");
    237     let right = verified(&corpus, "tie_right");
    238     assert_ne!(left.event().id(), right.event().id());
    239     assert_eq!(
    240         left.event().created_at_u64(),
    241         right.event().created_at_u64()
    242     );
    243     let (lowest, higher) = if left.event().id() < right.event().id() {
    244         (&left, &right)
    245     } else {
    246         (&right, &left)
    247     };
    248     let mut cases = 0;
    249     for order in [[&left, &right, &left], [&right, &left, &right]] {
    250         let head = select_all_versions(&order);
    251         assert_eq!(head.event_id, *lowest.event().id());
    252         assert_eq!(head.created_at, 1_800_000_300);
    253         assert_eq!(
    254             select_event_head(candidate(higher), Some(&head)),
    255             EventHeadDecision::SkippedSameTimestampHigherEventId
    256         );
    257         assert_eq!(
    258             select_event_head(candidate(lowest), Some(&head)),
    259             EventHeadDecision::SkippedDuplicate
    260         );
    261         cases += 1;
    262     }
    263     assert_eq!(cases, 2);
    264 }
    265 
    266 #[test]
    267 fn shared_coordinates_keep_distinct_authors_kinds_and_identifiers_separate() {
    268     let corpus = public_corpus();
    269     let original = verified(&corpus, "old_active");
    270     let head = select_all_versions(&[&original]);
    271     match &head.coordinate {
    272         EventHeadCoordinate::Addressable {
    273             kind,
    274             pubkey,
    275             d_tag,
    276         } => {
    277             assert_eq!(*kind, 30402);
    278             assert_eq!(pubkey.to_hex(), corpus.author);
    279             assert_eq!(d_tag, "hcav-lifecycle-v1");
    280         }
    281         _ => panic!("kind 30402 has an addressable coordinate"),
    282     }
    283     for name in ["other_identifier", "other_author", "other_kind"] {
    284         let distinct = verified(&corpus, name);
    285         assert_eq!(distinct.event().content(), original.event().content());
    286         assert_eq!(
    287             select_event_head(candidate(&distinct), Some(&head)),
    288             EventHeadDecision::CoordinateMismatch
    289         );
    290         assert_ne!(candidate(&distinct).coordinate, head.coordinate);
    291         if name == "other_kind" {
    292             assert_eq!(distinct.event().author(), original.event().author());
    293             match &candidate(&distinct).coordinate {
    294                 EventHeadCoordinate::Addressable {
    295                     kind,
    296                     pubkey,
    297                     d_tag,
    298                 } => {
    299                     assert_eq!(*kind, u32::from(corpus::OTHER_ADDRESSABLE_KIND));
    300                     assert_ne!(*kind, original.event().kind_u32());
    301                     assert_eq!(*pubkey, *original.event().author());
    302                     assert_eq!(d_tag, corpus::IDENTIFIER);
    303                 }
    304                 _ => panic!("control must have an addressable coordinate"),
    305             }
    306         }
    307         assert_eq!(
    308             select_all_versions(&[&distinct]).event_id,
    309             *distinct.event().id()
    310         );
    311     }
    312     assert_eq!(
    313         verified(&corpus, "other_author").event().author().to_hex(),
    314         corpus.other_author
    315     );
    316 }
    317 
    318 #[test]
    319 fn exact_deletion_before_target_and_deleted_head_never_resurrect_older_active() {
    320     let corpus = public_corpus();
    321     // Admission first retains the request without consulting any target event.
    322     let request = deletion(&corpus, "exact_newer");
    323     assert_eq!(request.event().created_at_u64(), 1_800_000_000);
    324     let old = verified(&corpus, "old_active");
    325     let new = verified(&corpus, "newer_sold");
    326     assert!(request.event().created_at_u64() < new.event().created_at_u64());
    327     let mut cases = 0;
    328     for order in corpus::ALL_THREE_PERMUTATIONS {
    329         let mut versions = Vec::new();
    330         let mut requests = Vec::new();
    331         for index in order {
    332             match index {
    333                 0 => versions.push(&old),
    334                 1 => versions.push(&new),
    335                 2 => requests.push(request.clone()),
    336                 _ => unreachable!("three-item permutation"),
    337             }
    338             if !versions.is_empty() {
    339                 let head = select_all_versions(&versions);
    340                 let winner = if head.event_id == *new.event().id() {
    341                     &new
    342                 } else {
    343                     &old
    344                 };
    345                 let decision = evaluate_nip09_suppression(winner, &requests);
    346                 if head.event_id == *new.event().id() && !requests.is_empty() {
    347                     assert_eq!(
    348                         decision.outcome(),
    349                         RadrootsNip09SuppressionOutcome::Suppressed
    350                     );
    351                     assert_eq!(
    352                         decision.reason(),
    353                         RadrootsNip09SuppressionReason::EventIdReference
    354                     );
    355                     assert_eq!(
    356                         decision
    357                             .event_reference()
    358                             .expect("exact reference evidence")
    359                             .request_id(),
    360                         request.event().id()
    361                     );
    362                 } else {
    363                     assert_eq!(decision.outcome(), RadrootsNip09SuppressionOutcome::Visible);
    364                     assert_eq!(
    365                         decision.reason(),
    366                         RadrootsNip09SuppressionReason::NoAuthorizedReference
    367                     );
    368                 }
    369             }
    370         }
    371         let head = select_all_versions(&versions);
    372         assert_eq!(head.event_id, *new.event().id());
    373         assert_eq!(
    374             evaluate_nip09_suppression(&new, &requests).outcome(),
    375             RadrootsNip09SuppressionOutcome::Suppressed
    376         );
    377         assert_eq!(
    378             evaluate_nip09_suppression(&old, &requests).outcome(),
    379             RadrootsNip09SuppressionOutcome::Visible
    380         );
    381         assert_eq!(
    382             select_event_head(candidate(&old), Some(&head)),
    383             EventHeadDecision::SkippedOlder
    384         );
    385         assert_focused_status(&old, "active");
    386         assert_ne!(head.event_id, *old.event().id());
    387         cases += 1;
    388     }
    389     assert_eq!(cases, 6);
    390 }
    391 
    392 #[test]
    393 fn address_deletion_cutoff_is_inclusive_and_arrival_order_independent() {
    394     let corpus = public_corpus();
    395     let request = deletion(&corpus, "address_cutoff");
    396     assert_eq!(request.event().created_at_u64(), 1_800_000_100);
    397     let mut cases = 0;
    398     for (name, outcome, reason) in [
    399         (
    400             "before_cutoff",
    401             RadrootsNip09SuppressionOutcome::Suppressed,
    402             RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff,
    403         ),
    404         (
    405             "at_cutoff",
    406             RadrootsNip09SuppressionOutcome::Suppressed,
    407             RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff,
    408         ),
    409         (
    410             "after_cutoff",
    411             RadrootsNip09SuppressionOutcome::Visible,
    412             RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget,
    413         ),
    414     ] {
    415         for order in [[0, 1], [1, 0]] {
    416             let mut target = None;
    417             let mut retained = Vec::new();
    418             for arrival in order {
    419                 match arrival {
    420                     0 => target = Some(verified(&corpus, name)),
    421                     1 => retained.push(deletion(&corpus, "address_cutoff")),
    422                     _ => unreachable!("two-item arrival permutation"),
    423                 }
    424                 if let Some(target) = target.as_ref() {
    425                     let head = select_all_versions(&[target]);
    426                     assert_eq!(head.event_id, *target.event().id());
    427                     let decision = evaluate_nip09_suppression(target, &retained);
    428                     if retained.is_empty() {
    429                         assert_eq!(decision.outcome(), RadrootsNip09SuppressionOutcome::Visible);
    430                         assert_eq!(
    431                             decision.reason(),
    432                             RadrootsNip09SuppressionReason::NoAuthorizedReference
    433                         );
    434                     } else {
    435                         assert_eq!(decision.outcome(), outcome);
    436                         assert_eq!(decision.reason(), reason);
    437                     }
    438                 } else {
    439                     assert_eq!(
    440                         retained.len(),
    441                         1,
    442                         "deletion retained before any target observation"
    443                     );
    444                 }
    445             }
    446             let target = target.expect("target observed in each permutation");
    447             let head = select_all_versions(&[&target]);
    448             let decision = evaluate_nip09_suppression(&target, &retained);
    449             assert_eq!(head.event_id, *target.event().id());
    450             assert_eq!(decision.outcome(), outcome);
    451             assert_eq!(decision.reason(), reason);
    452             assert!(decision.event_reference().is_none());
    453             let evidence = decision
    454                 .address_reference()
    455                 .expect("retained matching address evidence");
    456             assert_eq!(evidence.inclusive_cutoff(), corpus::CUTOFF);
    457             assert_eq!(evidence.request_id(), request.event().id());
    458             assert_eq!(evidence.coordinate().kind(), 30402);
    459             assert_eq!(evidence.coordinate().pubkey().to_hex(), corpus.author);
    460             assert_eq!(evidence.coordinate().identifier(), "hcav-lifecycle-v1");
    461             cases += 1;
    462         }
    463     }
    464     assert_eq!(cases, 6);
    465     let combined = deletion(&corpus, "combined_cutoff");
    466     let target = verified(&corpus, "at_cutoff");
    467     let decision = evaluate_nip09_suppression(&target, std::slice::from_ref(&combined));
    468     assert_eq!(
    469         decision.outcome(),
    470         RadrootsNip09SuppressionOutcome::Suppressed
    471     );
    472     assert_eq!(
    473         decision.reason(),
    474         RadrootsNip09SuppressionReason::EventIdAndAddressReference
    475     );
    476     assert_eq!(
    477         decision
    478             .event_reference()
    479             .expect("exact evidence")
    480             .request_id(),
    481         combined.event().id()
    482     );
    483     assert_eq!(
    484         decision
    485             .address_reference()
    486             .expect("address evidence")
    487             .request_id(),
    488         combined.event().id()
    489     );
    490 }
    491 
    492 #[test]
    493 fn forged_deletions_cannot_suppress_other_author_and_invalid_signatures_are_rejected() {
    494     let corpus = public_corpus();
    495     let target = verified(&corpus, "newer_sold");
    496     let head = select_all_versions(&[&target]);
    497     for name in ["forged_exact", "forged_address"] {
    498         let forged = deletion(&corpus, name);
    499         assert_eq!(forged.event().author().to_hex(), corpus.other_author);
    500         assert_ne!(forged.event().author(), target.event().author());
    501         let decision = evaluate_nip09_suppression(&target, std::slice::from_ref(&forged));
    502         assert_eq!(decision.outcome(), RadrootsNip09SuppressionOutcome::Visible);
    503         assert_eq!(
    504             decision.reason(),
    505             RadrootsNip09SuppressionReason::RequestAuthorMismatch
    506         );
    507         assert!(decision.event_reference().is_none());
    508         assert!(decision.address_reference().is_none());
    509         assert_eq!(head.event_id, *target.event().id());
    510     }
    511     let unrelated = deletion(&corpus, "unrelated_exact");
    512     let decision = evaluate_nip09_suppression(&target, std::slice::from_ref(&unrelated));
    513     assert_eq!(decision.outcome(), RadrootsNip09SuppressionOutcome::Visible);
    514     assert_eq!(
    515         decision.reason(),
    516         RadrootsNip09SuppressionReason::NoAuthorizedReference
    517     );
    518 
    519     let json = public_json(&corpus, "exact_newer");
    520     let wire = Nip01EventWire::parse_json_unverified(json).expect("public deletion wire");
    521     let signature_field = format!("\"sig\":\"{}\"", wire.sig);
    522     assert_eq!(json.matches(&signature_field).count(), 1);
    523     let tampered = json.replacen(
    524         &signature_field,
    525         &format!("\"sig\":\"{}\"", "0".repeat(128)),
    526         1,
    527     );
    528     let envelope = Nip01EventWire::parse_json_unverified(&tampered)
    529         .expect("bounded invalid-signature wire")
    530         .into_unverified_envelope()
    531         .expect("untrusted deletion envelope");
    532     assert_eq!(
    533         verify_and_admit_nip09_deletion_request_event(envelope)
    534             .expect_err("invalid deletion has no admitted request authority"),
    535         RadrootsNip09DeletionAdmissionError::Nip01Verification(
    536             RadrootsNip01VerificationError::SignatureInvalid
    537         )
    538     );
    539 }
    540 
    541 #[test]
    542 fn newer_verified_unsupported_heads_never_promote_older_active_versions() {
    543     let corpus = public_corpus();
    544     let old = verified(&corpus, "old_active");
    545     let mut cases = 0;
    546     for name in ["generic_head", "malformed_head"] {
    547         let unsupported = verified(&corpus, name);
    548         for order in [
    549             [&old, &unsupported, &old],
    550             [&unsupported, &old, &unsupported],
    551         ] {
    552             // The unsupported version participates before focused admission is attempted.
    553             let head = select_all_versions(&order);
    554             assert_eq!(head.event_id, *unsupported.event().id());
    555             assert_eq!(head.created_at, 1_800_000_200);
    556             let result = admit_verified_food_availability_event(unsupported.clone());
    557             if name == "generic_head" {
    558                 match result.expect("verified generic exclusion") {
    559                     RadrootsFoodAvailabilityAdmissionOutcome::Excluded(candidate) => {
    560                         assert_eq!(
    561                             candidate.partition(),
    562                             ClassifiedListingPartition::GenericNip99
    563                         );
    564                         assert_eq!(candidate.event().id(), &head.event_id);
    565                     }
    566                     _ => panic!("generic winning head remains unsupported"),
    567                 }
    568             } else {
    569                 assert_eq!(
    570                     result.expect_err("malformed focused winning head remains known"),
    571                     RadrootsFoodAvailabilityAdmissionError::Projection(
    572                         RadrootsFoodAvailabilityProjectionError::Domain(
    573                             FoodAvailabilityError::StatusInvalid
    574                         )
    575                     )
    576                 );
    577             }
    578             assert_eq!(
    579                 evaluate_nip09_suppression(&unsupported, &[]).outcome(),
    580                 RadrootsNip09SuppressionOutcome::Visible
    581             );
    582             assert_eq!(
    583                 select_event_head(candidate(&old), Some(&head)),
    584                 EventHeadDecision::SkippedOlder
    585             );
    586             assert_focused_status(&old, "active");
    587             assert_ne!(head.event_id, *old.event().id());
    588             cases += 1;
    589         }
    590     }
    591     assert_eq!(cases, 4);
    592 }
    593 
    594 #[test]
    595 fn lifecycle_corpus_is_bounded_and_retains_only_verified_public_material() {
    596     assert_eq!(corpus::VERSIONS.len(), 11);
    597     assert_eq!(corpus::DELETIONS.len(), 6);
    598     assert_eq!(corpus::ALL_THREE_PERMUTATIONS.len(), 6);
    599     assert!(RETAINED_SOURCE.len() <= MAX_RETAINED_SOURCE_BYTES);
    600     for forbidden in [
    601         "nsec1",
    602         "SecretKey",
    603         "secret_key",
    604         "private_key",
    605         "Keys::",
    606         "credential",
    607     ] {
    608         assert!(
    609             !RETAINED_SOURCE.contains(forbidden),
    610             "public retained raw recipes"
    611         );
    612     }
    613     let corpus = public_corpus();
    614     assert_eq!(corpus.events.len(), 18);
    615     for event in &corpus.events {
    616         for forbidden in ["nsec1", "secret_key", "private_key", "credential"] {
    617             assert!(
    618                 !event.json.contains(forbidden),
    619                 "public signed lifecycle artifact"
    620             );
    621         }
    622         let verified = verified(&corpus, event.name);
    623         assert!(matches!(verified.event().kind_u32(), 5 | 30402 | 30023));
    624         let author = verified.event().author().to_hex();
    625         assert!(author == corpus.author || author == corpus.other_author);
    626     }
    627 }