services_hardening_event_decisions.rs (21698B)
1 #![forbid(unsafe_code)] 2 3 use serde_json::Value; 4 use sha2::{Digest, Sha256}; 5 use std::collections::{BTreeMap, BTreeSet}; 6 7 use radroots_event::{ 8 id::{MutationId, TradeId}, 9 trade::{TradeMutationEnvelopeV1, trade_mutation_from_canonical_content}, 10 }; 11 use radroots_event_codec::{ 12 decode::trade::{RadrootsTradeMutationError, validate_trade_mutation_tags}, 13 encode::trade::{trade_mutation_event_build, trade_mutation_event_build_with_extra_tags}, 14 }; 15 16 const DECISION: &str = 17 include_str!("../../../contracts/architecture/decisions/services_hardening_events.v1.json"); 18 const REGISTRY: &str = 19 include_str!("../../../contracts/event_store/event_contract_registry_v7.inventory.json"); 20 const TRADE_VECTORS: &str = include_str!( 21 "../../../contracts/conformance/vectors/trade/mutation_index_tags_decision.v1.json" 22 ); 23 const RHI_VECTORS: &str = include_str!( 24 "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json" 25 ); 26 const AUTHORED_CORPUS: &str = 27 include_str!("../../../contracts/conformance/vectors/event/authored_operations.v1.json"); 28 29 fn json(source: &str) -> Value { 30 serde_json::from_str(source).expect("services-hardening machine contract must be valid JSON") 31 } 32 33 fn strings(value: &Value, field: &str) -> Vec<String> { 34 value[field] 35 .as_array() 36 .expect("field must be an array") 37 .iter() 38 .map(|entry| entry.as_str().expect("entry must be a string").to_owned()) 39 .collect() 40 } 41 42 fn error_codes(vectors: &Value) -> BTreeSet<String> { 43 vectors["vectors"] 44 .as_array() 45 .expect("vector list") 46 .iter() 47 .filter_map(|vector| vector["expected"]["error_code"].as_str()) 48 .map(str::to_owned) 49 .collect() 50 } 51 52 fn verify_attestation_digest_vector(vector: &Value) -> String { 53 let canonical_payload = vector["expected"]["canonical_statement_payload_utf8"] 54 .as_str() 55 .expect("canonical statement payload"); 56 assert_eq!( 57 serde_json::to_string(&vector["input"]["statement_payload"]).unwrap(), 58 canonical_payload 59 ); 60 let mut hasher = Sha256::new(); 61 hasher.update(b"radroots:rhi-evidence-attestation-statement:v1\0"); 62 hasher.update(canonical_payload.as_bytes()); 63 let digest = hex::encode(hasher.finalize()); 64 assert_eq!(vector["expected"]["statement_digest"], digest); 65 assert_eq!(vector["expected"]["report_id"], digest); 66 let canonical_event = vector["expected"]["canonical_event_content_utf8"] 67 .as_str() 68 .expect("canonical event content"); 69 let mut event_content = json(canonical_event); 70 assert_eq!(event_content["report_id"], digest); 71 assert_eq!(event_content["statement_digest"], digest); 72 let event_object = event_content.as_object_mut().expect("report object"); 73 event_object.remove("report_id"); 74 event_object.remove("statement_digest"); 75 assert_eq!( 76 serde_json::to_string(&event_content).unwrap(), 77 canonical_payload 78 ); 79 digest 80 } 81 82 #[test] 83 fn services_hardening_event_decision_reserves_unique_exact_kinds() { 84 let decision = json(DECISION); 85 assert_eq!( 86 decision["schema"], 87 "radroots.services-hardening.event-decisions.v1" 88 ); 89 assert_eq!(decision["decision_state"], "reserved_preimplementation"); 90 91 let expected_trade = BTreeSet::from([3470_u64, 3471, 3472, 3473, 3474]); 92 let trade = decision["trade_mutation"]["event_kinds"] 93 .as_array() 94 .expect("trade event kinds"); 95 let actual_trade = trade 96 .iter() 97 .map(|entry| entry["kind"].as_u64().expect("numeric kind")) 98 .collect::<BTreeSet<_>>(); 99 assert_eq!(actual_trade, expected_trade); 100 101 let registry = json(REGISTRY); 102 let registered = registry["kind_contracts"] 103 .as_array() 104 .expect("registry kind contracts") 105 .iter() 106 .map(|entry| entry["kind"].as_u64().expect("registered numeric kind")) 107 .collect::<Vec<_>>(); 108 for kind in expected_trade { 109 assert!(registered.contains(&kind), "trade kind {kind} must exist"); 110 } 111 let attestation_kind = decision["rhi_attestation"]["kind"] 112 .as_u64() 113 .expect("attestation kind"); 114 assert_eq!(attestation_kind, 3441); 115 assert_eq!( 116 registered 117 .iter() 118 .filter(|kind| **kind == attestation_kind) 119 .count(), 120 1, 121 "implemented attestation kind must be uniquely registered" 122 ); 123 } 124 125 #[test] 126 fn services_hardening_trade_tag_cardinality_and_query_contract_is_exact() { 127 let decision = json(DECISION); 128 let trade = &decision["trade_mutation"]; 129 assert_eq!(trade["event_class"], "regular_immutable"); 130 assert_eq!( 131 strings(trade, "canonical_tag_order"), 132 [ 133 "contract", 134 "d:trade", 135 "x:mutation", 136 "x:root", 137 "x:parent_sorted", 138 "p:buyer", 139 "p:seller", 140 ] 141 ); 142 let tags = trade["tags"].as_array().expect("trade tags"); 143 assert_eq!(tags.len(), 7); 144 assert_eq!(tags[1]["cardinality"], "exactly_one"); 145 assert_eq!(tags[2]["cardinality"], "exactly_one"); 146 assert_eq!( 147 tags[3]["cardinality"], 148 "proposal_zero_other_mutations_exactly_one" 149 ); 150 assert_eq!( 151 tags[4]["cardinality"], 152 "proposal_zero_other_mutations_one_to_four_sorted_unique" 153 ); 154 assert_eq!(tags[5]["cardinality"], "first_of_exactly_two"); 155 assert_eq!(tags[6]["cardinality"], "second_of_exactly_two"); 156 assert_eq!( 157 trade["validation"]["legacy_contract_d_p_e_shape_accepted"], 158 false 159 ); 160 161 let vectors = json(TRADE_VECTORS); 162 let first = &vectors["vectors"][0]; 163 assert_eq!(first["expected"]["kind"], 3472); 164 assert_eq!( 165 first["expected"]["tags"] 166 .as_array() 167 .expect("exact tags") 168 .len(), 169 8 170 ); 171 assert_eq!(first["expected"]["tags"][1].as_array().unwrap().len(), 2); 172 assert_eq!(first["expected"]["tags"][6].as_array().unwrap().len(), 2); 173 assert_eq!(first["expected"]["tags"][7].as_array().unwrap().len(), 2); 174 let kinds = vectors["vectors"] 175 .as_array() 176 .expect("trade decision vectors") 177 .iter() 178 .filter_map(|vector| vector["expected"]["kind"].as_u64()) 179 .collect::<BTreeSet<_>>(); 180 assert_eq!(kinds, BTreeSet::from([3470, 3471, 3472, 3473, 3474])); 181 assert_eq!( 182 error_codes(&vectors), 183 BTreeSet::from([ 184 "caller_structural_tag_forbidden".to_owned(), 185 "duplicate_trade_tag".to_owned(), 186 "legacy_parent_event_tag".to_owned(), 187 "missing_parent_tag".to_owned(), 188 "missing_mutation_tag".to_owned(), 189 "missing_root_tag".to_owned(), 190 "noncanonical_parent_order".to_owned(), 191 "party_tag_order_mismatch".to_owned(), 192 "unexpected_parent_tag".to_owned(), 193 "unexpected_root_tag".to_owned(), 194 ]) 195 ); 196 } 197 198 #[test] 199 fn every_trade_mutation_vector_executes_the_public_boundary() { 200 let vectors = json(TRADE_VECTORS); 201 let mut bases = BTreeMap::new(); 202 let mut positive_count = 0; 203 for vector in vectors["vectors"] 204 .as_array() 205 .expect("trade vectors") 206 .iter() 207 .filter(|vector| vector["kind"] == "trade.mutation_index_tags.valid") 208 { 209 let id = vector["id"].as_str().expect("vector id"); 210 let envelope = trade_vector_envelope(vector); 211 let expected_kind = vector["expected"]["kind"].as_u64().expect("kind") as u32; 212 assert_eq!(envelope.mutation_kind().nostr_kind(), expected_kind, "{id}"); 213 let built = trade_mutation_event_build(envelope).expect("typed trade builder"); 214 assert_eq!(built.kind, expected_kind, "{id}"); 215 let parsed = trade_mutation_from_canonical_content(&built.content) 216 .expect("builder emits canonical trade content"); 217 if let Some(tags) = vector["expected"].get("tags") { 218 assert_eq!(built.tags, json_tags(tags), "{id}"); 219 } else { 220 assert_eq!( 221 tag_semantics(&built.tags), 222 vector["expected"]["tag_names_and_semantics"] 223 .as_array() 224 .expect("tag semantics") 225 .iter() 226 .map(|value| value.as_str().expect("semantic").to_owned()) 227 .collect::<Vec<_>>(), 228 "{id}" 229 ); 230 assert_lineage_tags_match_vector(&built.tags, &vector["input"]); 231 } 232 validate_trade_mutation_tags(&parsed, &built.tags).expect("positive vector"); 233 assert!(bases.insert(id.to_owned(), (parsed, built.tags)).is_none()); 234 positive_count += 1; 235 } 236 assert_eq!(positive_count, 5); 237 238 let proposal = bases 239 .get("trade_mutation_index_proposal_002") 240 .expect("proposal base") 241 .0 242 .clone(); 243 for vector in vectors["vectors"] 244 .as_array() 245 .expect("trade vectors") 246 .iter() 247 .filter(|vector| vector["kind"] == "trade.mutation_index_tags.invalid") 248 { 249 let id = vector["id"].as_str().expect("vector id"); 250 let expected = vector["expected"]["error_code"] 251 .as_str() 252 .expect("error code"); 253 let input = &vector["input"]; 254 let actual = if let Some(extra_tags) = input.get("builder_extra_tags") { 255 assert_eq!(vector["expected"]["layer"], "builder", "{id}"); 256 trade_mutation_event_build_with_extra_tags(proposal.clone(), &json_tags(extra_tags)) 257 .expect_err("builder negative must fail") 258 } else { 259 assert_eq!(vector["expected"]["layer"], "wire", "{id}"); 260 let base = input["base"].as_str().expect("wire negative base"); 261 let (envelope, mut tags) = bases.get(base).expect("known positive base").clone(); 262 if let Some(pattern) = input.get("remove_tag") { 263 let before = tags.len(); 264 tags.retain(|tag| !tag_matches_pattern(tag, pattern)); 265 assert_eq!(tags.len() + 1, before, "{id}"); 266 } 267 if let Some(patterns) = input.get("remove_tags") { 268 for pattern in patterns.as_array().expect("remove tag patterns") { 269 let before = tags.len(); 270 tags.retain(|tag| !tag_matches_pattern(tag, pattern)); 271 assert!(tags.len() < before, "{id}"); 272 } 273 } 274 if let Some(tag) = input.get("append_tag") { 275 tags.push(json_tag(tag)); 276 } 277 if let Some(indexes) = input.get("swap_tag_indexes") { 278 let indexes = indexes.as_array().expect("swap indexes"); 279 tags.swap( 280 indexes[0].as_u64().expect("left index") as usize, 281 indexes[1].as_u64().expect("right index") as usize, 282 ); 283 } 284 if let Some(replacement) = input.get("replace_tag") { 285 let index = replacement["index"].as_u64().expect("replace index") as usize; 286 tags[index] = json_tag(&replacement["tag"]); 287 } 288 validate_trade_mutation_tags(&envelope, &tags).expect_err("negative vector must fail") 289 }; 290 assert_eq!(actual.code(), expected, "{id}"); 291 } 292 } 293 294 fn json_tag(value: &Value) -> Vec<String> { 295 value 296 .as_array() 297 .expect("tag array") 298 .iter() 299 .map(|value| value.as_str().expect("tag value").to_owned()) 300 .collect() 301 } 302 303 fn json_tags(value: &Value) -> Vec<Vec<String>> { 304 value 305 .as_array() 306 .expect("tag list") 307 .iter() 308 .map(json_tag) 309 .collect() 310 } 311 312 fn tag_matches_pattern(tag: &[String], pattern: &Value) -> bool { 313 let pattern = json_tag(pattern); 314 if pattern.first().map(String::as_str) == Some("x") && pattern.len() == 2 { 315 tag.first() == pattern.first() && tag.get(2) == pattern.get(1) 316 } else { 317 tag == pattern 318 } 319 } 320 321 fn tag_semantics(tags: &[Vec<String>]) -> Vec<String> { 322 let mut party = 0; 323 tags.iter() 324 .map(|tag| match tag.first().map(String::as_str) { 325 Some("contract") => "contract".to_owned(), 326 Some("d") => "d:trade".to_owned(), 327 Some("x") => format!("x:{}", tag.get(2).expect("x marker")), 328 Some("p") => { 329 party += 1; 330 format!( 331 "p:{}", 332 if party == 1 { 333 "buyer-first" 334 } else { 335 "seller-second" 336 } 337 ) 338 } 339 _ => panic!("unexpected structural tag"), 340 }) 341 .collect() 342 } 343 344 fn trade_from_corpus(contract_id: &str) -> TradeMutationEnvelopeV1 { 345 let corpus = json(AUTHORED_CORPUS); 346 let content = corpus["vectors"] 347 .as_array() 348 .expect("authored vectors") 349 .iter() 350 .find(|vector| vector["input"]["contract_id"] == contract_id) 351 .and_then(|vector| vector["expected"]["content"].as_str()) 352 .expect("typed trade content"); 353 trade_mutation_from_canonical_content(content).expect("canonical trade mutation") 354 } 355 356 fn trade_vector_envelope(vector: &Value) -> TradeMutationEnvelopeV1 { 357 let input = &vector["input"]; 358 let contract_id = input["contract_id"].as_str().expect("contract id"); 359 let mut envelope = trade_from_corpus(contract_id); 360 envelope.mutation_id = None; 361 envelope.root_mutation_id = input 362 .get("root_mutation_id") 363 .and_then(Value::as_str) 364 .map(|value| MutationId::parse(value).expect("root mutation id")); 365 envelope.parent_mutation_ids = input 366 .get("parent_mutation_ids") 367 .and_then(Value::as_array) 368 .map(|parents| { 369 parents 370 .iter() 371 .map(|value| { 372 MutationId::parse(value.as_str().expect("parent mutation id")) 373 .expect("parent mutation id") 374 }) 375 .collect() 376 }) 377 .unwrap_or_default(); 378 if let Some(trade_id) = input.get("trade_id").and_then(Value::as_str) { 379 envelope.trade_id = TradeId::parse(trade_id).expect("trade id"); 380 } 381 if let Some(buyer) = input.get("buyer_pubkey").and_then(Value::as_str) { 382 envelope.buyer_pubkey = radroots_identity::PublicKey::from_hex(buyer).expect("buyer key"); 383 envelope.author_pubkey = envelope.buyer_pubkey; 384 } 385 if let Some(seller) = input.get("seller_pubkey").and_then(Value::as_str) { 386 envelope.seller_pubkey = 387 radroots_identity::PublicKey::from_hex(seller).expect("seller key"); 388 envelope.counterparty_pubkey = envelope.seller_pubkey; 389 } 390 envelope 391 } 392 393 fn assert_lineage_tags_match_vector(tags: &[Vec<String>], input: &Value) { 394 let root = tags 395 .iter() 396 .find(|tag| tag.get(2).map(String::as_str) == Some("root")) 397 .map(|tag| tag[1].as_str()); 398 assert_eq!(root, input.get("root_mutation_id").and_then(Value::as_str)); 399 let parents = tags 400 .iter() 401 .filter(|tag| tag.get(2).map(String::as_str) == Some("parent")) 402 .map(|tag| tag[1].as_str()) 403 .collect::<Vec<_>>(); 404 let expected: Vec<&str> = input 405 .get("parent_mutation_ids") 406 .and_then(Value::as_array) 407 .map(|values| values.iter().map(|value| value.as_str().unwrap()).collect()) 408 .unwrap_or_default(); 409 assert_eq!(parents, expected); 410 } 411 412 fn synthetic_all_fields_envelope() -> TradeMutationEnvelopeV1 { 413 trade_vector_envelope(&json(TRADE_VECTORS)["vectors"][0]) 414 } 415 416 #[test] 417 fn additional_trade_mutation_shape_permutations_and_bounds_fail_closed() { 418 let built = trade_mutation_event_build(synthetic_all_fields_envelope()).expect("trade builder"); 419 let envelope = trade_mutation_from_canonical_content(&built.content).expect("trade content"); 420 let tags = built.tags; 421 422 let mut malformed_x = tags.clone(); 423 malformed_x[2].pop(); 424 assert_eq!( 425 validate_trade_mutation_tags(&envelope, &malformed_x).unwrap_err(), 426 RadrootsTradeMutationError::InvalidTagShape 427 ); 428 429 let mut unknown_marker = tags.clone(); 430 unknown_marker[2][2] = "unknown".to_owned(); 431 assert_eq!( 432 validate_trade_mutation_tags(&envelope, &unknown_marker).unwrap_err(), 433 RadrootsTradeMutationError::InvalidTagShape 434 ); 435 436 let mut duplicate_mutation = tags.clone(); 437 duplicate_mutation.insert(3, duplicate_mutation[2].clone()); 438 assert_eq!( 439 validate_trade_mutation_tags(&envelope, &duplicate_mutation).unwrap_err(), 440 RadrootsTradeMutationError::InvalidTagShape 441 ); 442 443 let mut duplicate_root = tags.clone(); 444 duplicate_root.insert(4, duplicate_root[3].clone()); 445 assert_eq!( 446 validate_trade_mutation_tags(&envelope, &duplicate_root).unwrap_err(), 447 RadrootsTradeMutationError::InvalidTagShape 448 ); 449 450 let mut five_parents = tags.clone(); 451 for value in ["6", "7", "8"] { 452 five_parents.insert( 453 five_parents.len() - 2, 454 vec!["x".to_owned(), value.repeat(64), "parent".to_owned()], 455 ); 456 } 457 assert_eq!( 458 validate_trade_mutation_tags(&envelope, &five_parents).unwrap_err(), 459 RadrootsTradeMutationError::InvalidTagShape 460 ); 461 462 let mut duplicate_parent = tags.clone(); 463 duplicate_parent.insert(6, duplicate_parent[5].clone()); 464 assert_eq!( 465 validate_trade_mutation_tags(&envelope, &duplicate_parent).unwrap_err(), 466 RadrootsTradeMutationError::NoncanonicalParentOrder 467 ); 468 469 let mut uppercase_identifier = tags.clone(); 470 uppercase_identifier[2][1] = "A".repeat(64); 471 assert_eq!( 472 validate_trade_mutation_tags(&envelope, &uppercase_identifier).unwrap_err(), 473 RadrootsTradeMutationError::InvalidIdentifier 474 ); 475 476 let mut unknown_tag = tags.clone(); 477 unknown_tag.push(vec!["t".to_owned(), "trade".to_owned()]); 478 assert_eq!( 479 validate_trade_mutation_tags(&envelope, &unknown_tag).unwrap_err(), 480 RadrootsTradeMutationError::UnexpectedTag 481 ); 482 483 let mut missing_party = tags; 484 missing_party.pop(); 485 assert_eq!( 486 validate_trade_mutation_tags(&envelope, &missing_party).unwrap_err(), 487 RadrootsTradeMutationError::InvalidTagShape 488 ); 489 490 let oversized = vec![vec!["t".to_owned(), "trade".to_owned()]; 10_000]; 491 assert_eq!( 492 validate_trade_mutation_tags(&envelope, &oversized).unwrap_err(), 493 RadrootsTradeMutationError::InvalidTagShape 494 ); 495 496 assert_eq!( 497 trade_mutation_event_build_with_extra_tags( 498 trade_from_corpus("radroots.trade.proposal.v1"), 499 &[vec!["t".to_owned(), "trade".to_owned()]], 500 ) 501 .unwrap_err(), 502 RadrootsTradeMutationError::UnexpectedTag 503 ); 504 assert_eq!( 505 trade_mutation_event_build_with_extra_tags( 506 trade_from_corpus("radroots.trade.proposal.v1"), 507 &[ 508 vec!["t".to_owned(), "trade".to_owned()], 509 vec!["p".to_owned(), "0".repeat(64)], 510 ], 511 ) 512 .unwrap_err(), 513 RadrootsTradeMutationError::CallerStructuralTagForbidden 514 ); 515 } 516 517 #[test] 518 fn services_hardening_attestation_is_immutable_and_fully_bound() { 519 let decision = json(DECISION); 520 let attestation = &decision["rhi_attestation"]; 521 assert_eq!(attestation["kind"], 3441); 522 assert_eq!(attestation["event_class"], "regular_immutable"); 523 assert_eq!(attestation["replaceability"], "none"); 524 assert_eq!(attestation["content_encoding"], "RFC8785_JCS_JSON_UTF8"); 525 assert_eq!( 526 attestation["fixed_values"]["attestation_method"], 527 "signed_evidence_snapshot" 528 ); 529 assert_eq!( 530 strings(attestation, "canonical_tag_order"), 531 [ 532 "contract", 533 "d:trade", 534 "x:claim", 535 "x:statement", 536 "t:outcome", 537 "x:supersedes_report", 538 "e:supersedes_event", 539 ] 540 ); 541 assert_eq!( 542 attestation["supersession"]["requires_both_references_or_neither"], 543 true 544 ); 545 assert_eq!( 546 attestation["supersession"]["report_id_equals_statement_digest"], 547 true 548 ); 549 assert_eq!( 550 attestation["supersession"]["relay_arrival_order_authoritative"], 551 false 552 ); 553 554 let vectors = json(RHI_VECTORS); 555 let positive = &vectors["vectors"][0]; 556 assert_eq!(positive["expected"]["kind"], 3441); 557 verify_attestation_digest_vector(positive); 558 assert_eq!(positive["expected"]["tags"][1].as_array().unwrap().len(), 2); 559 assert_eq!(positive["expected"]["tags"][4].as_array().unwrap().len(), 2); 560 assert_eq!( 561 vectors["vectors"][1]["expected"]["mutates_prior_report"], 562 false 563 ); 564 verify_attestation_digest_vector(&vectors["vectors"][1]); 565 assert_eq!( 566 vectors["vectors"][1]["expected"]["tags"][6] 567 .as_array() 568 .unwrap() 569 .len(), 570 2 571 ); 572 assert_eq!( 573 error_codes(&vectors), 574 BTreeSet::from([ 575 "caller_structural_tag_forbidden".to_owned(), 576 "duplicate_statement_tag".to_owned(), 577 "duplicate_trade_tag".to_owned(), 578 "incomplete_supersession_reference".to_owned(), 579 "invalid_attestation_kind".to_owned(), 580 "invalid_outcome".to_owned(), 581 "issuer_author_mismatch".to_owned(), 582 "missing_claim_tag".to_owned(), 583 "noncanonical_report_content".to_owned(), 584 "stale_trade_generation".to_owned(), 585 "statement_digest_mismatch".to_owned(), 586 ]) 587 ); 588 let stale = &vectors["vectors"][11]; 589 assert_eq!(stale["expected"]["layer"], "admission"); 590 }