food_availability_conformance.rs (27927B)
1 #![cfg(feature = "json")] 2 3 use std::{borrow::Cow, collections::BTreeSet, fs, path::Path}; 4 5 use nostr::secp256k1::Message; 6 use nostr::{Event as NostrEvent, JsonUtil, Keys, SECP256K1}; 7 use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; 8 use radroots_event::food::availability::{ 9 FoodAvailabilityDetails, FoodAvailabilityDetailsParts, FoodAvailabilityError, 10 FoodAvailabilityImage, FoodAvailabilityStatus, FoodContent, FoodCurrency, FoodIdentifier, 11 FoodImageDimensions, FoodPrice, FoodPublishedAt, FoodQuantity, FoodText, FoodUnit, 12 }; 13 use radroots_event::media::AuthoredImage; 14 use radroots_event::wire::{Nip01EventWire, compute_canonical_nip01_event_id}; 15 use radroots_event::{ 16 envelope::EventEnvelope, envelope::EventEnvelopeParts, 17 listing::classified::ClassifiedListingPartition, 18 }; 19 use radroots_event_codec::admission::food_availability::{ 20 RadrootsFoodAvailabilityAdmissionError, RadrootsFoodAvailabilityAdmissionOutcome, 21 verify_and_admit_food_availability_event, 22 }; 23 use radroots_event_codec::admission::food_availability::{ 24 RadrootsFoodAvailabilityRevisionError, validate_food_availability_revision, 25 }; 26 use radroots_event_codec::decode::food_availability::{ 27 RadrootsFoodAvailabilityImageDiagnostic, RadrootsFoodAvailabilityProjectionError, 28 RadrootsFoodAvailabilityProjectionOutcome, RadrootsInboundFoodAvailabilityProjection, 29 project_verified_food_availability_event, 30 }; 31 use radroots_event_codec::encode::food_availability::{ 32 RadrootsFoodAvailabilityEncodeError, authored_food_availability_build_tags, 33 authored_food_availability_to_wire_parts, 34 }; 35 use radroots_event_codec::verify::{RadrootsSignatureVerifiedEvent, verify_nip01_event}; 36 use serde::Deserialize; 37 use serde_json::{Value, json}; 38 39 const SECRET_KEY_ONE: &str = "0000000000000000000000000000000000000000000000000000000000000001"; 40 const SECRET_KEY_TWO: &str = "0000000000000000000000000000000000000000000000000000000000000002"; 41 const PACKAGED_VECTORS: &str = include_str!("fixtures/food_availability_profile.v1.json"); 42 const WORKSPACE_VECTOR_PATH: &str = 43 "../../contracts/conformance/vectors/food_availability/profile.v1.json"; 44 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; 45 46 const VECTOR_IDS: [&str; 40] = [ 47 "food_authored_unit_g_001", 48 "food_authored_unit_kg_002", 49 "food_authored_unit_lb_003", 50 "food_authored_unit_oz_004", 51 "food_authored_unit_each_005", 52 "food_authored_unit_dozen_006", 53 "food_authored_unit_bunch_007", 54 "food_authored_unit_punnet_008", 55 "food_authored_unit_bag_009", 56 "food_authored_unit_basket_010", 57 "food_authored_wire_budget_ascii_max_011", 58 "food_authored_wire_budget_escaped_overflow_012", 59 "food_authored_future_published_at_013", 60 "food_admission_normalizes_decimal_currency_014", 61 "food_admission_optional_standard_tags_015", 62 "food_admission_excludes_operational_before_validation_016", 63 "food_admission_excludes_generic_nip99_017", 64 "food_admission_rejects_ambiguous_markers_018", 65 "food_admission_rejects_wrong_kind_019", 66 "food_admission_rejects_core_tag_shape_020", 67 "food_admission_rejects_prohibited_delivery_021", 68 "food_admission_rejects_price_frequency_022", 69 "food_admission_requires_price_unit_023", 70 "food_admission_bounds_raw_decimal_digits_024", 71 "food_admission_rejects_malformed_price_unit_025", 72 "food_admission_rejects_quantity_unit_mismatch_026", 73 "food_admission_rejects_status_027", 74 "food_admission_rejects_future_published_at_028", 75 "food_admission_preserves_ordered_image_diagnostics_029", 76 "food_admission_bounds_image_projection_030", 77 "food_admission_rejects_invalid_signature_031", 78 "food_revision_accepts_later_created_at_032", 79 "food_revision_rejects_invalid_previous_033", 80 "food_revision_rejects_invalid_current_034", 81 "food_revision_rejects_identifier_coordinate_change_035", 82 "food_revision_rejects_author_coordinate_change_036", 83 "food_revision_rejects_published_at_change_037", 84 "food_revision_rejects_older_created_at_038", 85 "food_revision_equal_time_a_current_039", 86 "food_revision_equal_time_b_current_040", 87 ]; 88 89 #[derive(Debug, Deserialize)] 90 #[serde(deny_unknown_fields)] 91 struct Suite { 92 suite: String, 93 contract_version: String, 94 vectors: Vec<Vector>, 95 } 96 97 #[derive(Debug, Deserialize)] 98 #[serde(deny_unknown_fields)] 99 struct Vector { 100 id: String, 101 kind: String, 102 input: Value, 103 expected: Value, 104 } 105 106 #[derive(Debug, Deserialize)] 107 #[serde(deny_unknown_fields)] 108 struct AuthoredInput { 109 details: DetailsInput, 110 created_at: u64, 111 } 112 113 #[derive(Debug, Deserialize)] 114 #[serde(deny_unknown_fields)] 115 struct DetailsInput { 116 content: TextRecipe, 117 identifier: String, 118 title: String, 119 summary: String, 120 published_at: u64, 121 location: String, 122 price: PriceInput, 123 quantity: Option<QuantityInput>, 124 status: String, 125 images: Vec<ImageInput>, 126 } 127 128 #[derive(Debug, Deserialize)] 129 #[serde(untagged)] 130 enum TextRecipe { 131 Exact(ExactText), 132 Repeat(RepeatText), 133 } 134 135 #[derive(Debug, Deserialize)] 136 #[serde(deny_unknown_fields)] 137 struct ExactText { 138 value: String, 139 } 140 141 #[derive(Debug, Deserialize)] 142 #[serde(deny_unknown_fields)] 143 struct RepeatText { 144 repeat: String, 145 count: usize, 146 } 147 148 #[derive(Debug, Deserialize)] 149 #[serde(deny_unknown_fields)] 150 struct PriceInput { 151 amount: String, 152 currency: String, 153 unit: String, 154 } 155 156 #[derive(Debug, Deserialize)] 157 #[serde(deny_unknown_fields)] 158 struct QuantityInput { 159 amount: String, 160 unit: String, 161 } 162 163 #[derive(Debug, Deserialize)] 164 #[serde(deny_unknown_fields)] 165 struct ImageInput { 166 bytes_utf8: String, 167 url: String, 168 media_type: String, 169 uploaded_at: u64, 170 dimensions: String, 171 } 172 173 #[derive(Debug, Deserialize)] 174 #[serde(deny_unknown_fields)] 175 struct EventInput { 176 event: Value, 177 } 178 179 #[derive(Debug, Deserialize)] 180 #[serde(deny_unknown_fields)] 181 struct RevisionInput { 182 previous: Value, 183 current: Value, 184 } 185 186 #[derive(Debug, Deserialize)] 187 #[serde(deny_unknown_fields)] 188 struct RawEvent { 189 id: String, 190 pubkey: String, 191 created_at: u64, 192 kind: u32, 193 tags: Vec<Vec<String>>, 194 content: String, 195 sig: String, 196 } 197 198 #[test] 199 fn checked_in_food_availability_profile_vectors_execute() { 200 let suite = parse_suite(); 201 for vector in &suite.vectors { 202 execute(vector); 203 } 204 } 205 206 #[test] 207 fn strict_revision_rejects_a_direct_envelope_above_the_authored_wire_bound() { 208 let suite = parse_suite(); 209 let vector = suite 210 .vectors 211 .iter() 212 .find(|vector| vector.id == "food_revision_accepts_later_created_at_032") 213 .expect("revision fixture"); 214 let input = revision_input(vector); 215 let previous = verified_event(&input.previous, vector.id.as_str()); 216 let mut current = raw_event(&input.current, vector.id.as_str()); 217 current.content = core::iter::repeat_n('\0', 64 * 1024).collect(); 218 let current = directly_signed_verified_event(current, vector.id.as_str()); 219 220 let error = validate_food_availability_revision(&previous, ¤t) 221 .expect_err("oversized strict revision must fail"); 222 match error { 223 RadrootsFoodAvailabilityRevisionError::CurrentInvalid(error) => { 224 assert_eq!(error.code(), "food_event_wire_too_large"); 225 } 226 error => panic!("unexpected strict revision error: {error:?}"), 227 } 228 } 229 230 fn parse_suite() -> Suite { 231 let vectors = conformance_vectors(); 232 let suite: Suite = serde_json::from_str(&vectors) 233 .unwrap_or_else(|error| panic!("FoodAvailability vectors must parse: {error}")); 234 assert_eq!(suite.suite, "food_availability_profile"); 235 assert_eq!(suite.contract_version, "1.0.0"); 236 assert_eq!(suite.vectors.len(), VECTOR_IDS.len()); 237 238 let actual = suite 239 .vectors 240 .iter() 241 .map(|vector| vector.id.as_str()) 242 .collect::<BTreeSet<_>>(); 243 assert_eq!(actual.len(), suite.vectors.len(), "duplicate vector ids"); 244 let expected = VECTOR_IDS.iter().copied().collect::<BTreeSet<_>>(); 245 assert_eq!(actual, expected, "FoodAvailability vector inventory drift"); 246 suite 247 } 248 249 fn conformance_vectors() -> Cow<'static, str> { 250 let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH); 251 match fs::read_to_string(&workspace_path) { 252 Ok(canonical) => { 253 assert_eq!( 254 canonical, 255 PACKAGED_VECTORS, 256 "packaged FoodAvailability vectors must match {}", 257 workspace_path.display() 258 ); 259 Cow::Owned(canonical) 260 } 261 Err(error) 262 if error.kind() == std::io::ErrorKind::NotFound 263 && !Path::new(env!("CARGO_MANIFEST_DIR")) 264 .join(WORKSPACE_CONTRACT_MARKER_PATH) 265 .is_file() => 266 { 267 Cow::Borrowed(PACKAGED_VECTORS) 268 } 269 Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), 270 } 271 } 272 273 fn execute(vector: &Vector) { 274 match vector.kind.as_str() { 275 "food_availability.build_authored_draft.valid" => authored_valid( 276 vector, 277 vector.expected["wire_parts"] 278 .get("content_length") 279 .is_some(), 280 ), 281 "food_availability.build_authored_draft.invalid" => authored_invalid(vector), 282 "food_availability.project_verified_event.valid" => projection_valid(vector), 283 "food_availability.project_verified_event.invalid" => projection_invalid(vector), 284 "food_availability.verify_and_admit_event.valid" => verify_and_admit_valid(vector), 285 "food_availability.verify_and_admit_event.invalid" => verify_and_admit_invalid(vector), 286 "food_availability.validate_revision.valid" => revision_valid(vector), 287 "food_availability.validate_revision.invalid" => revision_invalid(vector), 288 kind => panic!("{} uses unsupported vector kind {kind}", vector.id), 289 } 290 } 291 292 fn authored_valid(vector: &Vector, summarized_content: bool) { 293 let input = authored_input(vector); 294 let (details, content) = strict_details(&input.details, &vector.id); 295 let tags = authored_food_availability_build_tags(&details, input.created_at) 296 .unwrap_or_else(|error| panic!("{} tags failed: {error}", vector.id)); 297 let wire = authored_food_availability_to_wire_parts(&details, input.created_at) 298 .unwrap_or_else(|error| panic!("{} wire failed: {error}", vector.id)); 299 assert_eq!(wire.tags, tags, "{}", vector.id); 300 assert_eq!(wire.content, content, "{}", vector.id); 301 302 let actual = if summarized_content { 303 json!({ 304 "wire_parts": { 305 "kind": wire.kind, 306 "content_length": wire.content.len(), 307 "tags": wire.tags, 308 } 309 }) 310 } else { 311 json!({ 312 "wire_parts": { 313 "kind": wire.kind, 314 "content": wire.content, 315 "tags": wire.tags, 316 } 317 }) 318 }; 319 assert_eq!(actual, vector.expected, "{}", vector.id); 320 } 321 322 fn authored_invalid(vector: &Vector) { 323 let input = authored_input(vector); 324 let (details, _) = strict_details(&input.details, &vector.id); 325 let error = authored_food_availability_to_wire_parts(&details, input.created_at) 326 .expect_err("invalid authored vector must fail"); 327 assert_eq!( 328 authored_error_value(&error), 329 vector.expected["error"], 330 "{}", 331 vector.id 332 ); 333 } 334 335 fn authored_input(vector: &Vector) -> AuthoredInput { 336 serde_json::from_value(vector.input.clone()) 337 .unwrap_or_else(|error| panic!("{} authored input is invalid: {error}", vector.id)) 338 } 339 340 fn strict_details(input: &DetailsInput, vector_id: &str) -> (FoodAvailabilityDetails, String) { 341 let content = input.content.materialize(vector_id); 342 let price_unit = FoodUnit::parse(&input.price.unit) 343 .unwrap_or_else(|error| panic!("{vector_id} price unit failed: {error}")); 344 let price = FoodPrice::new( 345 input.price.amount.clone(), 346 FoodCurrency::parse(&input.price.currency) 347 .unwrap_or_else(|error| panic!("{vector_id} currency failed: {error}")), 348 price_unit, 349 ) 350 .unwrap_or_else(|error| panic!("{vector_id} price failed: {error}")); 351 let quantity = input.quantity.as_ref().map(|quantity| { 352 let unit = FoodUnit::parse(&quantity.unit) 353 .unwrap_or_else(|error| panic!("{vector_id} quantity unit failed: {error}")); 354 FoodQuantity::new(quantity.amount.clone(), unit) 355 .unwrap_or_else(|error| panic!("{vector_id} quantity failed: {error}")) 356 }); 357 let images = input 358 .images 359 .iter() 360 .map(|image| strict_image(image, vector_id)) 361 .collect(); 362 let details = FoodAvailabilityDetails::new(FoodAvailabilityDetailsParts { 363 content: FoodContent::new(content.clone()) 364 .unwrap_or_else(|error| panic!("{vector_id} content failed: {error}")), 365 identifier: FoodIdentifier::parse(&input.identifier) 366 .unwrap_or_else(|error| panic!("{vector_id} identifier failed: {error}")), 367 title: FoodText::new(input.title.clone()) 368 .unwrap_or_else(|error| panic!("{vector_id} title failed: {error}")), 369 summary: FoodText::new(input.summary.clone()) 370 .unwrap_or_else(|error| panic!("{vector_id} summary failed: {error}")), 371 published_at: FoodPublishedAt::new(input.published_at) 372 .unwrap_or_else(|error| panic!("{vector_id} published_at failed: {error}")), 373 location: FoodText::new(input.location.clone()) 374 .unwrap_or_else(|error| panic!("{vector_id} location failed: {error}")), 375 price, 376 quantity, 377 status: FoodAvailabilityStatus::parse(&input.status) 378 .unwrap_or_else(|error| panic!("{vector_id} status failed: {error}")), 379 images, 380 }) 381 .unwrap_or_else(|error| panic!("{vector_id} details failed: {error}")); 382 (details, content) 383 } 384 385 fn strict_image(input: &ImageInput, vector_id: &str) -> FoodAvailabilityImage { 386 let bytes = input.bytes_utf8.as_bytes(); 387 let hash = Sha256::digest(bytes); 388 let media_type = MediaType::parse(&input.media_type) 389 .unwrap_or_else(|error| panic!("{vector_id} image media type failed: {error}")); 390 let verified = BlobDescriptor::new( 391 BlobUrl::parse(&input.url) 392 .unwrap_or_else(|error| panic!("{vector_id} image URL failed: {error}")), 393 hash, 394 bytes.len() as u64, 395 media_type.clone(), 396 input.uploaded_at, 397 ) 398 .unwrap_or_else(|error| panic!("{vector_id} image descriptor failed: {error}")) 399 .approve_reference() 400 .unwrap_or_else(|error| panic!("{vector_id} image reference failed: {error}")) 401 .verify_bytes(bytes, &media_type) 402 .unwrap_or_else(|error| panic!("{vector_id} image bytes failed: {error}")); 403 let image = AuthoredImage::try_from_verified_descriptor(verified) 404 .unwrap_or_else(|error| panic!("{vector_id} authored image failed: {error}")); 405 let dimensions = FoodImageDimensions::parse(&input.dimensions) 406 .unwrap_or_else(|error| panic!("{vector_id} image dimensions failed: {error}")); 407 FoodAvailabilityImage::new(image, dimensions) 408 } 409 410 impl TextRecipe { 411 fn materialize(&self, vector_id: &str) -> String { 412 match self { 413 Self::Exact(value) => value.value.clone(), 414 Self::Repeat(value) => { 415 assert_eq!( 416 value.repeat.chars().count(), 417 1, 418 "{vector_id} repeat value must be one character" 419 ); 420 value.repeat.repeat(value.count) 421 } 422 } 423 } 424 } 425 426 fn authored_error_value(error: &RadrootsFoodAvailabilityEncodeError) -> Value { 427 match error { 428 RadrootsFoodAvailabilityEncodeError::Domain(FoodAvailabilityError::PublishedAtFuture { 429 published_at, 430 created_at, 431 }) => json!({ 432 "code": error.code(), 433 "message": error.to_string(), 434 "published_at": published_at, 435 "created_at": created_at, 436 }), 437 RadrootsFoodAvailabilityEncodeError::EventWireTooLarge { max, actual } => json!({ 438 "code": error.code(), 439 "message": error.to_string(), 440 "max": max, 441 "actual": actual, 442 }), 443 _ => json!({ 444 "code": error.code(), 445 "message": error.to_string(), 446 }), 447 } 448 } 449 450 fn projection_valid(vector: &Vector) { 451 let input = event_input(vector); 452 let verified = verified_event(&input.event, &vector.id); 453 let event_id = verified.event().id_hex().to_string(); 454 let outcome = project_verified_food_availability_event(&verified) 455 .unwrap_or_else(|error| panic!("{} projection failed: {error}", vector.id)); 456 let actual = match outcome { 457 RadrootsFoodAvailabilityProjectionOutcome::Focused(projection) 458 if vector.expected.get("projection").is_some() => 459 { 460 json!({ 461 "outcome": "focused", 462 "event_id": event_id, 463 "projection": projection_value(&projection), 464 }) 465 } 466 RadrootsFoodAvailabilityProjectionOutcome::Focused(projection) => json!({ 467 "outcome": "focused", 468 "event_id": event_id, 469 "image_count": projection.images().len(), 470 "diagnostics": diagnostic_codes(projection.diagnostics()), 471 "first_raw_tag": projection.images().first().expect("first bounded image").raw_tag(), 472 "last_raw_tag": projection.images().last().expect("last bounded image").raw_tag(), 473 }), 474 RadrootsFoodAvailabilityProjectionOutcome::Excluded(partition) => json!({ 475 "outcome": "excluded", 476 "event_id": event_id, 477 "partition": partition_name(partition), 478 }), 479 _ => panic!("{} returned an unsupported projection outcome", vector.id), 480 }; 481 assert_eq!(actual, vector.expected, "{}", vector.id); 482 } 483 484 fn projection_invalid(vector: &Vector) { 485 let input = event_input(vector); 486 let verified = verified_event(&input.event, &vector.id); 487 let error = project_verified_food_availability_event(&verified) 488 .expect_err("invalid focused FoodAvailability projection vector must fail"); 489 assert_eq!( 490 projection_error_value(&error), 491 vector.expected["error"], 492 "{}", 493 vector.id 494 ); 495 } 496 497 fn verify_and_admit_valid(vector: &Vector) { 498 let input = event_input(vector); 499 let verified = verified_event(&input.event, &vector.id); 500 let outcome = verify_and_admit_food_availability_event(verified.into_event()) 501 .unwrap_or_else(|error| panic!("{} admission failed: {error}", vector.id)); 502 let admitted = match outcome { 503 RadrootsFoodAvailabilityAdmissionOutcome::Admitted(admitted) => *admitted, 504 RadrootsFoodAvailabilityAdmissionOutcome::Excluded(excluded) => { 505 panic!( 506 "{} was unexpectedly excluded as {:?}", 507 vector.id, 508 excluded.partition() 509 ) 510 } 511 _ => panic!("{} returned an unsupported admission outcome", vector.id), 512 }; 513 let actual = json!({ 514 "outcome": "admitted", 515 "event_id": admitted.event().id_hex(), 516 "projection": projection_value(admitted.projection()), 517 }); 518 assert_eq!(actual, vector.expected, "{}", vector.id); 519 } 520 521 fn verify_and_admit_invalid(vector: &Vector) { 522 let input = event_input(vector); 523 let envelope = canonical_envelope(&input.event, &vector.id); 524 let error = verify_and_admit_food_availability_event(envelope) 525 .expect_err("invalid signature vector must fail verification"); 526 assert_eq!( 527 admission_error_value(&error), 528 vector.expected["error"], 529 "{}", 530 vector.id 531 ); 532 } 533 534 fn event_input(vector: &Vector) -> EventInput { 535 serde_json::from_value(vector.input.clone()) 536 .unwrap_or_else(|error| panic!("{} event input is invalid: {error}", vector.id)) 537 } 538 539 fn admission_error_value(error: &RadrootsFoodAvailabilityAdmissionError) -> Value { 540 json!({ 541 "code": error.code(), 542 "message": error.to_string(), 543 }) 544 } 545 546 fn projection_error_value(error: &RadrootsFoodAvailabilityProjectionError) -> Value { 547 json!({ 548 "code": error.code(), 549 "message": error.to_string(), 550 }) 551 } 552 553 fn projection_value(projection: &RadrootsInboundFoodAvailabilityProjection) -> Value { 554 let quantity = projection.quantity().map(|quantity| { 555 json!({ 556 "amount": quantity.amount(), 557 "unit": quantity.unit().as_str(), 558 }) 559 }); 560 let images = projection 561 .images() 562 .iter() 563 .map(|image| { 564 let dimensions = image.dimensions().map(|dimensions| { 565 json!({ 566 "width": dimensions.width(), 567 "height": dimensions.height(), 568 }) 569 }); 570 json!({ 571 "raw_tag": image.raw_tag(), 572 "url": image.url(), 573 "dimensions": dimensions, 574 "diagnostics": diagnostic_codes(image.diagnostics()), 575 "qualifies": image.qualifies(), 576 }) 577 }) 578 .collect::<Vec<_>>(); 579 json!({ 580 "content": projection.content().as_str(), 581 "identifier": projection.identifier().as_str(), 582 "title": projection.title().as_str(), 583 "summary": projection.summary().as_str(), 584 "published_at": projection.published_at().as_u64(), 585 "location": projection.location().as_str(), 586 "price": { 587 "amount": projection.price().amount(), 588 "currency": projection.price().currency().as_str(), 589 "unit": projection.price().unit().as_str(), 590 }, 591 "quantity": quantity, 592 "status": projection.status().as_str(), 593 "images": images, 594 "diagnostics": diagnostic_codes(projection.diagnostics()), 595 }) 596 } 597 598 fn diagnostic_codes(diagnostics: &[RadrootsFoodAvailabilityImageDiagnostic]) -> Vec<&'static str> { 599 diagnostics 600 .iter() 601 .map(|diagnostic| diagnostic.code()) 602 .collect() 603 } 604 605 fn partition_name(partition: ClassifiedListingPartition) -> &'static str { 606 match partition { 607 ClassifiedListingPartition::FocusedFoodAvailability => "focused_food_availability", 608 ClassifiedListingPartition::OperationalListing => "operational_listing", 609 ClassifiedListingPartition::GenericNip99 => "generic_nip99", 610 ClassifiedListingPartition::Ambiguous => "ambiguous", 611 } 612 } 613 614 fn revision_valid(vector: &Vector) { 615 let input = revision_input(vector); 616 let previous = verified_event(&input.previous, &vector.id); 617 let current = verified_event(&input.current, &vector.id); 618 validate_food_availability_revision(&previous, ¤t) 619 .unwrap_or_else(|error| panic!("{} revision failed: {error}", vector.id)); 620 let actual = json!({ 621 "result": "accepted", 622 "current_event_id": current.event().id_hex(), 623 }); 624 assert_eq!(actual, vector.expected, "{}", vector.id); 625 } 626 627 fn revision_invalid(vector: &Vector) { 628 let input = revision_input(vector); 629 let previous = verified_event(&input.previous, &vector.id); 630 let current = verified_event(&input.current, &vector.id); 631 let error = validate_food_availability_revision(&previous, ¤t) 632 .expect_err("invalid revision vector must fail"); 633 assert_eq!( 634 revision_error_value(&error), 635 vector.expected["error"], 636 "{}", 637 vector.id 638 ); 639 } 640 641 fn revision_input(vector: &Vector) -> RevisionInput { 642 serde_json::from_value(vector.input.clone()) 643 .unwrap_or_else(|error| panic!("{} revision input is invalid: {error}", vector.id)) 644 } 645 646 fn revision_error_value(error: &RadrootsFoodAvailabilityRevisionError) -> Value { 647 json!({ 648 "code": error.code(), 649 "message": error.to_string(), 650 }) 651 } 652 653 fn verified_event(value: &Value, vector_id: &str) -> RadrootsSignatureVerifiedEvent { 654 let raw = raw_event(value, vector_id); 655 let raw_json = serde_json::to_string(value).expect("raw event must serialize"); 656 let event = NostrEvent::from_json(&raw_json) 657 .unwrap_or_else(|error| panic!("{vector_id} is not a NIP-01 event: {error}")); 658 assert_eq!(event.id.to_string(), raw.id, "{vector_id}"); 659 assert_eq!(event.sig.to_string(), raw.sig, "{vector_id}"); 660 assert_eq!(event.pubkey.to_string(), raw.pubkey, "{vector_id}"); 661 assert_eq!(event.created_at.as_secs(), raw.created_at, "{vector_id}"); 662 assert_eq!(u32::from(event.kind.as_u16()), raw.kind, "{vector_id}"); 663 assert_eq!( 664 event 665 .tags 666 .iter() 667 .map(|tag| tag.as_slice().to_vec()) 668 .collect::<Vec<_>>(), 669 raw.tags, 670 "{vector_id}" 671 ); 672 assert_eq!(event.content, raw.content, "{vector_id}"); 673 event 674 .verify() 675 .unwrap_or_else(|error| panic!("{vector_id} NIP-01 verification failed: {error}")); 676 677 let keys = keys_for_pubkey(&raw.pubkey, vector_id); 678 let message = Message::from_digest(event.id.to_bytes()); 679 let deterministic = SECP256K1.sign_schnorr_no_aux_rand( 680 &message, 681 &nostr::secp256k1::Keypair::from_secret_key(SECP256K1, keys.secret_key()), 682 ); 683 assert_eq!( 684 event.sig, deterministic, 685 "{vector_id} deterministic signature drift" 686 ); 687 688 verify_nip01_event(canonical_envelope(value, vector_id)) 689 .unwrap_or_else(|error| panic!("{vector_id} Radroots verification failed: {error}")) 690 } 691 692 fn canonical_envelope(value: &Value, vector_id: &str) -> EventEnvelope { 693 let raw_json = serde_json::to_string(value).expect("raw event must serialize"); 694 let wire = Nip01EventWire::parse_json(&raw_json) 695 .unwrap_or_else(|error| panic!("{vector_id} wire parsing failed: {error}")); 696 wire.verify_id() 697 .unwrap_or_else(|error| panic!("{vector_id} event id failed: {error}")); 698 wire.into_envelope() 699 .unwrap_or_else(|error| panic!("{vector_id} envelope conversion failed: {error}")) 700 } 701 702 fn raw_event(value: &Value, vector_id: &str) -> RawEvent { 703 serde_json::from_value(value.clone()) 704 .unwrap_or_else(|error| panic!("{vector_id} raw event is invalid: {error}")) 705 } 706 707 fn directly_signed_verified_event( 708 mut raw: RawEvent, 709 vector_id: &str, 710 ) -> RadrootsSignatureVerifiedEvent { 711 let id = compute_canonical_nip01_event_id( 712 &raw.pubkey, 713 raw.created_at, 714 raw.kind, 715 &raw.tags, 716 &raw.content, 717 ) 718 .unwrap_or_else(|error| panic!("{vector_id} direct event id failed: {error}")); 719 let nostr_id = nostr::EventId::from_hex(&id.to_hex()) 720 .unwrap_or_else(|error| panic!("{vector_id} direct event id conversion failed: {error}")); 721 let keys = keys_for_pubkey(&raw.pubkey, vector_id); 722 let message = Message::from_digest(nostr_id.to_bytes()); 723 let signature = SECP256K1.sign_schnorr_no_aux_rand( 724 &message, 725 &nostr::secp256k1::Keypair::from_secret_key(SECP256K1, keys.secret_key()), 726 ); 727 raw.id = id.into_string(); 728 raw.sig = signature.to_string(); 729 730 let event = EventEnvelope::new(EventEnvelopeParts { 731 id: raw.id, 732 author: raw.pubkey, 733 created_at: raw.created_at, 734 kind: raw.kind, 735 tags: raw.tags, 736 content: raw.content, 737 sig: raw.sig, 738 }) 739 .unwrap_or_else(|error| panic!("{vector_id} direct envelope failed: {error}")); 740 verify_nip01_event(event) 741 .unwrap_or_else(|error| panic!("{vector_id} direct verification failed: {error}")) 742 } 743 744 fn keys_for_pubkey(pubkey: &str, vector_id: &str) -> Keys { 745 for secret in [SECRET_KEY_ONE, SECRET_KEY_TWO] { 746 let keys = Keys::parse(secret).expect("fixed secret key must parse"); 747 if keys.public_key().to_string() == pubkey { 748 return keys; 749 } 750 } 751 panic!("{vector_id} uses an ungoverned fixture pubkey {pubkey}") 752 }