lib

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

event_head.rs (16616B)


      1 #![forbid(unsafe_code)]
      2 
      3 #[cfg(not(feature = "std"))]
      4 use alloc::{string::String, vec::Vec};
      5 
      6 use crate::RadrootsNostrEvent;
      7 use crate::contract::{
      8     RadrootsContractMatchError, RadrootsEventClass, RadrootsEventContract, identify_event_contract,
      9 };
     10 use crate::ids::{RadrootsDTag, RadrootsEventId, RadrootsIdParseError, RadrootsPublicKey};
     11 use crate::tags::TAG_D;
     12 
     13 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
     14 pub enum RadrootsEventHeadCoordinate {
     15     Replaceable {
     16         kind: u32,
     17         pubkey: RadrootsPublicKey,
     18     },
     19     Addressable {
     20         kind: u32,
     21         pubkey: RadrootsPublicKey,
     22         d_tag: RadrootsDTag,
     23     },
     24 }
     25 
     26 #[derive(Clone, Debug, PartialEq, Eq)]
     27 pub struct RadrootsEventHeadCandidate {
     28     pub coordinate: RadrootsEventHeadCoordinate,
     29     pub event_id: RadrootsEventId,
     30     pub created_at: u32,
     31 }
     32 
     33 #[derive(Clone, Debug, PartialEq, Eq)]
     34 pub struct RadrootsCurrentEventHead {
     35     pub coordinate: RadrootsEventHeadCoordinate,
     36     pub event_id: RadrootsEventId,
     37     pub created_at: u32,
     38 }
     39 
     40 impl From<RadrootsEventHeadCandidate> for RadrootsCurrentEventHead {
     41     fn from(candidate: RadrootsEventHeadCandidate) -> Self {
     42         Self {
     43             coordinate: candidate.coordinate,
     44             event_id: candidate.event_id,
     45             created_at: candidate.created_at,
     46         }
     47     }
     48 }
     49 
     50 #[derive(Clone, Debug, PartialEq, Eq)]
     51 pub enum RadrootsEventHeadMalformed {
     52     InvalidEventId(RadrootsIdParseError),
     53     InvalidPubkey(RadrootsIdParseError),
     54     MissingDTag,
     55     InvalidDTag(RadrootsIdParseError),
     56 }
     57 
     58 #[derive(Clone, Debug, PartialEq, Eq)]
     59 pub enum RadrootsEventHeadCandidateResult {
     60     Candidate(RadrootsEventHeadCandidate),
     61     NotHeadSelected,
     62     NotPersisted,
     63     Malformed(RadrootsEventHeadMalformed),
     64 }
     65 
     66 #[derive(Clone, Debug, PartialEq, Eq)]
     67 pub enum RadrootsEventHeadDecision {
     68     Applied(RadrootsCurrentEventHead),
     69     SkippedDuplicate,
     70     SkippedOlder,
     71     SkippedSameTimestampHigherEventId,
     72     CoordinateMismatch,
     73 }
     74 
     75 pub fn event_head_candidate_for_class(
     76     event: &RadrootsNostrEvent,
     77     class: RadrootsEventClass,
     78 ) -> RadrootsEventHeadCandidateResult {
     79     match class {
     80         RadrootsEventClass::Regular => RadrootsEventHeadCandidateResult::NotHeadSelected,
     81         RadrootsEventClass::Ephemeral => RadrootsEventHeadCandidateResult::NotPersisted,
     82         RadrootsEventClass::Replaceable | RadrootsEventClass::Addressable => {
     83             let event_id = match RadrootsEventId::parse(&event.id) {
     84                 Ok(event_id) => event_id,
     85                 Err(error) => {
     86                     return RadrootsEventHeadCandidateResult::Malformed(
     87                         RadrootsEventHeadMalformed::InvalidEventId(error),
     88                     );
     89                 }
     90             };
     91             let pubkey = match RadrootsPublicKey::parse(&event.author) {
     92                 Ok(pubkey) => pubkey,
     93                 Err(error) => {
     94                     return RadrootsEventHeadCandidateResult::Malformed(
     95                         RadrootsEventHeadMalformed::InvalidPubkey(error),
     96                     );
     97                 }
     98             };
     99             let coordinate = match class {
    100                 RadrootsEventClass::Replaceable => RadrootsEventHeadCoordinate::Replaceable {
    101                     kind: event.kind,
    102                     pubkey,
    103                 },
    104                 RadrootsEventClass::Addressable => {
    105                     let Some(d_tag) = first_tag_value(&event.tags, TAG_D) else {
    106                         return RadrootsEventHeadCandidateResult::Malformed(
    107                             RadrootsEventHeadMalformed::MissingDTag,
    108                         );
    109                     };
    110                     let d_tag = match RadrootsDTag::parse(d_tag) {
    111                         Ok(d_tag) => d_tag,
    112                         Err(error) => {
    113                             return RadrootsEventHeadCandidateResult::Malformed(
    114                                 RadrootsEventHeadMalformed::InvalidDTag(error),
    115                             );
    116                         }
    117                     };
    118                     RadrootsEventHeadCoordinate::Addressable {
    119                         kind: event.kind,
    120                         pubkey,
    121                         d_tag,
    122                     }
    123                 }
    124                 RadrootsEventClass::Regular | RadrootsEventClass::Ephemeral => unreachable!(),
    125             };
    126             RadrootsEventHeadCandidateResult::Candidate(RadrootsEventHeadCandidate {
    127                 coordinate,
    128                 event_id,
    129                 created_at: event.created_at,
    130             })
    131         }
    132     }
    133 }
    134 
    135 pub fn event_head_candidate_for_contract(
    136     event: &RadrootsNostrEvent,
    137     contract: &RadrootsEventContract,
    138 ) -> RadrootsEventHeadCandidateResult {
    139     event_head_candidate_for_class(event, contract.class)
    140 }
    141 
    142 pub fn event_head_candidate_for_event(
    143     event: &RadrootsNostrEvent,
    144 ) -> Result<RadrootsEventHeadCandidateResult, RadrootsContractMatchError> {
    145     let contract = identify_event_contract(event.kind, &event.tags, &event.content)?;
    146     Ok(event_head_candidate_for_contract(event, contract))
    147 }
    148 
    149 pub fn select_event_head(
    150     candidate: RadrootsEventHeadCandidate,
    151     current: Option<&RadrootsCurrentEventHead>,
    152 ) -> RadrootsEventHeadDecision {
    153     let Some(current) = current else {
    154         return RadrootsEventHeadDecision::Applied(candidate.into());
    155     };
    156     if candidate.coordinate != current.coordinate {
    157         return RadrootsEventHeadDecision::CoordinateMismatch;
    158     }
    159     if candidate.event_id == current.event_id {
    160         return RadrootsEventHeadDecision::SkippedDuplicate;
    161     }
    162     if candidate.created_at > current.created_at {
    163         return RadrootsEventHeadDecision::Applied(candidate.into());
    164     }
    165     if candidate.created_at < current.created_at {
    166         return RadrootsEventHeadDecision::SkippedOlder;
    167     }
    168     if candidate.event_id < current.event_id {
    169         RadrootsEventHeadDecision::Applied(candidate.into())
    170     } else {
    171         RadrootsEventHeadDecision::SkippedSameTimestampHigherEventId
    172     }
    173 }
    174 
    175 fn first_tag_value<'a>(tags: &'a [Vec<String>], name: &str) -> Option<&'a str> {
    176     tags.iter()
    177         .find(|tag| tag.first().map(String::as_str) == Some(name))
    178         .and_then(|tag| tag.get(1))
    179         .map(String::as_str)
    180 }
    181 
    182 #[cfg(test)]
    183 mod tests {
    184     use super::*;
    185     use crate::contract::RadrootsContractMatchError;
    186     use crate::kinds::{
    187         KIND_FOLLOW, KIND_LIST_SET_GENERIC, KIND_ORDER_REQUEST, KIND_POST, KIND_PROFILE,
    188     };
    189 
    190     fn hex_64(character: char) -> String {
    191         core::iter::repeat_n(character, 64).collect()
    192     }
    193 
    194     fn event(
    195         kind: u32,
    196         id: &str,
    197         author: &str,
    198         created_at: u32,
    199         tags: Vec<Vec<String>>,
    200     ) -> RadrootsNostrEvent {
    201         RadrootsNostrEvent {
    202             id: id.to_string(),
    203             author: author.to_string(),
    204             created_at,
    205             kind,
    206             tags,
    207             content: String::new(),
    208             sig: String::new(),
    209         }
    210     }
    211 
    212     fn event_with_content(
    213         kind: u32,
    214         id: &str,
    215         author: &str,
    216         created_at: u32,
    217         tags: Vec<Vec<String>>,
    218         content: &str,
    219     ) -> RadrootsNostrEvent {
    220         let mut event = event(kind, id, author, created_at, tags);
    221         event.content = content.to_string();
    222         event
    223     }
    224 
    225     fn candidate(id: char, created_at: u32) -> RadrootsEventHeadCandidate {
    226         expect_candidate(event_head_candidate_for_class(
    227             &event(10002, &hex_64(id), &hex_64('a'), created_at, Vec::new()),
    228             RadrootsEventClass::Replaceable,
    229         ))
    230     }
    231 
    232     fn expect_candidate(result: RadrootsEventHeadCandidateResult) -> RadrootsEventHeadCandidate {
    233         match result {
    234             RadrootsEventHeadCandidateResult::Candidate(candidate) => candidate,
    235             other => panic!("expected candidate: {other:?}"),
    236         }
    237     }
    238 
    239     #[test]
    240     fn regular_and_ephemeral_events_do_not_create_heads() {
    241         let event = event(1, &hex_64('1'), &hex_64('a'), 1, Vec::new());
    242         assert_eq!(
    243             event_head_candidate_for_class(&event, RadrootsEventClass::Regular),
    244             RadrootsEventHeadCandidateResult::NotHeadSelected
    245         );
    246         assert_eq!(
    247             event_head_candidate_for_class(&event, RadrootsEventClass::Ephemeral),
    248             RadrootsEventHeadCandidateResult::NotPersisted
    249         );
    250     }
    251 
    252     #[test]
    253     fn replaceable_events_use_kind_and_pubkey_coordinates() {
    254         let event = event(10002, &hex_64('1'), &hex_64('a'), 5, Vec::new());
    255         let candidate = expect_candidate(event_head_candidate_for_class(
    256             &event,
    257             RadrootsEventClass::Replaceable,
    258         ));
    259         assert_eq!(
    260             candidate.coordinate,
    261             RadrootsEventHeadCoordinate::Replaceable {
    262                 kind: 10002,
    263                 pubkey: RadrootsPublicKey::parse(hex_64('a')).unwrap()
    264             }
    265         );
    266         assert_eq!(candidate.created_at, 5);
    267     }
    268 
    269     #[test]
    270     fn addressable_events_use_kind_pubkey_and_d_tag_coordinates() {
    271         let event = event(
    272             30023,
    273             &hex_64('2'),
    274             &hex_64('b'),
    275             7,
    276             vec![vec![TAG_D.to_string(), "article-1".to_string()]],
    277         );
    278         let candidate = expect_candidate(event_head_candidate_for_class(
    279             &event,
    280             RadrootsEventClass::Addressable,
    281         ));
    282         assert_eq!(
    283             candidate.coordinate,
    284             RadrootsEventHeadCoordinate::Addressable {
    285                 kind: 30023,
    286                 pubkey: RadrootsPublicKey::parse(hex_64('b')).unwrap(),
    287                 d_tag: RadrootsDTag::parse("article-1").unwrap()
    288             }
    289         );
    290     }
    291 
    292     #[test]
    293     fn addressable_events_require_valid_d_tags() {
    294         let missing = event(30023, &hex_64('2'), &hex_64('b'), 7, Vec::new());
    295         assert_eq!(
    296             event_head_candidate_for_class(&missing, RadrootsEventClass::Addressable),
    297             RadrootsEventHeadCandidateResult::Malformed(RadrootsEventHeadMalformed::MissingDTag)
    298         );
    299 
    300         let invalid = event(
    301             30023,
    302             &hex_64('2'),
    303             &hex_64('b'),
    304             7,
    305             vec![vec![TAG_D.to_string(), "bad d".to_string()]],
    306         );
    307         assert!(matches!(
    308             event_head_candidate_for_class(&invalid, RadrootsEventClass::Addressable),
    309             RadrootsEventHeadCandidateResult::Malformed(RadrootsEventHeadMalformed::InvalidDTag(_))
    310         ));
    311     }
    312 
    313     #[test]
    314     fn malformed_candidates_report_invalid_event_ids_and_pubkeys() {
    315         let bad_event_id = event(10002, "not-hex", &hex_64('a'), 1, Vec::new());
    316         assert!(matches!(
    317             event_head_candidate_for_class(&bad_event_id, RadrootsEventClass::Replaceable),
    318             RadrootsEventHeadCandidateResult::Malformed(
    319                 RadrootsEventHeadMalformed::InvalidEventId(_)
    320             )
    321         ));
    322 
    323         let bad_pubkey = event(10002, &hex_64('1'), "not-hex", 1, Vec::new());
    324         assert!(matches!(
    325             event_head_candidate_for_class(&bad_pubkey, RadrootsEventClass::Replaceable),
    326             RadrootsEventHeadCandidateResult::Malformed(RadrootsEventHeadMalformed::InvalidPubkey(
    327                 _
    328             ))
    329         ));
    330     }
    331 
    332     #[test]
    333     fn event_head_selection_uses_nip01_time_and_lowest_id_rules() {
    334         let current: RadrootsCurrentEventHead = candidate('3', 10).into();
    335 
    336         assert!(matches!(
    337             select_event_head(candidate('1', 1), None),
    338             RadrootsEventHeadDecision::Applied(_)
    339         ));
    340         assert!(matches!(
    341             select_event_head(candidate('4', 11), Some(&current)),
    342             RadrootsEventHeadDecision::Applied(_)
    343         ));
    344         assert_eq!(
    345             select_event_head(candidate('2', 9), Some(&current)),
    346             RadrootsEventHeadDecision::SkippedOlder
    347         );
    348         assert_eq!(
    349             select_event_head(candidate('3', 10), Some(&current)),
    350             RadrootsEventHeadDecision::SkippedDuplicate
    351         );
    352         assert!(matches!(
    353             select_event_head(candidate('2', 10), Some(&current)),
    354             RadrootsEventHeadDecision::Applied(_)
    355         ));
    356         assert_eq!(
    357             select_event_head(candidate('4', 10), Some(&current)),
    358             RadrootsEventHeadDecision::SkippedSameTimestampHigherEventId
    359         );
    360     }
    361 
    362     #[test]
    363     fn event_head_selection_rejects_coordinate_mismatch() {
    364         let current: RadrootsCurrentEventHead = candidate('3', 10).into();
    365         let other = event_head_candidate_for_class(
    366             &event(
    367                 30023,
    368                 &hex_64('2'),
    369                 &hex_64('a'),
    370                 11,
    371                 vec![vec![TAG_D.to_string(), "article".to_string()]],
    372             ),
    373             RadrootsEventClass::Addressable,
    374         );
    375         let other = expect_candidate(other);
    376         assert_eq!(
    377             select_event_head(other, Some(&current)),
    378             RadrootsEventHeadDecision::CoordinateMismatch
    379         );
    380     }
    381 
    382     #[test]
    383     fn contract_bridge_uses_replaceable_event_classes() {
    384         let event = event(KIND_FOLLOW, &hex_64('1'), &hex_64('a'), 1, Vec::new());
    385         let candidate = expect_candidate(event_head_candidate_for_event(&event).expect("contract"));
    386         assert_eq!(
    387             candidate.coordinate,
    388             RadrootsEventHeadCoordinate::Replaceable {
    389                 kind: KIND_FOLLOW,
    390                 pubkey: RadrootsPublicKey::parse(hex_64('a')).unwrap()
    391             }
    392         );
    393     }
    394 
    395     #[test]
    396     fn contract_bridge_uses_addressable_event_classes() {
    397         let event = event(
    398             KIND_LIST_SET_GENERIC,
    399             &hex_64('2'),
    400             &hex_64('b'),
    401             1,
    402             vec![vec![TAG_D.to_string(), "member_of.farms".to_string()]],
    403         );
    404         let candidate = expect_candidate(event_head_candidate_for_event(&event).expect("contract"));
    405         assert_eq!(
    406             candidate.coordinate,
    407             RadrootsEventHeadCoordinate::Addressable {
    408                 kind: KIND_LIST_SET_GENERIC,
    409                 pubkey: RadrootsPublicKey::parse(hex_64('b')).unwrap(),
    410                 d_tag: RadrootsDTag::parse("member_of.farms").unwrap()
    411             }
    412         );
    413     }
    414 
    415     #[test]
    416     fn contract_bridge_uses_profile_replaceable_heads() {
    417         let profile = event_with_content(
    418             KIND_PROFILE,
    419             &hex_64('3'),
    420             &hex_64('c'),
    421             1,
    422             Vec::new(),
    423             r#"{"name":"Alice"}"#,
    424         );
    425         let candidate =
    426             expect_candidate(event_head_candidate_for_event(&profile).expect("profile contract"));
    427         assert_eq!(
    428             candidate.coordinate,
    429             RadrootsEventHeadCoordinate::Replaceable {
    430                 kind: KIND_PROFILE,
    431                 pubkey: RadrootsPublicKey::parse(hex_64('c')).unwrap()
    432             }
    433         );
    434     }
    435 
    436     #[test]
    437     fn contract_bridge_keeps_order_events_out_of_head_selection() {
    438         let order = event_with_content(
    439             KIND_ORDER_REQUEST,
    440             &hex_64('4'),
    441             &hex_64('d'),
    442             1,
    443             vec![
    444                 vec!["p".to_string(), hex_64('e')],
    445                 vec!["a".to_string(), format!("30402:{}:listing-1", hex_64('f'))],
    446                 vec![TAG_D.to_string(), "order-1".to_string()],
    447             ],
    448             "{}",
    449         );
    450         assert_eq!(
    451             event_head_candidate_for_event(&order).expect("order contract"),
    452             RadrootsEventHeadCandidateResult::NotHeadSelected
    453         );
    454     }
    455 
    456     #[test]
    457     fn contract_bridge_reports_unsupported_and_malformed_shapes() {
    458         let unsupported = event(999_999, &hex_64('5'), &hex_64('a'), 1, Vec::new());
    459         assert_eq!(
    460             event_head_candidate_for_event(&unsupported),
    461             Err(RadrootsContractMatchError::UnsupportedKind(999_999))
    462         );
    463 
    464         let malformed_addressable = event(
    465             KIND_LIST_SET_GENERIC,
    466             &hex_64('6'),
    467             &hex_64('a'),
    468             1,
    469             Vec::new(),
    470         );
    471         assert_eq!(
    472             event_head_candidate_for_event(&malformed_addressable),
    473             Err(RadrootsContractMatchError::UnsupportedShape(
    474                 KIND_LIST_SET_GENERIC
    475             ))
    476         );
    477 
    478         let regular_with_d_tag = event(
    479             KIND_POST,
    480             &hex_64('7'),
    481             &hex_64('a'),
    482             1,
    483             vec![vec![TAG_D.to_string(), "not-a-head".to_string()]],
    484         );
    485         assert_eq!(
    486             event_head_candidate_for_event(&regular_with_d_tag).expect("post contract"),
    487             RadrootsEventHeadCandidateResult::NotHeadSelected
    488         );
    489     }
    490 }