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 }