tests.rs (16667B)
1 use super::*; 2 3 use nostr::{Keys, SECP256K1, secp256k1::Message}; 4 use radroots_event::{ 5 envelope::EventEnvelope, envelope::EventEnvelopeParts, wire::compute_canonical_nip01_event_id, 6 }; 7 8 use crate::{ 9 deletion::admission::verify_and_admit_nip09_deletion_request_event, 10 test_fixtures::{FIXTURE_ALICE_SECRET_KEY_HEX, FIXTURE_BOB_SECRET_KEY_HEX}, 11 verification::verify_nip01_event, 12 }; 13 14 const TARGET_TIME: u64 = 1_800_100_100; 15 16 #[test] 17 fn nip09_evaluator_codes_and_opaque_evidence_are_stable() { 18 let outcome_codes = [ 19 (RadrootsNip09SuppressionOutcome::Visible, "visible"), 20 (RadrootsNip09SuppressionOutcome::Suppressed, "suppressed"), 21 ]; 22 for (outcome, code) in outcome_codes { 23 assert_eq!(outcome.code(), code); 24 } 25 26 let reason_codes = [ 27 ( 28 RadrootsNip09SuppressionReason::DeletionRequestImmune, 29 "deletion_request_immune", 30 ), 31 ( 32 RadrootsNip09SuppressionReason::NoAuthorizedReference, 33 "deletion_no_authorized_reference", 34 ), 35 ( 36 RadrootsNip09SuppressionReason::RequestAuthorMismatch, 37 "deletion_request_author_mismatch", 38 ), 39 ( 40 RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, 41 "deletion_address_cutoff_precedes_target", 42 ), 43 ( 44 RadrootsNip09SuppressionReason::EventIdReference, 45 "deletion_event_id_reference", 46 ), 47 ( 48 RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, 49 "deletion_address_reference", 50 ), 51 ( 52 RadrootsNip09SuppressionReason::EventIdAndAddressReference, 53 "deletion_event_id_and_address_reference", 54 ), 55 ]; 56 for (reason, code) in reason_codes { 57 assert_eq!(reason.code(), code); 58 } 59 } 60 61 #[test] 62 fn nip09_evaluator_deletion_requests_are_immune() { 63 let target = admitted_request( 64 FIXTURE_ALICE_SECRET_KEY_HEX, 65 TARGET_TIME, 66 vec![event_reference("a")], 67 "target", 68 ); 69 let request = admitted_request( 70 FIXTURE_ALICE_SECRET_KEY_HEX, 71 TARGET_TIME + 1, 72 vec![event_reference(target.event().id_hex().as_str())], 73 "attempt", 74 ); 75 76 let decision = evaluate_nip09_suppression(target.verified_event(), &[request]); 77 78 assert_decision( 79 &decision, 80 RadrootsNip09SuppressionOutcome::Visible, 81 RadrootsNip09SuppressionReason::DeletionRequestImmune, 82 ); 83 assert!(decision.event_reference().is_none()); 84 assert!(decision.address_reference().is_none()); 85 } 86 87 #[test] 88 fn nip09_evaluator_exact_event_reference_is_time_independent() { 89 let target = verified_event( 90 FIXTURE_ALICE_SECRET_KEY_HEX, 91 TARGET_TIME, 92 1, 93 Vec::new(), 94 "target", 95 ); 96 let request = admitted_request( 97 FIXTURE_ALICE_SECRET_KEY_HEX, 98 TARGET_TIME - 1, 99 vec![event_reference(target.event().id_hex().as_str())], 100 "exact", 101 ); 102 let request_id = *request.event().id(); 103 104 let decision = evaluate_nip09_suppression(&target, &[request]); 105 106 assert_decision( 107 &decision, 108 RadrootsNip09SuppressionOutcome::Suppressed, 109 RadrootsNip09SuppressionReason::EventIdReference, 110 ); 111 assert_eq!( 112 decision 113 .event_reference() 114 .expect("event evidence") 115 .request_id(), 116 &request_id 117 ); 118 assert!(decision.address_reference().is_none()); 119 } 120 121 #[test] 122 fn nip09_evaluator_address_cutoff_is_inclusive_and_later_replacement_is_visible() { 123 let at_cutoff = verified_event( 124 FIXTURE_ALICE_SECRET_KEY_HEX, 125 TARGET_TIME, 126 30_402, 127 vec![d_tag("produce")], 128 "at cutoff", 129 ); 130 let coordinate = coordinate_for(&at_cutoff, "produce"); 131 let request = admitted_request( 132 FIXTURE_ALICE_SECRET_KEY_HEX, 133 TARGET_TIME, 134 vec![address_reference(coordinate.as_str())], 135 "address", 136 ); 137 let request_id = *request.event().id(); 138 139 let suppressed = evaluate_nip09_suppression(&at_cutoff, core::slice::from_ref(&request)); 140 assert_decision( 141 &suppressed, 142 RadrootsNip09SuppressionOutcome::Suppressed, 143 RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff, 144 ); 145 let evidence = suppressed.address_reference().expect("address evidence"); 146 assert_eq!(evidence.coordinate(), &coordinate); 147 assert_eq!(evidence.inclusive_cutoff(), TARGET_TIME); 148 assert_eq!(evidence.request_id(), &request_id); 149 150 let later = verified_event( 151 FIXTURE_ALICE_SECRET_KEY_HEX, 152 TARGET_TIME + 1, 153 30_402, 154 vec![d_tag("produce")], 155 "later", 156 ); 157 let visible = evaluate_nip09_suppression(&later, &[request]); 158 assert_decision( 159 &visible, 160 RadrootsNip09SuppressionOutcome::Visible, 161 RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, 162 ); 163 assert_eq!( 164 visible 165 .address_reference() 166 .expect("stale address evidence") 167 .inclusive_cutoff(), 168 TARGET_TIME 169 ); 170 } 171 172 #[test] 173 fn nip09_evaluator_author_mismatch_and_unrelated_requests_are_distinct() { 174 let target = verified_event( 175 FIXTURE_ALICE_SECRET_KEY_HEX, 176 TARGET_TIME, 177 1, 178 Vec::new(), 179 "target", 180 ); 181 let mismatch = admitted_request( 182 FIXTURE_BOB_SECRET_KEY_HEX, 183 TARGET_TIME + 1, 184 vec![event_reference(target.event().id_hex().as_str())], 185 "wrong author", 186 ); 187 let unrelated = admitted_request( 188 FIXTURE_ALICE_SECRET_KEY_HEX, 189 TARGET_TIME + 1, 190 vec![event_reference("f")], 191 "unrelated", 192 ); 193 194 let mismatch_decision = evaluate_nip09_suppression(&target, &[mismatch]); 195 assert_decision( 196 &mismatch_decision, 197 RadrootsNip09SuppressionOutcome::Visible, 198 RadrootsNip09SuppressionReason::RequestAuthorMismatch, 199 ); 200 201 let unrelated_decision = evaluate_nip09_suppression(&target, &[unrelated]); 202 assert_decision( 203 &unrelated_decision, 204 RadrootsNip09SuppressionOutcome::Visible, 205 RadrootsNip09SuppressionReason::NoAuthorizedReference, 206 ); 207 } 208 209 #[test] 210 fn nip09_evaluator_authorized_stale_address_precedes_unauthorized_exact_event() { 211 let target = verified_event( 212 FIXTURE_ALICE_SECRET_KEY_HEX, 213 TARGET_TIME, 214 0, 215 Vec::new(), 216 "profile", 217 ); 218 let coordinate = coordinate_for(&target, ""); 219 let stale = admitted_request( 220 FIXTURE_ALICE_SECRET_KEY_HEX, 221 TARGET_TIME - 1, 222 vec![address_reference(coordinate.as_str())], 223 "stale", 224 ); 225 let mismatch = admitted_request( 226 FIXTURE_BOB_SECRET_KEY_HEX, 227 TARGET_TIME + 1, 228 vec![event_reference(target.event().id_hex().as_str())], 229 "wrong author", 230 ); 231 232 let decision = evaluate_nip09_suppression(&target, &[mismatch, stale]); 233 234 assert_decision( 235 &decision, 236 RadrootsNip09SuppressionOutcome::Visible, 237 RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget, 238 ); 239 } 240 241 #[test] 242 fn nip09_evaluator_exact_event_dominates_stale_address_reference() { 243 let target = verified_event( 244 FIXTURE_ALICE_SECRET_KEY_HEX, 245 TARGET_TIME, 246 30_000, 247 vec![d_tag("harvest")], 248 "target", 249 ); 250 let coordinate = coordinate_for(&target, "harvest"); 251 let exact = admitted_request( 252 FIXTURE_ALICE_SECRET_KEY_HEX, 253 TARGET_TIME - 2, 254 vec![event_reference(target.event().id_hex().as_str())], 255 "exact", 256 ); 257 let stale = admitted_request( 258 FIXTURE_ALICE_SECRET_KEY_HEX, 259 TARGET_TIME - 1, 260 vec![address_reference(coordinate.as_str())], 261 "stale", 262 ); 263 264 let decision = evaluate_nip09_suppression(&target, &[stale, exact]); 265 266 assert_decision( 267 &decision, 268 RadrootsNip09SuppressionOutcome::Suppressed, 269 RadrootsNip09SuppressionReason::EventIdReference, 270 ); 271 assert!(decision.event_reference().is_some()); 272 assert!(decision.address_reference().is_some()); 273 } 274 275 #[test] 276 fn nip09_evaluator_reduction_is_order_and_repeat_invariant() { 277 let target = verified_event( 278 FIXTURE_ALICE_SECRET_KEY_HEX, 279 TARGET_TIME, 280 39_999, 281 vec![d_tag("market"), d_tag("ignored")], 282 "target", 283 ); 284 let coordinate = coordinate_for(&target, "market"); 285 let exact_a = admitted_request( 286 FIXTURE_ALICE_SECRET_KEY_HEX, 287 TARGET_TIME - 10, 288 vec![event_reference(target.event().id_hex().as_str())], 289 "exact a", 290 ); 291 let exact_b = admitted_request( 292 FIXTURE_ALICE_SECRET_KEY_HEX, 293 TARGET_TIME + 20, 294 vec![event_reference(target.event().id_hex().as_str())], 295 "exact b", 296 ); 297 let address_a = admitted_request( 298 FIXTURE_ALICE_SECRET_KEY_HEX, 299 TARGET_TIME + 20, 300 vec![address_reference(coordinate.as_str())], 301 "address a", 302 ); 303 let address_b = admitted_request( 304 FIXTURE_ALICE_SECRET_KEY_HEX, 305 TARGET_TIME + 20, 306 vec![address_reference(coordinate.as_str())], 307 "address b", 308 ); 309 let address_older = admitted_request( 310 FIXTURE_ALICE_SECRET_KEY_HEX, 311 TARGET_TIME + 10, 312 vec![address_reference(coordinate.as_str())], 313 "address older", 314 ); 315 let lower_exact_id = *core::cmp::min(exact_a.event().id(), exact_b.event().id()); 316 let lower_address_id = *core::cmp::min(address_a.event().id(), address_b.event().id()); 317 318 let forward = vec![ 319 exact_a.clone(), 320 address_a.clone(), 321 exact_b.clone(), 322 address_b.clone(), 323 address_older.clone(), 324 ]; 325 let repeated_reverse = vec![ 326 address_b, 327 address_older, 328 exact_b, 329 address_a, 330 exact_a.clone(), 331 exact_a, 332 ]; 333 let expected = evaluate_nip09_suppression(&target, &forward); 334 let actual = evaluate_nip09_suppression(&target, &repeated_reverse); 335 336 assert_eq!(actual, expected); 337 assert_decision( 338 &actual, 339 RadrootsNip09SuppressionOutcome::Suppressed, 340 RadrootsNip09SuppressionReason::EventIdAndAddressReference, 341 ); 342 assert_eq!( 343 actual 344 .event_reference() 345 .expect("event evidence") 346 .request_id(), 347 &lower_exact_id 348 ); 349 let address = actual.address_reference().expect("address evidence"); 350 assert_eq!(address.coordinate(), &coordinate); 351 assert_eq!(address.inclusive_cutoff(), TARGET_TIME + 20); 352 assert_eq!(address.request_id(), &lower_address_id); 353 } 354 355 #[test] 356 fn nip09_borrowed_request_iterator_matches_slice_evaluation() { 357 let target = verified_event( 358 FIXTURE_ALICE_SECRET_KEY_HEX, 359 TARGET_TIME, 360 30_402, 361 vec![d_tag("borrowed")], 362 "target", 363 ); 364 let coordinate = coordinate_for(&target, "borrowed"); 365 let requests = vec![ 366 admitted_request( 367 FIXTURE_ALICE_SECRET_KEY_HEX, 368 TARGET_TIME + 1, 369 vec![event_reference("f")], 370 "unrelated", 371 ), 372 admitted_request( 373 FIXTURE_ALICE_SECRET_KEY_HEX, 374 TARGET_TIME - 1, 375 vec![event_reference(target.event().id_hex().as_str())], 376 "exact", 377 ), 378 admitted_request( 379 FIXTURE_BOB_SECRET_KEY_HEX, 380 TARGET_TIME + 2, 381 vec![address_reference(coordinate.as_str())], 382 "wrong author", 383 ), 384 admitted_request( 385 FIXTURE_ALICE_SECRET_KEY_HEX, 386 TARGET_TIME + 3, 387 vec![ 388 event_reference(target.event().id_hex().as_str()), 389 address_reference(coordinate.as_str()), 390 ], 391 "exact and address", 392 ), 393 ]; 394 let expected = evaluate_nip09_suppression_v1(&target, &requests); 395 let actual = evaluate_nip09_suppression_from_borrowed_requests_v1( 396 &target, 397 requests.iter().rev().filter(|request| { 398 request 399 .projection() 400 .event_targets() 401 .iter() 402 .any(|reference| reference.event_id() == target.event().id()) 403 || request 404 .projection() 405 .address_targets() 406 .iter() 407 .any(|reference| reference.coordinate() == &coordinate) 408 }), 409 ); 410 411 assert_eq!(actual, expected); 412 assert_decision( 413 &actual, 414 RadrootsNip09SuppressionOutcome::Suppressed, 415 RadrootsNip09SuppressionReason::EventIdAndAddressReference, 416 ); 417 } 418 419 #[test] 420 fn nip09_evaluator_uses_generic_coordinates_and_rejects_malformed_first_d() { 421 for kind in [0, 3, 10_000, 19_999] { 422 let target = verified_event( 423 FIXTURE_ALICE_SECRET_KEY_HEX, 424 TARGET_TIME, 425 kind, 426 Vec::new(), 427 "replaceable", 428 ); 429 let coordinate = coordinate_for(&target, ""); 430 let request = admitted_request( 431 FIXTURE_ALICE_SECRET_KEY_HEX, 432 TARGET_TIME, 433 vec![address_reference(coordinate.as_str())], 434 "replaceable", 435 ); 436 assert_eq!( 437 evaluate_nip09_suppression(&target, &[request]).reason(), 438 RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff 439 ); 440 } 441 442 let target = verified_event( 443 FIXTURE_ALICE_SECRET_KEY_HEX, 444 TARGET_TIME, 445 30_000, 446 vec![vec!["d".to_string()], d_tag("later")], 447 "malformed", 448 ); 449 let later_coordinate = coordinate_for(&target, "later"); 450 let request = admitted_request( 451 FIXTURE_ALICE_SECRET_KEY_HEX, 452 TARGET_TIME, 453 vec![address_reference(later_coordinate.as_str())], 454 "must not match", 455 ); 456 assert_eq!( 457 evaluate_nip09_suppression(&target, &[request]).reason(), 458 RadrootsNip09SuppressionReason::NoAuthorizedReference 459 ); 460 } 461 462 fn assert_decision( 463 decision: &RadrootsNip09SuppressionDecision, 464 outcome: RadrootsNip09SuppressionOutcome, 465 reason: RadrootsNip09SuppressionReason, 466 ) { 467 assert_eq!(decision.outcome(), outcome); 468 assert_eq!(decision.reason(), reason); 469 } 470 471 fn event_reference(value: &str) -> Vec<String> { 472 vec!["e".to_string(), normalize_event_id(value)] 473 } 474 475 fn address_reference(value: &str) -> Vec<String> { 476 vec!["a".to_string(), value.to_string()] 477 } 478 479 fn d_tag(value: &str) -> Vec<String> { 480 vec!["d".to_string(), value.to_string()] 481 } 482 483 fn normalize_event_id(value: &str) -> String { 484 if value.len() == 1 { 485 value.repeat(64) 486 } else { 487 value.to_string() 488 } 489 } 490 491 fn coordinate_for(target: &RadrootsSignatureVerifiedEvent, identifier: &str) -> Nip01Coordinate { 492 Nip01Coordinate::parse(format!( 493 "{}:{}:{identifier}", 494 target.event().kind_u32(), 495 target.event().author().to_hex() 496 )) 497 .expect("target coordinate") 498 } 499 500 fn admitted_request( 501 secret_key_hex: &str, 502 created_at: u64, 503 tags: Vec<Vec<String>>, 504 content: &str, 505 ) -> RadrootsAdmittedNip09DeletionRequestEventV1 { 506 verify_and_admit_nip09_deletion_request_event(signed_event( 507 secret_key_hex, 508 created_at, 509 KIND_DELETION_REQUEST, 510 tags, 511 content, 512 )) 513 .expect("valid admitted deletion request") 514 } 515 516 fn verified_event( 517 secret_key_hex: &str, 518 created_at: u64, 519 kind: u32, 520 tags: Vec<Vec<String>>, 521 content: &str, 522 ) -> RadrootsSignatureVerifiedEvent { 523 verify_nip01_event(signed_event( 524 secret_key_hex, 525 created_at, 526 kind, 527 tags, 528 content, 529 )) 530 .expect("valid verified event") 531 } 532 533 fn signed_event( 534 secret_key_hex: &str, 535 created_at: u64, 536 kind: u32, 537 tags: Vec<Vec<String>>, 538 content: &str, 539 ) -> EventEnvelope { 540 let keys = Keys::parse(secret_key_hex).expect("fixed fixture secret key must parse"); 541 let author = keys.public_key().to_string(); 542 let id = compute_canonical_nip01_event_id(author.as_str(), created_at, kind, &tags, content) 543 .expect("canonical event id"); 544 let nostr_id = nostr::EventId::from_hex(&id.to_hex()).expect("Nostr event id"); 545 let message = Message::from_digest(nostr_id.to_bytes()); 546 let signature = SECP256K1.sign_schnorr_no_aux_rand( 547 &message, 548 &nostr::secp256k1::Keypair::from_secret_key(SECP256K1, keys.secret_key()), 549 ); 550 551 EventEnvelope::new(EventEnvelopeParts { 552 id: id.into_string(), 553 author, 554 created_at, 555 kind, 556 tags, 557 content: content.to_string(), 558 sig: signature.to_string(), 559 }) 560 .expect("valid event envelope") 561 }