1use super::version;
15use crate::stored_event::event_kind;
16use nostr_sdk::prelude::*;
17
18const TAG_SUBKIND: &str = "vsk";
19const TAG_ENTITY: &str = "eid";
20const TAG_EVERSION: &str = "ev";
21const TAG_EPREV: &str = "ep";
22const TAG_VERSION: &str = "v";
23const PROTOCOL_VERSION: &str = "1";
24pub const TAG_AUTHORITY_CITATION: &str = "vac";
33
34#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct AuthorityCitation {
40 pub entity_id: [u8; 32],
42 pub version: u64,
44 pub edition_hash: [u8; 32],
46}
47
48impl AuthorityCitation {
49 pub fn to_tag(&self) -> Tag {
51 Tag::custom(
52 TagKind::Custom(TAG_AUTHORITY_CITATION.into()),
53 [
54 crate::simd::hex::bytes_to_hex_32(&self.entity_id),
55 self.version.to_string(),
56 crate::simd::hex::bytes_to_hex_32(&self.edition_hash),
57 ],
58 )
59 }
60
61 pub fn from_tags(tags: &Tags) -> Option<AuthorityCitation> {
65 let s = tags.iter().find_map(|t| {
66 let s = t.as_slice();
67 (s.len() >= 4 && s[0] == TAG_AUTHORITY_CITATION).then(|| (s[1].clone(), s[2].clone(), s[3].clone()))
68 })?;
69 let valid_hex = |h: &str| h.len() == 64 && h.bytes().all(|b| b.is_ascii_hexdigit());
70 if !valid_hex(&s.0) || !valid_hex(&s.2) {
71 return None;
72 }
73 Some(AuthorityCitation {
74 entity_id: crate::simd::hex::hex_to_bytes_32(&s.0),
75 version: s.1.parse().ok()?,
76 edition_hash: crate::simd::hex::hex_to_bytes_32(&s.2),
77 })
78 }
79}
80
81pub fn build_edition_inner(
86 author: PublicKey,
87 vsk: &str,
88 entity_id: &[u8; 32],
89 version: u64,
90 prev_hash: Option<&[u8; 32]>,
91 content: &str,
92 created_at_secs: u64,
93 authority: Option<&AuthorityCitation>,
94) -> UnsignedEvent {
95 let mut tags = vec![
96 Tag::custom(TagKind::Custom(TAG_SUBKIND.into()), [vsk.to_string()]),
97 Tag::custom(TagKind::Custom(TAG_ENTITY.into()), [crate::simd::hex::bytes_to_hex_32(entity_id)]),
98 Tag::custom(TagKind::Custom(TAG_EVERSION.into()), [version.to_string()]),
99 Tag::custom(TagKind::Custom(TAG_VERSION.into()), [PROTOCOL_VERSION.to_string()]),
100 ];
101 if let Some(p) = prev_hash {
102 tags.push(Tag::custom(TagKind::Custom(TAG_EPREV.into()), [crate::simd::hex::bytes_to_hex_32(p)]));
103 }
104 if let Some(a) = authority {
108 tags.push(a.to_tag());
109 }
110 EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), content)
111 .tags(tags)
112 .custom_created_at(Timestamp::from_secs(created_at_secs))
113 .build(author)
114}
115
116#[derive(Clone, Debug)]
118pub struct ParsedEdition {
119 pub author: PublicKey,
121 pub vsk: String,
122 pub entity_id: [u8; 32],
123 pub version: u64,
124 pub prev_hash: Option<[u8; 32]>,
125 pub content: String,
126 pub self_hash: [u8; 32],
128 pub created_at: u64,
129 pub inner_id: [u8; 32],
130 pub authority: Option<AuthorityCitation>,
133}
134
135#[derive(Debug, PartialEq, Eq)]
136pub enum EditionError {
137 BadSignature,
138 MissingField(&'static str),
139 BadField(&'static str),
140}
141
142fn decode_hash(hex: &str, field: &'static str) -> Result<[u8; 32], EditionError> {
143 if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
144 return Err(EditionError::BadField(field));
145 }
146 Ok(crate::simd::hex::hex_to_bytes_32(hex))
147}
148
149pub fn parse_edition_inner(inner: &Event) -> Result<ParsedEdition, EditionError> {
153 inner.verify().map_err(|_| EditionError::BadSignature)?;
154 for name in [TAG_SUBKIND, TAG_ENTITY, TAG_EVERSION, TAG_EPREV, TAG_AUTHORITY_CITATION] {
158 let count = inner
159 .tags
160 .iter()
161 .filter(|t| t.as_slice().first().map(|s| s.as_str() == name).unwrap_or(false))
162 .count();
163 if count > 1 {
164 return Err(EditionError::BadField("duplicate authority tag"));
165 }
166 }
167 let get = |name: &str| -> Option<String> {
168 inner.tags.iter().find_map(|t| {
169 let s = t.as_slice();
170 (s.len() >= 2 && s[0] == name).then(|| s[1].clone())
171 })
172 };
173 let vsk = get(TAG_SUBKIND).ok_or(EditionError::MissingField("vsk"))?;
174 let entity_id = decode_hash(&get(TAG_ENTITY).ok_or(EditionError::MissingField("eid"))?, "eid")?;
175 let ev_raw = get(TAG_EVERSION).ok_or(EditionError::MissingField("ev"))?;
178 if ev_raw.is_empty() || !ev_raw.bytes().all(|b| b.is_ascii_digit()) {
179 return Err(EditionError::BadField("ev"));
180 }
181 let version: u64 = ev_raw.parse().map_err(|_| EditionError::BadField("ev"))?;
182 let prev_hash = match get(TAG_EPREV) {
183 Some(h) => Some(decode_hash(&h, "ep")?),
184 None => None,
185 };
186 let content = inner.content.clone();
187 let self_hash = version::edition_hash(&entity_id, version, prev_hash.as_ref(), content.as_bytes());
188 Ok(ParsedEdition {
189 author: inner.pubkey,
190 vsk,
191 entity_id,
192 version,
193 prev_hash,
194 content,
195 self_hash,
196 created_at: inner.created_at.as_secs(),
197 inner_id: inner.id.to_bytes(),
198 authority: AuthorityCitation::from_tags(&inner.tags),
199 })
200}
201
202impl ParsedEdition {
203 pub fn to_fold_edition(&self) -> version::Edition {
205 version::Edition {
206 version: self.version,
207 prev_hash: self.prev_hash,
208 self_hash: self.self_hash,
209 created_at: self.created_at,
210 tiebreak_id: self.inner_id,
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 const VSK_GRANT: &str = "3";
220
221 fn eid() -> [u8; 32] {
222 [0x42; 32]
223 }
224
225 #[test]
226 fn round_trips_authorship_version_and_chain_hash() {
227 let actor = Keys::generate();
228 let prev = version::edition_hash(&eid(), 1, None, b"{}");
229 let inner = build_edition_inner(actor.public_key(), VSK_GRANT, &eid(), 2, Some(&prev), "{\"role_ids\":[]}", 1_700_000_000, None)
230 .sign_with_keys(&actor)
231 .unwrap();
232
233 let parsed = parse_edition_inner(&inner).expect("valid edition parses");
234 assert_eq!(parsed.author, actor.public_key(), "authorship = the real signer");
235 assert_eq!(parsed.vsk, VSK_GRANT);
236 assert_eq!(parsed.entity_id, eid());
237 assert_eq!(parsed.version, 2);
238 assert_eq!(parsed.prev_hash, Some(prev));
239 assert_eq!(parsed.created_at, 1_700_000_000);
240 assert_eq!(
242 parsed.self_hash,
243 version::edition_hash(&eid(), 2, Some(&prev), b"{\"role_ids\":[]}")
244 );
245 let fe = parsed.to_fold_edition();
247 assert_eq!(fe.version, 2);
248 assert_eq!(fe.prev_hash, Some(prev));
249 }
250
251 #[test]
252 fn authority_citation_round_trips_on_an_edition() {
253 let actor = Keys::generate();
256 let cite = AuthorityCitation { entity_id: [0xab; 32], version: 7, edition_hash: [0xcd; 32] };
257 let inner = build_edition_inner(actor.public_key(), VSK_GRANT, &eid(), 1, None, "{}", 100, Some(&cite))
258 .sign_with_keys(&actor)
259 .unwrap();
260 let parsed = parse_edition_inner(&inner).unwrap();
261 assert_eq!(parsed.authority.as_ref(), Some(&cite), "citation round-trips");
262 assert_eq!(parsed.self_hash, version::edition_hash(&eid(), 1, None, b"{}"));
264
265 let owner = build_edition_inner(actor.public_key(), VSK_GRANT, &eid(), 1, None, "{}", 100, None)
267 .sign_with_keys(&actor)
268 .unwrap();
269 assert_eq!(parse_edition_inner(&owner).unwrap().authority, None);
270 }
271
272 #[test]
273 fn authority_citation_tag_layout_is_frozen() {
274 let cite = AuthorityCitation { entity_id: [0x11; 32], version: 9, edition_hash: [0x22; 32] };
278 let tag = cite.to_tag();
279 let s = tag.as_slice();
280 assert_eq!(s.len(), 4, "vac is a 4-element tag");
281 assert_eq!(s[0], TAG_AUTHORITY_CITATION);
282 assert_eq!(s[1], "11".repeat(32), "entity id is lowercase hex");
283 assert_eq!(s[2], "9", "version is the decimal string");
284 assert_eq!(s[3], "22".repeat(32), "edition hash is lowercase hex");
285 }
286
287 #[test]
288 fn genesis_edition_has_no_prev() {
289 let actor = Keys::generate();
290 let inner = build_edition_inner(actor.public_key(), "1", &eid(), 1, None, "{}", 100, None)
291 .sign_with_keys(&actor)
292 .unwrap();
293 let parsed = parse_edition_inner(&inner).unwrap();
294 assert_eq!(parsed.prev_hash, None, "first edition cites no predecessor");
295 assert_eq!(parsed.version, 1);
296 }
297
298 #[test]
299 fn tampered_content_fails_verification() {
300 let actor = Keys::generate();
302 let inner = build_edition_inner(actor.public_key(), "3", &eid(), 1, None, "{\"a\":1}", 100, None)
303 .sign_with_keys(&actor)
304 .unwrap();
305 let mut json: serde_json::Value = serde_json::from_str(&inner.as_json()).unwrap();
306 json["content"] = serde_json::Value::String("{\"a\":2}".into()); let tampered: Event = serde_json::from_value(json).unwrap();
308 assert!(matches!(parse_edition_inner(&tampered), Err(EditionError::BadSignature)));
309 }
310
311 #[test]
312 fn missing_required_field_is_rejected_not_panicked() {
313 let actor = Keys::generate();
315 let inner = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), "{}")
316 .tags([Tag::custom(TagKind::Custom("vsk".into()), ["3".to_string()])])
317 .sign_with_keys(&actor)
318 .unwrap();
319 assert!(matches!(parse_edition_inner(&inner), Err(EditionError::MissingField("eid"))));
320 }
321
322 #[test]
323 fn duplicate_authority_tag_is_rejected() {
324 let actor = Keys::generate();
328 let hash = crate::simd::hex::bytes_to_hex_32(&[0xAB; 32]);
329 let base = || -> Vec<Tag> {
330 vec![
331 Tag::custom(TagKind::Custom("vsk".into()), ["1".to_string()]),
332 Tag::custom(TagKind::Custom("eid".into()), [crate::simd::hex::bytes_to_hex_32(&eid())]),
333 Tag::custom(TagKind::Custom("ev".into()), ["1".to_string()]),
334 Tag::custom(TagKind::Custom("ep".into()), [hash.clone()]),
335 Tag::custom(TagKind::Custom("vac".into()), [crate::simd::hex::bytes_to_hex_32(&eid()), "1".to_string(), hash.clone()]),
336 ]
337 };
338 let build = |tags: Vec<Tag>| EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_CONTROL), "{}")
339 .tags(tags).sign_with_keys(&actor).unwrap();
340 assert!(parse_edition_inner(&build(base())).is_ok(), "a clean 5-tag base edition parses");
341 for name in ["vsk", "eid", "ev", "ep", "vac"] {
342 let mut tags = base();
343 let dup = tags.iter().find(|t| t.as_slice().first().map(|s| s == name).unwrap_or(false)).cloned().unwrap();
344 tags.push(dup);
345 assert!(
346 matches!(parse_edition_inner(&build(tags)), Err(EditionError::BadField("duplicate authority tag"))),
347 "a duplicate `{name}` tag must be rejected"
348 );
349 }
350 }
351}