lib

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

authored_wire_corpus.rs (34032B)


      1 mod support;
      2 
      3 use std::{borrow::Cow, fs, path::Path};
      4 
      5 use nostr::{
      6     Event, Keys, SecretKey, UnsignedEvent,
      7     secp256k1::rand::{CryptoRng, RngCore},
      8 };
      9 use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256};
     10 use radroots_event::{
     11     GenericEventDraft,
     12     calendar::{AuthoredCalendarDateEvent, AuthoredCalendarTimeEvent, CalendarDate},
     13     envelope::kind::KIND_CLASSIFIED_LISTING,
     14     envelope::{EventEnvelope, EventEnvelopeParts},
     15     food::availability::{
     16         FoodAvailabilityDetails, FoodAvailabilityDetailsParts, FoodAvailabilityStatus, FoodContent,
     17         FoodCurrency, FoodIdentifier, FoodPrice, FoodPublishedAt, FoodQuantity, FoodText, FoodUnit,
     18     },
     19     id::{ClassifiedListingAddress, DTag, EventId, InventoryBinId, TradeId},
     20     media::AuthoredImage,
     21     post::{
     22         AuthoredAsk, AuthoredPhotoUpdate, AuthoredPostImage, AuthoredUpdate, PostImageDimensions,
     23         RADROOTS_POST_CONTENT_MAX_BYTES,
     24         comment::{AuthoredNip22Comment, Nip22EventRootReference},
     25         deletion::{
     26             AuthoredNip09DeletionRequest, Nip09DeletionAddressTarget, Nip09DeletionEventTarget,
     27         },
     28         reply::{AuthoredNip10Reply, Nip10ReplyReference},
     29     },
     30     profile::AuthoredProfile,
     31     trade::{
     32         FulfillmentProfileV1, RADROOTS_TRADE_CANCELLATION_CONTRACT_ID,
     33         RADROOTS_TRADE_DECISION_CONTRACT_ID, RADROOTS_TRADE_PROPOSAL_CONTRACT_ID,
     34         RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID, RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID,
     35         RADROOTS_TRADE_SCHEMA_VERSION, TradeCancellationProfileV1, TradeCandidateLineV1,
     36         TradeCandidateTermsV1, TradeDecisionV1, TradeEconomicAdjustmentV1, TradeEconomicsProfileV1,
     37         TradeMutationBodyV1, TradeMutationEnvelopeV1, canonical_trade_mutation_content,
     38     },
     39     wire::canonical_nip01_event_id_preimage,
     40 };
     41 use radroots_event_codec::{
     42     authoring::AuthoredEventPlan,
     43     decode::{
     44         rhi::{RadrootsRhiEvidenceAttestationV1, rhi_evidence_attestation_from_verified_event},
     45         trade::trade_mutation_from_verified_event,
     46     },
     47     verify::verify_nip01_event,
     48 };
     49 use radroots_identity::PublicKey;
     50 use radroots_nostr::{
     51     event::{
     52         GenericBuilder, Kind, Timestamp, build_ask, build_calendar_date, build_calendar_time,
     53         build_food_availability, build_nip09_deletion_request, build_nip10_reply,
     54         build_nip22_comment, build_photo_update, build_profile, build_update,
     55     },
     56     tag::Tag,
     57 };
     58 use serde::{Deserialize, Serialize};
     59 
     60 use support::{
     61     APPROVED_FIXTURE_NAMESPACE, FIXTURE_ALICE_PUBLIC_KEY_HEX, FIXTURE_ALICE_SECRET_KEY_HEX,
     62     FIXTURE_BOB_PUBLIC_KEY_HEX, FIXTURE_BOB_SECRET_KEY_HEX, RELAY_PRIMARY_WSS,
     63 };
     64 
     65 const CREATED_AT: u64 = 1_784_347_200;
     66 const ROOT_EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
     67 const TARGET_EVENT_ID: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
     68 const RHI_REPORT: &str = "{\"attestation_method\":\"signed_evidence_snapshot\",\"claim_mutation_id\":\"2222222222222222222222222222222222222222222222222222222222222222\",\"contract_id\":\"radroots.rhi.evidence_attestation.v1\",\"contract_version\":1,\"evidence_manifest_digest\":\"4444444444444444444444444444444444444444444444444444444444444444\",\"evidence_policy_digest\":\"5555555555555555555555555555555555555555555555555555555555555555\",\"issuer_pubkey\":\"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df\",\"observed_at_unix_s\":1800000000,\"outcome\":\"indeterminate\",\"projection_digest\":\"6666666666666666666666666666666666666666666666666666666666666666\",\"reason_codes\":[\"required_source_incomplete\"],\"reducer_contract_id\":\"radroots.trade.reducer.v1\",\"reducer_contract_version\":1,\"report_id\":\"254686397c19fa4235eee820543e8166c0bb758af106ee998684cd9b0f548b30\",\"statement_digest\":\"254686397c19fa4235eee820543e8166c0bb758af106ee998684cd9b0f548b30\",\"supersedes_event_id\":null,\"supersedes_report_id\":null,\"trade_generation\":7,\"trade_id\":\"11111111111111111111111111111111\"}";
     69 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml";
     70 const PACKAGED_CORPUS: &str = include_str!("fixtures/authored_operations.v1.json");
     71 const WORKSPACE_CORPUS_PATH: &str =
     72     "../../contracts/conformance/vectors/event/authored_operations.v1.json";
     73 
     74 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
     75 #[serde(deny_unknown_fields)]
     76 struct Corpus {
     77     suite: String,
     78     contract_version: String,
     79     vectors: Vec<WireVector>,
     80 }
     81 
     82 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
     83 #[serde(deny_unknown_fields)]
     84 struct WireVector {
     85     id: String,
     86     kind: String,
     87     input: WireInput,
     88     expected: WireExpected,
     89 }
     90 
     91 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
     92 #[serde(deny_unknown_fields)]
     93 struct WireInput {
     94     contract_id: String,
     95     authoring: String,
     96 }
     97 
     98 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
     99 #[serde(deny_unknown_fields)]
    100 struct WireExpected {
    101     kind: u32,
    102     created_at: u64,
    103     pubkey: String,
    104     tags: Vec<Vec<String>>,
    105     content: String,
    106     preimage: String,
    107     event_id: String,
    108     signature: String,
    109     raw_json: String,
    110 }
    111 
    112 #[test]
    113 fn checked_in_authored_wire_corpus_is_exact_and_executable() {
    114     let corpus_json = conformance_vectors();
    115     let expected: Corpus = serde_json::from_str(&corpus_json).expect("authored corpus must parse");
    116     let actual = Corpus {
    117         suite: "authored_operations_wire".to_owned(),
    118         contract_version: "1.0.0".to_owned(),
    119         vectors: authored_wire_vectors(),
    120     };
    121     if expected.vectors.is_empty() {
    122         panic!(
    123             "authored corpus requires generated vectors:\n{}",
    124             serde_json::to_string_pretty(&actual).expect("serialize generated corpus")
    125         );
    126     }
    127     assert_eq!(actual, expected);
    128     assert_eq!(actual.vectors.len(), 17);
    129     assert_eq!(APPROVED_FIXTURE_NAMESPACE, "radroots-approved-fixture-v1");
    130     assert_eq!(
    131         actual
    132             .vectors
    133             .iter()
    134             .filter(|vector| vector.input.authoring == "typed")
    135             .count(),
    136         16
    137     );
    138     assert!(actual.vectors.iter().all(|vector| {
    139         !vector
    140             .expected
    141             .raw_json
    142             .contains(FIXTURE_ALICE_SECRET_KEY_HEX)
    143             && !vector
    144                 .expected
    145                 .preimage
    146                 .contains(FIXTURE_ALICE_SECRET_KEY_HEX)
    147     }));
    148 }
    149 
    150 #[test]
    151 fn authored_post_content_boundary_is_exact_without_bloating_the_corpus() {
    152     let exact = "x".repeat(RADROOTS_POST_CONTENT_MAX_BYTES);
    153     assert!(AuthoredUpdate::new(exact).is_ok());
    154     assert!(AuthoredUpdate::new("x".repeat(RADROOTS_POST_CONTENT_MAX_BYTES + 1)).is_err());
    155 }
    156 
    157 #[test]
    158 fn signature_verified_trade_boundary_rejects_a_different_valid_signer() {
    159     let mutation = trade_mutations().remove(0);
    160     let plan = AuthoredEventPlan::from_trade_mutation(mutation).expect("typed trade plan");
    161     let signer = Keys::new(SecretKey::from_hex(FIXTURE_BOB_SECRET_KEY_HEX).expect("fixture key"));
    162     let event = UnsignedEvent {
    163         id: None,
    164         pubkey: signer.public_key(),
    165         created_at: Timestamp::from_secs(plan.created_at()),
    166         kind: Kind::Custom(u16::try_from(plan.body().kind()).expect("trade kind")),
    167         tags: nostr::Tags::from_list(
    168             plan.body()
    169                 .tags()
    170                 .iter()
    171                 .cloned()
    172                 .map(Tag::parse)
    173                 .collect::<Result<Vec<_>, _>>()
    174                 .expect("trade tags"),
    175         ),
    176         content: plan.body().content().to_owned(),
    177     }
    178     .sign_with_ctx(nostr::SECP256K1, &mut CorpusAuxRng, &signer)
    179     .expect("validly signed trade event");
    180     let verified = verify_nip01_event(
    181         EventEnvelope::new(EventEnvelopeParts {
    182             id: event.id.to_hex(),
    183             author: event.pubkey.to_hex(),
    184             created_at: event.created_at.as_secs(),
    185             kind: u32::from(event.kind.as_u16()),
    186             tags: event
    187                 .tags
    188                 .iter()
    189                 .map(|tag| tag.as_slice().to_vec())
    190                 .collect(),
    191             content: event.content,
    192             sig: event.sig.to_string(),
    193         })
    194         .expect("trade event envelope"),
    195     )
    196     .expect("valid signature");
    197     assert_eq!(
    198         trade_mutation_from_verified_event(&verified)
    199             .expect_err("different valid signer must fail")
    200             .code(),
    201         "author_mismatch"
    202     );
    203 }
    204 
    205 fn authored_wire_vectors() -> Vec<WireVector> {
    206     let keys = fixture_keys();
    207     let created_at = Timestamp::from_secs(CREATED_AT);
    208     let mut vectors = Vec::with_capacity(16);
    209 
    210     let profile = AuthoredProfile::new("Alice \"Sprout\"")
    211         .expect("profile name")
    212         .with_display_name("Alice's Orchard")
    213         .with_about("Tree fruit\nDirect from the farm")
    214         .with_bot(false);
    215     vectors.push(vector(
    216         "typed_profile_optional_escaping_001",
    217         "radroots.profile.metadata.v1",
    218         "typed",
    219         deterministic_event(
    220             build_profile(&profile)
    221                 .expect("profile builder")
    222                 .custom_created_at(created_at)
    223                 .sign_with_keys(&keys)
    224                 .expect("profile event"),
    225             &keys,
    226         ),
    227     ));
    228 
    229     let update = AuthoredUpdate::new("Farm update: \"ready\"\\\nšŸ“").expect("authored update");
    230     vectors.push(vector(
    231         "typed_update_escaping_002",
    232         "radroots.social.update.v1",
    233         "typed",
    234         deterministic_event(
    235             build_update(&update)
    236                 .expect("update builder")
    237                 .custom_created_at(created_at)
    238                 .sign_with_keys(&keys)
    239                 .expect("update event"),
    240             &keys,
    241         ),
    242     ));
    243 
    244     let image = authored_post_image();
    245     let photo = AuthoredPhotoUpdate::new(format!("Harvest {}", image.url()), vec![image.clone()])
    246         .expect("photo update");
    247     vectors.push(vector(
    248         "typed_photo_update_imeta_003",
    249         "radroots.social.photo_update.v1",
    250         "typed",
    251         deterministic_event(
    252             build_photo_update(&photo)
    253                 .expect("photo builder")
    254                 .custom_created_at(created_at)
    255                 .sign_with_keys(&keys)
    256                 .expect("photo event"),
    257             &keys,
    258         ),
    259     ));
    260 
    261     let ask =
    262         AuthoredAsk::new(format!("Is this ready? {}", image.url()), vec![image]).expect("ask");
    263     vectors.push(vector(
    264         "typed_ask_marker_order_004",
    265         "radroots.social.ask.v1",
    266         "typed",
    267         deterministic_event(
    268             build_ask(&ask)
    269                 .expect("ask builder")
    270                 .custom_created_at(created_at)
    271                 .sign_with_keys(&keys)
    272                 .expect("ask event"),
    273             &keys,
    274         ),
    275     ));
    276 
    277     let reply = AuthoredNip10Reply::direct(
    278         "Direct reply",
    279         Nip10ReplyReference::parse(
    280             ROOT_EVENT_ID,
    281             FIXTURE_BOB_PUBLIC_KEY_HEX,
    282             Some(RELAY_PRIMARY_WSS),
    283         )
    284         .expect("reply root"),
    285     )
    286     .expect("reply");
    287     vectors.push(vector(
    288         "typed_nip10_direct_reply_005",
    289         "radroots.social.reply.v1",
    290         "typed",
    291         deterministic_event(
    292             build_nip10_reply(&reply)
    293                 .expect("reply builder")
    294                 .custom_created_at(created_at)
    295                 .sign_with_keys(&keys)
    296                 .expect("reply event"),
    297             &keys,
    298         ),
    299     ));
    300 
    301     let deletion = deletion_request();
    302     vectors.push(vector(
    303         "typed_nip09_sorted_targets_006",
    304         "radroots.social.deletion_request.v1",
    305         "typed",
    306         deterministic_event(
    307             build_nip09_deletion_request(&deletion)
    308                 .expect("deletion builder")
    309                 .custom_created_at(created_at)
    310                 .sign_with_keys(&keys)
    311                 .expect("deletion event"),
    312             &keys,
    313         ),
    314     ));
    315 
    316     let comment = AuthoredNip22Comment::top_level_event(
    317         "Are these carrots available Saturday?",
    318         Nip22EventRootReference::parse(
    319             ROOT_EVENT_ID,
    320             FIXTURE_BOB_PUBLIC_KEY_HEX,
    321             KIND_CLASSIFIED_LISTING,
    322             Some(RELAY_PRIMARY_WSS),
    323         )
    324         .expect("comment root"),
    325     )
    326     .expect("comment");
    327     vectors.push(vector(
    328         "typed_nip22_top_level_event_007",
    329         "radroots.social.comment.v1",
    330         "typed",
    331         deterministic_event(
    332             build_nip22_comment(&comment)
    333                 .expect("comment builder")
    334                 .custom_created_at(created_at)
    335                 .sign_with_keys(&keys)
    336                 .expect("comment event"),
    337             &keys,
    338         ),
    339     ));
    340 
    341     vectors.push(vector(
    342         "typed_food_availability_optional_quantity_008",
    343         "radroots.food.availability.v1",
    344         "typed",
    345         deterministic_event(
    346             build_food_availability(&food_details(), created_at)
    347                 .expect("food builder")
    348                 .sign_with_keys(&keys)
    349                 .expect("food event"),
    350             &keys,
    351         ),
    352     ));
    353 
    354     let date = authored_calendar_date_event();
    355     vectors.push(vector(
    356         "typed_calendar_date_event_011",
    357         "radroots.calendar.date_event.v1",
    358         "typed",
    359         deterministic_event(
    360             build_calendar_date(&date)
    361                 .expect("calendar date builder")
    362                 .custom_created_at(created_at)
    363                 .sign_with_keys(&keys)
    364                 .expect("calendar date event"),
    365             &keys,
    366         ),
    367     ));
    368 
    369     let time = authored_calendar_time_event();
    370     vectors.push(vector(
    371         "typed_calendar_time_event_012",
    372         "radroots.calendar.time_event.v1",
    373         "typed",
    374         deterministic_event(
    375             build_calendar_time(&time)
    376                 .expect("calendar time builder")
    377                 .custom_created_at(created_at)
    378                 .sign_with_keys(&keys)
    379                 .expect("calendar time event"),
    380             &keys,
    381         ),
    382     ));
    383 
    384     vectors.push(generic_vector(
    385         "generic_operational_listing_009",
    386         "radroots.operational_listing.published.v1",
    387         CREATED_AT,
    388         30_402,
    389         operational_listing_tags(),
    390         "# Nantes Carrots\n\nFresh bunches harvested in Saanich".to_owned(),
    391         &keys,
    392     ));
    393 
    394     for (id, mutation) in [
    395         "typed_trade_proposal_010",
    396         "typed_trade_decision_013",
    397         "typed_trade_revision_proposal_014",
    398         "typed_trade_revision_decision_015",
    399         "typed_trade_cancellation_016",
    400     ]
    401     .into_iter()
    402     .zip(trade_mutations())
    403     {
    404         vectors.push(typed_trade_vector(id, mutation, &keys));
    405     }
    406 
    407     vectors.push(typed_rhi_vector(
    408         "typed_rhi_evidence_attestation_017",
    409         &keys,
    410     ));
    411 
    412     vectors
    413 }
    414 
    415 fn typed_rhi_vector(id: &str, keys: &Keys) -> WireVector {
    416     let report = RadrootsRhiEvidenceAttestationV1::from_canonical_content(RHI_REPORT)
    417         .expect("typed RHI report");
    418     let plan = AuthoredEventPlan::from_rhi_evidence_attestation(&report, CREATED_AT)
    419         .expect("typed RHI plan");
    420     let tags = plan
    421         .body()
    422         .tags()
    423         .iter()
    424         .cloned()
    425         .map(Tag::parse)
    426         .collect::<Result<Vec<_>, _>>()
    427         .expect("RHI tags");
    428     let event = UnsignedEvent {
    429         id: Some(
    430             nostr::EventId::from_hex(&plan.expected_event_id().to_hex())
    431                 .expect("planned RHI event id"),
    432         ),
    433         pubkey: keys.public_key(),
    434         created_at: Timestamp::from_secs(plan.created_at()),
    435         kind: Kind::Custom(u16::try_from(plan.body().kind()).expect("RHI kind")),
    436         tags: nostr::Tags::from_list(tags),
    437         content: plan.body().content().to_owned(),
    438     }
    439     .sign_with_ctx(nostr::SECP256K1, &mut CorpusAuxRng, keys)
    440     .expect("RHI event");
    441     let verified = verify_nip01_event(
    442         EventEnvelope::new(EventEnvelopeParts {
    443             id: event.id.to_hex(),
    444             author: event.pubkey.to_hex(),
    445             created_at: event.created_at.as_secs(),
    446             kind: u32::from(event.kind.as_u16()),
    447             tags: event
    448                 .tags
    449                 .iter()
    450                 .map(|tag| tag.as_slice().to_vec())
    451                 .collect(),
    452             content: event.content.clone(),
    453             sig: event.sig.to_string(),
    454         })
    455         .expect("RHI event envelope"),
    456     )
    457     .expect("verified RHI event");
    458     let signature_verified = radroots_event::admission::RawEvent::new(verified.into_event())
    459         .verify_id()
    460         .expect("RHI identifier typestate")
    461         .verify_signature(&radroots_event_codec::verify::Nip01SignatureVerifier)
    462         .expect("RHI signature typestate");
    463     assert_eq!(
    464         rhi_evidence_attestation_from_verified_event(&signature_verified)
    465             .expect("validated RHI event"),
    466         report
    467     );
    468     vector(id, "radroots.rhi.evidence_attestation.v1", "typed", event)
    469 }
    470 
    471 fn typed_trade_vector(id: &str, mutation: TradeMutationEnvelopeV1, keys: &Keys) -> WireVector {
    472     let plan = AuthoredEventPlan::from_trade_mutation(mutation).expect("typed trade plan");
    473     let tags = plan
    474         .body()
    475         .tags()
    476         .iter()
    477         .cloned()
    478         .map(Tag::parse)
    479         .collect::<Result<Vec<_>, _>>()
    480         .expect("trade tags");
    481     let event = UnsignedEvent {
    482         id: Some(
    483             nostr::EventId::from_hex(&plan.expected_event_id().to_hex()).expect("planned event id"),
    484         ),
    485         pubkey: keys.public_key(),
    486         created_at: Timestamp::from_secs(plan.created_at()),
    487         kind: Kind::Custom(u16::try_from(plan.body().kind()).expect("trade kind")),
    488         tags: nostr::Tags::from_list(tags),
    489         content: plan.body().content().to_owned(),
    490     }
    491     .sign_with_ctx(nostr::SECP256K1, &mut CorpusAuxRng, keys)
    492     .expect("trade event");
    493     assert_eq!(event.pubkey.to_hex(), plan.author().to_hex());
    494     assert_eq!(
    495         event
    496             .tags
    497             .iter()
    498             .map(|tag| tag.as_slice().to_vec())
    499             .collect::<Vec<_>>(),
    500         plan.body().tags()
    501     );
    502     assert_eq!(event.id.to_hex(), plan.expected_event_id().to_hex());
    503     let verified = verify_nip01_event(
    504         EventEnvelope::new(EventEnvelopeParts {
    505             id: event.id.to_hex(),
    506             author: event.pubkey.to_hex(),
    507             created_at: event.created_at.as_secs(),
    508             kind: u32::from(event.kind.as_u16()),
    509             tags: event
    510                 .tags
    511                 .iter()
    512                 .map(|tag| tag.as_slice().to_vec())
    513                 .collect(),
    514             content: event.content.clone(),
    515             sig: event.sig.to_string(),
    516         })
    517         .expect("trade event envelope"),
    518     )
    519     .expect("verified trade event");
    520     let parsed = trade_mutation_from_verified_event(&verified).expect("validated trade event");
    521     assert_eq!(
    522         parsed.contract_id,
    523         plan.body().contract().contract_id().as_str()
    524     );
    525     vector(
    526         id,
    527         plan.body().contract().contract_id().as_str(),
    528         "typed",
    529         event,
    530     )
    531 }
    532 
    533 fn generic_vector(
    534     id: &str,
    535     contract_id: &str,
    536     created_at: u64,
    537     kind: u32,
    538     tags: Vec<Vec<String>>,
    539     content: String,
    540     keys: &Keys,
    541 ) -> WireVector {
    542     let draft = GenericEventDraft::new(
    543         contract_id,
    544         kind,
    545         created_at,
    546         tags,
    547         content,
    548         FIXTURE_ALICE_PUBLIC_KEY_HEX,
    549     )
    550     .expect("validated generic draft");
    551     draft
    552         .validate_for_authoring()
    553         .expect("authorable generic draft");
    554     let nostr_tags = draft
    555         .tags_as_vec()
    556         .into_iter()
    557         .map(Tag::parse)
    558         .collect::<Result<Vec<_>, _>>()
    559         .expect("Nostr tags");
    560     let event = GenericBuilder::new(
    561         Kind::Custom(u16::try_from(kind).expect("Nostr kind")),
    562         draft.content(),
    563     )
    564     .tags(nostr_tags)
    565     .custom_created_at(Timestamp::from_secs(created_at))
    566     .sign_with_keys(keys)
    567     .expect("generic event");
    568     assert_eq!(event.id.to_hex(), draft.expected_event_id().to_hex());
    569     vector(id, contract_id, "generic", deterministic_event(event, keys))
    570 }
    571 
    572 fn deterministic_event(event: Event, keys: &Keys) -> Event {
    573     let unsigned = UnsignedEvent {
    574         id: Some(event.id),
    575         pubkey: event.pubkey,
    576         created_at: event.created_at,
    577         kind: event.kind,
    578         tags: event.tags,
    579         content: event.content,
    580     };
    581     unsigned
    582         .sign_with_ctx(nostr::SECP256K1, &mut CorpusAuxRng, keys)
    583         .expect("deterministically signed corpus event")
    584 }
    585 
    586 fn vector(id: &str, contract_id: &str, authoring: &str, event: Event) -> WireVector {
    587     event.verify().expect("valid corpus event");
    588     let tags = event
    589         .tags
    590         .iter()
    591         .map(|tag| tag.as_slice().to_vec())
    592         .collect::<Vec<_>>();
    593     let preimage = canonical_nip01_event_id_preimage(
    594         &event.pubkey.to_hex(),
    595         event.created_at.as_secs(),
    596         u32::from(event.kind.as_u16()),
    597         &tags,
    598         &event.content,
    599     )
    600     .expect("canonical preimage");
    601     WireVector {
    602         id: id.to_owned(),
    603         kind: "authored_operations.wire".to_owned(),
    604         input: WireInput {
    605             contract_id: contract_id.to_owned(),
    606             authoring: authoring.to_owned(),
    607         },
    608         expected: WireExpected {
    609             kind: u32::from(event.kind.as_u16()),
    610             created_at: event.created_at.as_secs(),
    611             pubkey: event.pubkey.to_hex(),
    612             tags,
    613             content: event.content.clone(),
    614             preimage,
    615             event_id: event.id.to_hex(),
    616             signature: event.sig.to_string(),
    617             raw_json: serde_json::to_string(&event).expect("canonical event JSON"),
    618         },
    619     }
    620 }
    621 
    622 struct CorpusAuxRng;
    623 
    624 impl RngCore for CorpusAuxRng {
    625     fn next_u32(&mut self) -> u32 {
    626         0x7261_6472
    627     }
    628 
    629     fn next_u64(&mut self) -> u64 {
    630         0x7261_6472_6f6f_7473
    631     }
    632 
    633     fn fill_bytes(&mut self, destination: &mut [u8]) {
    634         for (index, byte) in destination.iter_mut().enumerate() {
    635             *byte = b"radroots-authored-wire-corpus"[index % 29];
    636         }
    637     }
    638 
    639     fn try_fill_bytes(
    640         &mut self,
    641         destination: &mut [u8],
    642     ) -> Result<(), nostr::secp256k1::rand::Error> {
    643         self.fill_bytes(destination);
    644         Ok(())
    645     }
    646 }
    647 
    648 impl CryptoRng for CorpusAuxRng {}
    649 
    650 fn fixture_keys() -> Keys {
    651     Keys::new(SecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("fixture secret key"))
    652 }
    653 
    654 fn authored_post_image() -> AuthoredPostImage {
    655     let bytes = b"strawberries";
    656     let hash = Sha256::digest(bytes);
    657     let media_type = MediaType::parse("image/webp").expect("media type");
    658     let descriptor = BlobDescriptor::new(
    659         BlobUrl::parse(&format!("https://media.example/{hash}.webp")).expect("blob URL"),
    660         hash,
    661         bytes.len() as u64,
    662         media_type.clone(),
    663         CREATED_AT,
    664     )
    665     .expect("descriptor")
    666     .approve_reference()
    667     .expect("approved reference")
    668     .verify_bytes(bytes, &media_type)
    669     .expect("verified bytes");
    670     AuthoredPostImage::new(
    671         AuthoredImage::try_from(descriptor).expect("authored image"),
    672         PostImageDimensions::new(1200, 900).expect("dimensions"),
    673         "Harvest",
    674     )
    675     .expect("post image")
    676 }
    677 
    678 fn deletion_request() -> AuthoredNip09DeletionRequest {
    679     AuthoredNip09DeletionRequest::new(
    680         "superseded",
    681         vec![Nip09DeletionEventTarget::parse(TARGET_EVENT_ID, 1).expect("event target")],
    682         vec![
    683             Nip09DeletionAddressTarget::parse(format!(
    684                 "30402:{FIXTURE_BOB_PUBLIC_KEY_HEX}:carrots"
    685             ))
    686             .expect("listing target"),
    687             Nip09DeletionAddressTarget::parse(format!("0:{FIXTURE_BOB_PUBLIC_KEY_HEX}:"))
    688                 .expect("profile target"),
    689         ],
    690     )
    691     .expect("deletion request")
    692 }
    693 
    694 fn food_details() -> FoodAvailabilityDetails {
    695     FoodAvailabilityDetails::new(FoodAvailabilityDetailsParts {
    696         content: FoodContent::new("Carrots available this week.").expect("content"),
    697         identifier: FoodIdentifier::parse("nantes-carrots").expect("identifier"),
    698         title: FoodText::new("Nantes Carrots").expect("title"),
    699         summary: FoodText::new("Fresh bunches").expect("summary"),
    700         published_at: FoodPublishedAt::new(1_784_347_100).expect("published_at"),
    701         location: FoodText::new("Central Saanich, BC").expect("location"),
    702         price: FoodPrice::new(
    703             "3",
    704             FoodCurrency::parse("CAD").expect("currency"),
    705             FoodUnit::Pound,
    706         )
    707         .expect("price"),
    708         quantity: Some(FoodQuantity::new("24", FoodUnit::Pound).expect("quantity")),
    709         status: FoodAvailabilityStatus::Active,
    710         images: Vec::new(),
    711     })
    712     .expect("food details")
    713 }
    714 
    715 fn authored_calendar_date_event() -> AuthoredCalendarDateEvent {
    716     AuthoredCalendarDateEvent::new(
    717         "market-day",
    718         "Saturday Market",
    719         CalendarDate::parse("2026-08-08").expect("calendar date"),
    720     )
    721     .expect("date event")
    722     .with_end(CalendarDate::parse("2026-08-09").expect("calendar end"))
    723     .expect("date end")
    724     .with_description("Farmers market and seed swap.")
    725     .expect("date description")
    726     .with_locations(vec!["Central Saanich, BC".to_owned()])
    727     .expect("date location")
    728     .with_summary("Local food and seeds")
    729     .expect("date summary")
    730 }
    731 
    732 fn authored_calendar_time_event() -> AuthoredCalendarTimeEvent {
    733     AuthoredCalendarTimeEvent::new("farm-tour", "Farm Tour", 1_784_380_800)
    734         .expect("time event")
    735         .with_end(1_784_387_900)
    736         .expect("time end")
    737         .with_description("Walk the fields and meet the growers.")
    738         .expect("time description")
    739         .with_start_tzid("America/Vancouver")
    740         .expect("time zone")
    741         .with_locations(vec!["Saanich Peninsula".to_owned()])
    742         .expect("time location")
    743 }
    744 
    745 fn operational_listing_tags() -> Vec<Vec<String>> {
    746     [
    747         &["d", "AAAAAAAAAAAAAAAAAAAAAg"][..],
    748         &["p", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
    749         &["a", "30340:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:AAAAAAAAAAAAAAAAAAAAAA"],
    750         &["key", "carrot-nantes"],
    751         &["title", "Nantes Carrots"],
    752         &["category", "produce"],
    753         &["summary", "Fresh bunches harvested in Saanich"],
    754         &["published_at", "1700000000"],
    755         &["radroots:primary_bin", "bunch"],
    756         &["radroots:bin", "bunch", "1", "each"],
    757         &["radroots:price", "bunch", "4", "CAD", "1", "each"],
    758         &["price", "4", "CAD"],
    759         &["inventory", "24"],
    760         &["status", "active"],
    761         &["delivery", "pickup"],
    762         &["location", "Saanich Peninsula", "Victoria", "BC", "CA"],
    763         &["g", "c28hr"],
    764     ]
    765     .into_iter()
    766     .map(|tag| tag.iter().map(|value| (*value).to_owned()).collect())
    767     .collect()
    768 }
    769 
    770 fn trade_proposal() -> TradeMutationEnvelopeV1 {
    771     TradeMutationEnvelopeV1 {
    772         mutation_id: None,
    773         contract_id: RADROOTS_TRADE_PROPOSAL_CONTRACT_ID.to_owned(),
    774         schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
    775         trade_id: TradeId::parse("11111111111111111111111111111111").expect("trade id"),
    776         root_mutation_id: None,
    777         buyer_pubkey: PublicKey::from_hex(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("buyer key"),
    778         seller_pubkey: PublicKey::from_hex(FIXTURE_BOB_PUBLIC_KEY_HEX).expect("seller key"),
    779         farm_id: DTag::parse("farm-1").expect("farm id"),
    780         parent_mutation_ids: Vec::new(),
    781         author_pubkey: PublicKey::from_hex(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("author key"),
    782         counterparty_pubkey: PublicKey::from_hex(FIXTURE_BOB_PUBLIC_KEY_HEX)
    783             .expect("counterparty key"),
    784         authored_at_unix_s: CREATED_AT,
    785         body: TradeMutationBodyV1::Proposal {
    786             candidate: TradeCandidateTermsV1 {
    787                 candidate_id: None,
    788                 schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
    789                 base_candidate_id: None,
    790                 supersession_intent: None,
    791                 buyer_pubkey: PublicKey::from_hex(FIXTURE_ALICE_PUBLIC_KEY_HEX).expect("buyer key"),
    792                 seller_pubkey: PublicKey::from_hex(FIXTURE_BOB_PUBLIC_KEY_HEX).expect("seller key"),
    793                 farm_id: DTag::parse("farm-1").expect("farm id"),
    794                 lines: vec![TradeCandidateLineV1 {
    795                     line_id: DTag::parse("line-1").expect("line id"),
    796                     listing_addr: ClassifiedListingAddress::parse(format!(
    797                         "30402:{FIXTURE_BOB_PUBLIC_KEY_HEX}:listing-1"
    798                     ))
    799                     .expect("listing address"),
    800                     listing_event_id: EventId::parse("c".repeat(64)).expect("listing event id"),
    801                     listing_snapshot_sha256: "d".repeat(64),
    802                     product_id: "carrots".to_owned(),
    803                     option_id: None,
    804                     bin_id: InventoryBinId::parse("bin-1").expect("bin id"),
    805                     quantity_mantissa: "2".to_owned(),
    806                     quantity_scale: 0,
    807                     unit_code: "count".to_owned(),
    808                     unit_profile: "mvp-count".to_owned(),
    809                     unit_price_mantissa: "500".to_owned(),
    810                     currency_code: "USD".to_owned(),
    811                     line_subtotal_mantissa: "1000".to_owned(),
    812                     replaces_line_id: None,
    813                 }],
    814                 line_tombstones: Vec::new(),
    815                 economics: TradeEconomicsProfileV1 {
    816                     profile_id: "mvp-fixed".to_owned(),
    817                     currency_code: "USD".to_owned(),
    818                     currency_exponent: 2,
    819                     rounding_profile: "half-even".to_owned(),
    820                     subtotal_mantissa: "1000".to_owned(),
    821                     discount_total_mantissa: "0".to_owned(),
    822                     adjustment_total_mantissa: "0".to_owned(),
    823                     total_mantissa: "1000".to_owned(),
    824                     adjustments: Vec::<TradeEconomicAdjustmentV1>::new(),
    825                 },
    826                 fulfillment: FulfillmentProfileV1 {
    827                     profile_id: "market-pickup".to_owned(),
    828                     method: "pickup".to_owned(),
    829                     starts_at_unix_s: 1_800_000_000,
    830                     ends_at_unix_s: 1_800_003_600,
    831                     timezone: "America/New_York".to_owned(),
    832                     utc_offset_seconds: -18_000,
    833                     fold: 0,
    834                     location_class: "farmstand".to_owned(),
    835                     requires_private_terms: false,
    836                 },
    837                 cancellation: TradeCancellationProfileV1 {
    838                     profile_id: "buyer-pre-agreement".to_owned(),
    839                     buyer_pre_agreement: true,
    840                     post_agreement_cutoff_unix_s: None,
    841                 },
    842                 private_terms: None,
    843                 proposal_expires_at_unix_s: 1_799_999_000,
    844             },
    845         },
    846     }
    847 }
    848 
    849 fn trade_mutations() -> Vec<TradeMutationEnvelopeV1> {
    850     let proposal = canonical_trade_mutation_content(trade_proposal())
    851         .expect("canonical proposal")
    852         .envelope;
    853     let proposal_id = proposal.mutation_id.expect("proposal mutation id");
    854     let candidate = match &proposal.body {
    855         TradeMutationBodyV1::Proposal { candidate } => candidate.clone(),
    856         _ => unreachable!(),
    857     };
    858     let candidate_id = candidate.candidate_id.expect("candidate id");
    859     let trade_id = proposal.trade_id;
    860     let buyer = proposal.buyer_pubkey;
    861     let seller = proposal.seller_pubkey;
    862     let farm = proposal.farm_id.clone();
    863     let author = proposal.author_pubkey;
    864     let counterparty = proposal.counterparty_pubkey;
    865     let child = |contract_id: &str, body| TradeMutationEnvelopeV1 {
    866         mutation_id: None,
    867         contract_id: contract_id.to_owned(),
    868         schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
    869         trade_id,
    870         root_mutation_id: Some(proposal_id),
    871         buyer_pubkey: buyer,
    872         seller_pubkey: seller,
    873         farm_id: farm.clone(),
    874         parent_mutation_ids: vec![proposal_id],
    875         author_pubkey: author,
    876         counterparty_pubkey: counterparty,
    877         authored_at_unix_s: CREATED_AT,
    878         body,
    879     };
    880     let decision = child(
    881         RADROOTS_TRADE_DECISION_CONTRACT_ID,
    882         TradeMutationBodyV1::Decision {
    883             proposal_mutation_id: proposal_id,
    884             candidate_id,
    885             decision: TradeDecisionV1::Declined {
    886                 reason: "inventory unavailable".to_owned(),
    887             },
    888         },
    889     );
    890     let revision_proposal = child(
    891         RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID,
    892         TradeMutationBodyV1::RevisionProposal { candidate },
    893     );
    894     let revision_decision = child(
    895         RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID,
    896         TradeMutationBodyV1::RevisionDecision {
    897             proposal_mutation_id: proposal_id,
    898             candidate_id,
    899             decision: TradeDecisionV1::Declined {
    900                 reason: "inventory unavailable".to_owned(),
    901             },
    902         },
    903     );
    904     let cancellation = child(
    905         RADROOTS_TRADE_CANCELLATION_CONTRACT_ID,
    906         TradeMutationBodyV1::Cancellation {
    907             target_candidate_id: Some(candidate_id),
    908             target_claim_mutation_id: None,
    909             reason: "cancelled".to_owned(),
    910         },
    911     );
    912     vec![
    913         proposal,
    914         decision,
    915         revision_proposal,
    916         revision_decision,
    917         cancellation,
    918     ]
    919 }
    920 
    921 fn conformance_vectors() -> Cow<'static, str> {
    922     let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    923     if !manifest_dir.join(WORKSPACE_CONTRACT_MARKER_PATH).is_file() {
    924         return Cow::Borrowed(PACKAGED_CORPUS);
    925     }
    926     let workspace_path = manifest_dir.join(WORKSPACE_CORPUS_PATH);
    927     let workspace = fs::read_to_string(&workspace_path)
    928         .unwrap_or_else(|error| panic!("failed to read {}: {error}", workspace_path.display()));
    929     assert_eq!(workspace, PACKAGED_CORPUS, "packaged authored corpus drift");
    930     Cow::Owned(workspace)
    931 }