v1.rs (6894B)
1 #![forbid(unsafe_code)] 2 3 //! Frozen addressable-feed-v1 head-selection semantics. 4 5 #[cfg(not(feature = "std"))] 6 use alloc::string::String; 7 8 use crate::contract::registry_v7::{ 9 ContractMatchError, EventClass, EventContract, identify_event_contract, 10 }; 11 use crate::envelope::{EventEnvelope, EventKindClass, EventTag}; 12 use crate::id::{DTag, EventId, ParseError}; 13 use crate::tag::name::TAG_D; 14 use radroots_identity::PublicKey; 15 16 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 17 pub enum EventHeadCoordinate { 18 Replaceable { 19 kind: u32, 20 pubkey: PublicKey, 21 }, 22 Addressable { 23 kind: u32, 24 pubkey: PublicKey, 25 d_tag: String, 26 }, 27 } 28 29 #[derive(Clone, Debug, PartialEq, Eq)] 30 pub struct EventHeadCandidate { 31 pub coordinate: EventHeadCoordinate, 32 pub event_id: EventId, 33 pub created_at: u64, 34 } 35 36 #[derive(Clone, Debug, PartialEq, Eq)] 37 pub struct CurrentEventHead { 38 pub coordinate: EventHeadCoordinate, 39 pub event_id: EventId, 40 pub created_at: u64, 41 } 42 43 impl From<EventHeadCandidate> for CurrentEventHead { 44 fn from(candidate: EventHeadCandidate) -> Self { 45 Self { 46 coordinate: candidate.coordinate, 47 event_id: candidate.event_id, 48 created_at: candidate.created_at, 49 } 50 } 51 } 52 53 #[derive(Clone, Debug, PartialEq, Eq)] 54 pub enum EventHeadMalformed { 55 InvalidEventId(ParseError), 56 InvalidPubkey(ParseError), 57 MissingDTag, 58 InvalidDTag(ParseError), 59 } 60 61 #[derive(Clone, Debug, PartialEq, Eq)] 62 pub enum EventHeadCandidateResult { 63 Candidate(EventHeadCandidate), 64 NotHeadSelected, 65 NotPersisted, 66 Malformed(EventHeadMalformed), 67 } 68 69 #[derive(Clone, Debug, PartialEq, Eq)] 70 pub enum EventHeadDecision { 71 Applied(CurrentEventHead), 72 SkippedDuplicate, 73 SkippedOlder, 74 SkippedSameTimestampHigherEventId, 75 CoordinateMismatch, 76 } 77 78 pub fn event_head_candidate_for_class( 79 event: &EventEnvelope, 80 class: EventClass, 81 ) -> EventHeadCandidateResult { 82 match class { 83 EventClass::Regular => EventHeadCandidateResult::NotHeadSelected, 84 EventClass::Ephemeral => EventHeadCandidateResult::NotPersisted, 85 EventClass::Replaceable | EventClass::Addressable => { 86 let event_id = *event.id(); 87 let pubkey = *event.author(); 88 let coordinate = if class == EventClass::Replaceable { 89 EventHeadCoordinate::Replaceable { 90 kind: event.kind_u32(), 91 pubkey, 92 } 93 } else { 94 let Some(d_tag) = first_tag_value(event.tag_slices(), TAG_D) else { 95 return EventHeadCandidateResult::Malformed(EventHeadMalformed::MissingDTag); 96 }; 97 let d_tag = match DTag::parse(d_tag) { 98 Ok(d_tag) => d_tag, 99 Err(error) => { 100 return EventHeadCandidateResult::Malformed( 101 EventHeadMalformed::InvalidDTag(error), 102 ); 103 } 104 }; 105 EventHeadCoordinate::Addressable { 106 kind: event.kind_u32(), 107 pubkey, 108 d_tag: d_tag.into_string(), 109 } 110 }; 111 EventHeadCandidateResult::Candidate(EventHeadCandidate { 112 coordinate, 113 event_id, 114 created_at: event.created_at_u64(), 115 }) 116 } 117 } 118 } 119 120 /// Derives the raw NIP-01 head candidate from the numeric event-kind class. 121 /// 122 /// This deliberately does not identify or validate a Radroots product 123 /// contract. Raw replacement ordering must include every signature-verified 124 /// replaceable or addressable event, including unsupported product shapes. 125 pub fn event_head_candidate_for_nip01_event(event: &EventEnvelope) -> EventHeadCandidateResult { 126 event_head_candidate_for_nip01_event_v1(event) 127 } 128 129 /// Derives a raw head candidate with addressable-feed-v1 semantics. 130 pub fn event_head_candidate_for_nip01_event_v1(event: &EventEnvelope) -> EventHeadCandidateResult { 131 let coordinate = match event.kind_class() { 132 EventKindClass::Regular => { 133 return EventHeadCandidateResult::NotHeadSelected; 134 } 135 EventKindClass::Ephemeral => { 136 return EventHeadCandidateResult::NotPersisted; 137 } 138 EventKindClass::Replaceable => EventHeadCoordinate::Replaceable { 139 kind: event.kind_u32(), 140 pubkey: *event.author(), 141 }, 142 EventKindClass::Addressable => EventHeadCoordinate::Addressable { 143 kind: event.kind_u32(), 144 pubkey: *event.author(), 145 d_tag: String::from(first_tag_value(event.tag_slices(), TAG_D).unwrap_or("")), 146 }, 147 }; 148 EventHeadCandidateResult::Candidate(EventHeadCandidate { 149 coordinate, 150 event_id: *event.id(), 151 created_at: event.created_at_u64(), 152 }) 153 } 154 155 pub fn event_head_candidate_for_contract( 156 event: &EventEnvelope, 157 contract: &EventContract, 158 ) -> EventHeadCandidateResult { 159 event_head_candidate_for_class(event, contract.class) 160 } 161 162 pub fn event_head_candidate_for_event( 163 event: &EventEnvelope, 164 ) -> Result<EventHeadCandidateResult, ContractMatchError> { 165 let tags = event.tags_as_vec(); 166 let contract = identify_event_contract(event.kind_u32(), &tags, event.content())?; 167 Ok(event_head_candidate_for_contract(event, contract)) 168 } 169 170 pub fn select_event_head( 171 candidate: EventHeadCandidate, 172 current: Option<&CurrentEventHead>, 173 ) -> EventHeadDecision { 174 select_event_head_v1(candidate, current) 175 } 176 177 /// Selects a raw head with addressable-feed-v1 ordering semantics. 178 pub fn select_event_head_v1( 179 candidate: EventHeadCandidate, 180 current: Option<&CurrentEventHead>, 181 ) -> EventHeadDecision { 182 let Some(current) = current else { 183 return EventHeadDecision::Applied(candidate.into()); 184 }; 185 if candidate.coordinate != current.coordinate { 186 return EventHeadDecision::CoordinateMismatch; 187 } 188 if candidate.event_id == current.event_id { 189 return EventHeadDecision::SkippedDuplicate; 190 } 191 if candidate.created_at > current.created_at { 192 return EventHeadDecision::Applied(candidate.into()); 193 } 194 if candidate.created_at < current.created_at { 195 return EventHeadDecision::SkippedOlder; 196 } 197 if candidate.event_id < current.event_id { 198 EventHeadDecision::Applied(candidate.into()) 199 } else { 200 EventHeadDecision::SkippedSameTimestampHigherEventId 201 } 202 } 203 204 fn first_tag_value<'a>(tags: &'a [EventTag], name: &str) -> Option<&'a str> { 205 tags.iter() 206 .find(|tag| tag.as_slice().first().map(String::as_str) == Some(name)) 207 .and_then(|tag| tag.as_slice().get(1)) 208 .map(String::as_str) 209 } 210 211 #[cfg(test)] 212 #[path = "v1/tests.rs"] 213 #[cfg_attr(coverage_nightly, coverage(off))] 214 mod tests;