lib

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

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 }