1use nostr_sdk::prelude::{FinalizeEvent, FinalizeUnsignedEvent};
23use nostr_sdk::prelude::{Event, Keys, PublicKey, Tag, Timestamp, UnsignedEvent};
24use serde::{Deserialize, Serialize};
25
26use super::super::edition::{AuthorityCitation, EditionError, ParsedEdition, TAG_AUTHORITY_CITATION};
27use super::super::{version, ChannelId, CommunityId, Epoch};
28use super::derive::{control_group_key, verify_community_id, GroupKey};
29use super::stream::{self, OpenedStream, SealForm, StreamError};
30use super::{kind, vsk};
31
32const TAG_SUBKIND: &str = "vsk";
33const TAG_ENTITY: &str = "eid";
34const TAG_EVERSION: &str = "ev";
35const TAG_EPREV: &str = "ep";
36
37pub const MAX_NAME_BYTES: usize = 64;
39pub const MAX_DESCRIPTION_BYTES: usize = 10_000;
41
42#[derive(Debug)]
44pub enum ControlError {
45 Stream(StreamError),
46 Edition(EditionError),
47 NotAnEdition(u16),
49 NotPlaintextSealed,
53 OverCap(&'static str),
55}
56
57impl std::fmt::Display for ControlError {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 ControlError::Stream(e) => write!(f, "stream: {e}"),
61 ControlError::Edition(e) => write!(f, "edition: {e:?}"),
62 ControlError::NotAnEdition(k) => write!(f, "rumor kind {k} is not a control edition"),
63 ControlError::NotPlaintextSealed => write!(f, "control edition must ride a plaintext seal"),
64 ControlError::OverCap(what) => write!(f, "{what} exceeds its byte cap"),
65 }
66 }
67}
68
69impl std::error::Error for ControlError {}
70
71impl From<StreamError> for ControlError {
72 fn from(e: StreamError) -> Self {
73 ControlError::Stream(e)
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct CommunityIdentity {
83 pub community_id: CommunityId,
84 pub owner_xonly: [u8; 32],
85 pub owner_salt: [u8; 32],
86}
87
88impl CommunityIdentity {
89 pub fn mint(owner: &PublicKey) -> CommunityIdentity {
91 let owner_xonly = owner.to_bytes();
92 let owner_salt = super::super::random_32();
93 CommunityIdentity {
94 community_id: super::derive::community_id_of(&owner_xonly, &owner_salt),
95 owner_xonly,
96 owner_salt,
97 }
98 }
99
100 pub fn verify(&self) -> bool {
102 verify_community_id(&self.community_id, &self.owner_xonly, &self.owner_salt)
103 }
104
105 pub fn owner(&self) -> Result<PublicKey, String> {
107 PublicKey::from_slice(&self.owner_xonly).map_err(|e| e.to_string())
108 }
109}
110
111#[allow(clippy::too_many_arguments)]
116pub fn build_edition_rumor(
117 author: PublicKey,
118 vsk: &str,
119 entity_id: &[u8; 32],
120 version: u64,
121 prev_hash: Option<&[u8; 32]>,
122 content: &str,
123 created_at_secs: u64,
124 authority: Option<&AuthorityCitation>,
125) -> UnsignedEvent {
126 let mut tags = vec![
127 Tag::custom(TAG_SUBKIND, [vsk.to_string()]),
128 Tag::custom(TAG_ENTITY, [crate::simd::hex::bytes_to_hex_32(entity_id)]),
129 Tag::custom(TAG_EVERSION, [version.to_string()]),
130 ];
131 if let Some(p) = prev_hash {
132 tags.push(Tag::custom(TAG_EPREV, [crate::simd::hex::bytes_to_hex_32(p)]));
133 }
134 if let Some(a) = authority {
135 tags.push(a.to_tag());
136 }
137 stream::build_rumor_secs(kind::CONTROL, author, content, tags, created_at_secs)
138}
139
140pub fn parse_edition_rumor(rumor: &UnsignedEvent) -> Result<ParsedEdition, ControlError> {
145 if rumor.kind.as_u16() != kind::CONTROL {
146 return Err(ControlError::NotAnEdition(rumor.kind.as_u16()));
147 }
148 for name in [TAG_SUBKIND, TAG_ENTITY, TAG_EVERSION, TAG_EPREV, TAG_AUTHORITY_CITATION] {
151 let count = rumor
152 .tags
153 .iter()
154 .filter(|t| t.as_slice().first().map(|s| s.as_str() == name).unwrap_or(false))
155 .count();
156 if count > 1 {
157 return Err(ControlError::Edition(EditionError::BadField("duplicate authority tag")));
158 }
159 }
160 let get = |name: &str| -> Option<String> {
161 rumor.tags.iter().find_map(|t| {
162 let s = t.as_slice();
163 (s.len() >= 2 && s[0] == name).then(|| s[1].clone())
164 })
165 };
166 let decode_hash = |hex: &str, field: &'static str| -> Result<[u8; 32], ControlError> {
167 if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
168 return Err(ControlError::Edition(EditionError::BadField(field)));
169 }
170 Ok(crate::simd::hex::hex_to_bytes_32(hex))
171 };
172 let vsk = get(TAG_SUBKIND).ok_or(ControlError::Edition(EditionError::MissingField("vsk")))?;
173 let entity_id = decode_hash(&get(TAG_ENTITY).ok_or(ControlError::Edition(EditionError::MissingField("eid")))?, "eid")?;
174 let ev_raw = get(TAG_EVERSION).ok_or(ControlError::Edition(EditionError::MissingField("ev")))?;
179 if !crate::community::edition::is_tag_decimal(&ev_raw) {
180 return Err(ControlError::Edition(EditionError::BadField("ev")));
181 }
182 let version: u64 = ev_raw.parse().map_err(|_| ControlError::Edition(EditionError::BadField("ev")))?;
183 let prev_hash = match get(TAG_EPREV) {
184 Some(h) => Some(decode_hash(&h, "ep")?),
185 None => None,
186 };
187 let self_hash = version::edition_hash(&entity_id, version, prev_hash.as_ref(), rumor.content.as_bytes());
188 Ok(ParsedEdition {
189 author: rumor.pubkey,
190 vsk,
191 entity_id,
192 version,
193 prev_hash,
194 content: rumor.content.clone(),
195 self_hash,
196 created_at: rumor.created_at.as_secs(),
197 inner_id: rumor.id.expect("verified rumors carry their id").to_bytes(),
198 authority: AuthorityCitation::from_tags(&rumor.tags),
199 })
200}
201
202pub fn seal_control_edition(
208 rumor: &UnsignedEvent,
209 group: &GroupKey,
210 author_keys: &Keys,
211 wrap_at: Timestamp,
212) -> Result<(Event, Keys), ControlError> {
213 let seal = stream::build_seal(rumor, SealForm::Plaintext, group, author_keys)?;
214 Ok(stream::wrap_seal(&seal, group, stream::KIND_WRAP, wrap_at)?)
215}
216
217pub async fn seal_control_edition_signed<S: crate::signer::VectorSigner + ?Sized>(
221 signer: &S,
222 author: PublicKey,
223 rumor: &UnsignedEvent,
224 group: &GroupKey,
225 wrap_at: Timestamp,
226) -> Result<(Event, Keys), ControlError> {
227 Ok(stream::seal_and_wrap_signed(signer, author, rumor, SealForm::Plaintext, group, stream::KIND_WRAP, wrap_at, &[]).await?)
228}
229
230pub fn open_control_edition(wrap: &Event, group: &GroupKey) -> Result<(ParsedEdition, OpenedStream), ControlError> {
233 let opened = stream::open_wrap(wrap, group)?;
234 if opened.seal_form != SealForm::Plaintext {
235 return Err(ControlError::NotPlaintextSealed);
236 }
237 let edition = parse_edition_rumor(&opened.rumor)?;
238 Ok((edition, opened))
239}
240
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246pub struct ImageRef {
247 pub url: String,
248 pub key: String,
249 pub nonce: String,
250 pub hash: String,
251 #[serde(flatten)]
254 pub extra: serde_json::Map<String, serde_json::Value>,
255}
256
257impl ImageRef {
258 pub fn to_community_image(&self) -> crate::community::CommunityImage {
262 let ext = self
263 .extra
264 .get("ext")
265 .and_then(|v| v.as_str())
266 .filter(|s| !s.is_empty())
267 .map(str::to_string)
268 .or_else(|| {
269 std::path::Path::new(&self.url)
270 .extension()
271 .and_then(|e| e.to_str())
272 .map(|s| s.to_lowercase())
273 })
274 .unwrap_or_else(|| "png".to_string());
275 crate::community::CommunityImage {
276 url: self.url.clone(),
277 key: self.key.clone(),
278 nonce: self.nonce.clone(),
279 hash: self.hash.clone(),
280 ext,
281 }
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
288pub struct CommunityMetadata {
289 pub name: String,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub description: Option<String>,
292 #[serde(default, skip_serializing_if = "Vec::is_empty")]
293 pub relays: Vec<String>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub icon: Option<ImageRef>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub banner: Option<ImageRef>,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub custom: Option<serde_json::Map<String, serde_json::Value>>,
301 #[serde(flatten)]
303 pub extra: serde_json::Map<String, serde_json::Value>,
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
310pub struct ChannelMetadata {
311 pub name: String,
312 pub private: bool,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub voice: Option<bool>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub deleted: Option<bool>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub custom: Option<serde_json::Map<String, serde_json::Value>>,
319 #[serde(flatten)]
320 pub extra: serde_json::Map<String, serde_json::Value>,
321}
322
323pub fn validate_community_metadata(meta: &CommunityMetadata) -> Result<(), ControlError> {
326 if meta.name.len() > MAX_NAME_BYTES {
327 return Err(ControlError::OverCap("name"));
328 }
329 if meta.description.as_ref().is_some_and(|d| d.len() > MAX_DESCRIPTION_BYTES) {
330 return Err(ControlError::OverCap("description"));
331 }
332 Ok(())
333}
334
335pub fn validate_channel_metadata(meta: &ChannelMetadata) -> Result<(), ControlError> {
336 if meta.name.len() > MAX_NAME_BYTES {
337 return Err(ControlError::OverCap("name"));
338 }
339 Ok(())
340}
341
342pub struct Genesis {
348 pub identity: CommunityIdentity,
349 pub community_root: [u8; 32],
351 pub general_channel_id: ChannelId,
352 pub wraps: [Event; 2],
354}
355
356pub fn genesis(owner_keys: &Keys, mut metadata: CommunityMetadata, at_secs: u64) -> Result<Genesis, ControlError> {
360 validate_community_metadata(&metadata)?;
361 metadata.relays.truncate(super::super::MAX_COMMUNITY_RELAYS);
363
364 let identity = CommunityIdentity::mint(&owner_keys.public_key());
365 let community_root = super::super::random_32();
366 let general_channel_id = ChannelId(super::super::random_32());
367 let group = control_group_key(&community_root, &identity.community_id, Epoch(0));
368
369 let meta_json = serde_json::to_string(&metadata).map_err(|e| ControlError::Stream(StreamError::Parse(e.to_string())))?;
370 let meta_rumor = build_edition_rumor(
371 owner_keys.public_key(),
372 vsk::COMMUNITY_METADATA,
373 &identity.community_id.0,
374 1,
375 None,
376 &meta_json,
377 at_secs,
378 None,
379 );
380
381 let general = ChannelMetadata { name: "general".into(), private: false, ..Default::default() };
382 let general_json = serde_json::to_string(&general).map_err(|e| ControlError::Stream(StreamError::Parse(e.to_string())))?;
383 let general_rumor = build_edition_rumor(
384 owner_keys.public_key(),
385 vsk::CHANNEL_METADATA,
386 &general_channel_id.0,
387 1,
388 None,
389 &general_json,
390 at_secs,
391 None,
392 );
393
394 let (meta_wrap, _) = seal_control_edition(&meta_rumor, &group, owner_keys, Timestamp::from_secs(at_secs))?;
395 let (general_wrap, _) = seal_control_edition(&general_rumor, &group, owner_keys, Timestamp::from_secs(at_secs))?;
396
397 Ok(Genesis {
398 identity,
399 community_root,
400 general_channel_id,
401 wraps: [meta_wrap, general_wrap],
402 })
403}
404
405pub async fn genesis_signed<S: crate::signer::VectorSigner + ?Sized>(
409 owner_pk: PublicKey,
410 signer: &S,
411 metadata: CommunityMetadata,
412 at_secs: u64,
413) -> Result<Genesis, ControlError> {
414 genesis_signed_with_primary(owner_pk, signer, metadata, at_secs, None).await
415}
416
417pub async fn genesis_signed_with_primary<S: crate::signer::VectorSigner + ?Sized>(
422 owner_pk: PublicKey,
423 signer: &S,
424 mut metadata: CommunityMetadata,
425 at_secs: u64,
426 primary: Option<(ChannelId, String)>,
427) -> Result<Genesis, ControlError> {
428 validate_community_metadata(&metadata)?;
429 metadata.relays.truncate(super::super::MAX_COMMUNITY_RELAYS);
430
431 let identity = CommunityIdentity::mint(&owner_pk);
432 let community_root = super::super::random_32();
433 let (general_channel_id, general_name) = match primary {
434 Some((id, name)) => (id, name),
435 None => (ChannelId(super::super::random_32()), "general".to_string()),
436 };
437 let group = control_group_key(&community_root, &identity.community_id, Epoch(0));
438
439 let meta_json = serde_json::to_string(&metadata).map_err(|e| ControlError::Stream(StreamError::Parse(e.to_string())))?;
440 let meta_rumor = build_edition_rumor(owner_pk, vsk::COMMUNITY_METADATA, &identity.community_id.0, 1, None, &meta_json, at_secs, None);
441
442 let general = ChannelMetadata { name: general_name, private: false, ..Default::default() };
443 let general_json = serde_json::to_string(&general).map_err(|e| ControlError::Stream(StreamError::Parse(e.to_string())))?;
444 let general_rumor = build_edition_rumor(owner_pk, vsk::CHANNEL_METADATA, &general_channel_id.0, 1, None, &general_json, at_secs, None);
445
446 let (meta_wrap, _) = seal_control_edition_signed(signer, owner_pk, &meta_rumor, &group, Timestamp::from_secs(at_secs)).await?;
447 let (general_wrap, _) = seal_control_edition_signed(signer, owner_pk, &general_rumor, &group, Timestamp::from_secs(at_secs)).await?;
448
449 Ok(Genesis {
450 identity,
451 community_root,
452 general_channel_id,
453 wraps: [meta_wrap, general_wrap],
454 })
455}
456
457#[cfg(test)]
458mod tests {
459 use super::super::super::edition::build_edition_inner;
460 use super::*;
461
462 fn cid() -> CommunityId {
463 CommunityId([0x33; 32])
464 }
465
466 fn group_at(epoch: u64) -> GroupKey {
467 control_group_key(&[0x44; 32], &cid(), Epoch(epoch))
468 }
469
470 fn simple_edition(author: &Keys, version: u64, prev: Option<&[u8; 32]>) -> UnsignedEvent {
471 build_edition_rumor(
472 author.public_key(),
473 vsk::GRANT,
474 &[0x55; 32],
475 version,
476 prev,
477 "{\"member\":\"aa\",\"role_ids\":[]}",
478 1_700_000_000,
479 None,
480 )
481 }
482
483 #[test]
484 fn edition_round_trips_through_the_control_plane() {
485 let owner = Keys::generate();
486 let group = group_at(0);
487 let cite = AuthorityCitation { entity_id: [0xab; 32], version: 7, edition_hash: [0xcd; 32] };
488 let rumor = build_edition_rumor(
489 owner.public_key(),
490 vsk::GRANT,
491 &[0x55; 32],
492 2,
493 Some(&[0x66; 32]),
494 "{\"member\":\"aa\",\"role_ids\":[]}",
495 1_700_000_000,
496 Some(&cite),
497 );
498 let (wrap, _) = seal_control_edition(&rumor, &group, &owner, Timestamp::from_secs(1_700_000_001)).unwrap();
499
500 let (edition, opened) = open_control_edition(&wrap, &group).unwrap();
501 assert_eq!(edition.author, owner.public_key());
502 assert_eq!(edition.vsk, vsk::GRANT);
503 assert_eq!(edition.entity_id, [0x55; 32]);
504 assert_eq!(edition.version, 2);
505 assert_eq!(edition.prev_hash, Some([0x66; 32]));
506 assert_eq!(edition.authority.as_ref(), Some(&cite));
507 assert_eq!(opened.seal_form, SealForm::Plaintext);
508 assert_eq!(
510 edition.self_hash,
511 version::edition_hash(&[0x55; 32], 2, Some(&[0x66; 32]), rumor.content.as_bytes())
512 );
513 }
514
515 #[test]
516 fn v2_edition_tags_carry_no_protocol_version_tag() {
517 let owner = Keys::generate();
520 let rumor = simple_edition(&owner, 1, None);
521 assert!(
522 !rumor.tags.iter().any(|t| t.as_slice().first().map(|s| s == "v").unwrap_or(false)),
523 "v2 editions must not carry a protocol version tag"
524 );
525 assert!(!rumor.tags.iter().any(|t| t.as_slice().first().map(|s| s == "ms").unwrap_or(false)));
527 }
528
529 #[test]
530 fn edition_hash_is_identical_across_protocols() {
531 let author = Keys::generate();
536 let entity = [0x55; 32];
537 let content = "{\"member\":\"aa\",\"role_ids\":[]}";
538 let v1_inner = build_edition_inner(author.public_key(), "3", &entity, 2, Some(&[0x66; 32]), content, 100, None)
539 .finalize(&author)
540 .unwrap();
541 let v1_parsed = super::super::super::edition::parse_edition_inner(&v1_inner).unwrap();
542
543 let v2_rumor = build_edition_rumor(author.public_key(), "3", &entity, 2, Some(&[0x66; 32]), content, 100, None);
544 let v2_parsed = parse_edition_rumor(&v2_rumor).unwrap();
545
546 assert_eq!(v1_parsed.self_hash, v2_parsed.self_hash);
547 }
548
549 #[test]
550 fn encrypted_seal_control_edition_is_rejected() {
551 let owner = Keys::generate();
552 let group = group_at(0);
553 let rumor = simple_edition(&owner, 1, None);
554 let seal = stream::build_seal(&rumor, SealForm::Encrypted, &group, &owner).unwrap();
555 let (wrap, _) = stream::wrap_seal(&seal, &group, stream::KIND_WRAP, Timestamp::from_secs(1)).unwrap();
556 assert!(matches!(open_control_edition(&wrap, &group), Err(ControlError::NotPlaintextSealed)));
557 }
558
559 #[test]
560 fn non_edition_rumor_is_rejected() {
561 let owner = Keys::generate();
562 let group = group_at(0);
563 let rumor = stream::build_rumor_secs(super::kind::MESSAGE, owner.public_key(), "hi", vec![], 100);
564 let seal = stream::build_seal(&rumor, SealForm::Plaintext, &group, &owner).unwrap();
565 let (wrap, _) = stream::wrap_seal(&seal, &group, stream::KIND_WRAP, Timestamp::from_secs(1)).unwrap();
566 assert!(matches!(open_control_edition(&wrap, &group), Err(ControlError::NotAnEdition(k)) if k == super::kind::MESSAGE));
567 }
568
569 #[test]
570 fn duplicate_machinery_tags_are_rejected() {
571 let owner = Keys::generate();
572 let mut rumor = simple_edition(&owner, 1, None);
573 let dup = Tag::custom("eid", [crate::simd::hex::bytes_to_hex_32(&[0x55; 32])]);
574 let mut tags: Vec<Tag> = rumor.tags.iter().cloned().collect();
575 tags.push(dup);
576 rumor = stream::build_rumor_secs(kind::CONTROL, owner.public_key(), &rumor.content, tags, 100);
577 assert!(matches!(
578 parse_edition_rumor(&rumor),
579 Err(ControlError::Edition(EditionError::BadField("duplicate authority tag")))
580 ));
581 }
582
583 #[test]
584 fn a_non_canonical_ev_version_is_rejected() {
585 let owner = Keys::generate();
586 for bad in ["007", "+5", "01"] {
589 let tags = vec![
590 Tag::custom("vsk", [vsk::GRANT]),
591 Tag::custom("eid", [crate::simd::hex::bytes_to_hex_32(&[0x55; 32])]),
592 Tag::custom("ev", [bad]),
593 ];
594 let rumor = stream::build_rumor_secs(kind::CONTROL, owner.public_key(), "{\"member\":\"aa\",\"role_ids\":[]}", tags, 100);
595 assert!(
596 matches!(parse_edition_rumor(&rumor), Err(ControlError::Edition(EditionError::BadField("ev")))),
597 "a non-canonical ev {bad:?} is rejected"
598 );
599 }
600 }
601
602 #[test]
603 fn compaction_rewrap_preserves_the_edition_chain_identity() {
604 let owner = Keys::generate();
608 let e0 = group_at(0);
609 let rumor = simple_edition(&owner, 3, Some(&[0x77; 32]));
610 let (wrap, _) = seal_control_edition(&rumor, &e0, &owner, Timestamp::from_secs(10)).unwrap();
611 let (edition, opened) = open_control_edition(&wrap, &e0).unwrap();
612
613 let e1 = group_at(1);
614 let (rewrapped, _) = stream::rewrap_seal(&opened.seal, &e1, Timestamp::from_secs(20)).unwrap();
615 let (re_edition, _) = open_control_edition(&rewrapped, &e1).unwrap();
616
617 assert_eq!(re_edition.self_hash, edition.self_hash);
618 assert_eq!(re_edition.author, edition.author);
619 assert_eq!(re_edition.inner_id, edition.inner_id, "rumor id survives compaction");
620 }
621
622 #[test]
623 fn editions_opened_from_wraps_fold_to_the_head() {
624 let owner = Keys::generate();
625 let group = group_at(0);
626 let entity = [0x55; 32];
627 let content = |v: u64| format!("{{\"v\":{v}}}");
628
629 let mut prev: Option<[u8; 32]> = None;
631 let mut parsed = Vec::new();
632 for v in 1..=3u64 {
633 let rumor = build_edition_rumor(owner.public_key(), vsk::GRANT, &entity, v, prev.as_ref(), &content(v), 100 + v, None);
634 let (wrap, _) = seal_control_edition(&rumor, &group, &owner, Timestamp::from_secs(100 + v)).unwrap();
635 let (edition, _) = open_control_edition(&wrap, &group).unwrap();
636 prev = Some(edition.self_hash);
637 parsed.push(edition);
638 }
639
640 let fold_editions: Vec<version::Edition> = parsed.iter().map(|p| p.to_fold_edition()).collect();
641 let folded = version::fold(&fold_editions, 0, None);
642 assert_eq!(fold_editions[folded.head.expect("chain folds")].version, 3);
643 assert!(!folded.gap);
644
645 let partial = [fold_editions[0].clone(), fold_editions[2].clone()];
647 let gapped = version::fold(&partial, 0, None);
648 assert_eq!(partial[gapped.head.expect("genesis edition anchors")].version, 1);
649 assert!(gapped.gap, "a missing middle version is a gap, not a silent skip");
650 }
651
652 #[test]
653 fn genesis_mints_a_verifiable_two_edition_community() {
654 let owner = Keys::generate();
655 let meta = CommunityMetadata {
656 name: "Vector".into(),
657 description: Some("Private messaging, no compromises.".into()),
658 relays: vec!["wss://jskitty.com/nostr".into()],
659 ..Default::default()
660 };
661 let g = genesis(&owner, meta, 1_700_000_000).unwrap();
662
663 assert!(g.identity.verify());
665 assert_eq!(g.identity.owner().unwrap(), owner.public_key());
666
667 let group = control_group_key(&g.community_root, &g.identity.community_id, Epoch(0));
669 let (meta_ed, _) = open_control_edition(&g.wraps[0], &group).unwrap();
670 let (chan_ed, _) = open_control_edition(&g.wraps[1], &group).unwrap();
671 assert_eq!(meta_ed.author, owner.public_key());
672 assert_eq!(chan_ed.author, owner.public_key());
673 assert_eq!(meta_ed.vsk, vsk::COMMUNITY_METADATA);
674 assert_eq!(chan_ed.vsk, vsk::CHANNEL_METADATA);
675 assert_eq!(meta_ed.entity_id, g.identity.community_id.0);
677 assert_eq!(chan_ed.entity_id, g.general_channel_id.0);
678 for e in [&meta_ed, &chan_ed] {
680 assert_eq!(e.version, 1);
681 assert_eq!(e.prev_hash, None);
682 assert_eq!(e.authority, None);
683 }
684 let general: ChannelMetadata = serde_json::from_str(&chan_ed.content).unwrap();
685 assert_eq!(general.name, "general");
686 assert!(!general.private);
687 }
688
689 #[test]
690 fn a_forged_identity_fails_the_commitment() {
691 let owner = Keys::generate();
692 let attacker = Keys::generate();
693 let real = CommunityIdentity::mint(&owner.public_key());
694 let forged = CommunityIdentity {
697 community_id: real.community_id,
698 owner_xonly: attacker.public_key().to_bytes(),
699 owner_salt: real.owner_salt,
700 };
701 assert!(!forged.verify());
702 }
703
704 #[test]
705 fn metadata_caps_and_unknown_field_round_trip() {
706 let over_name = CommunityMetadata { name: "x".repeat(MAX_NAME_BYTES + 1), ..Default::default() };
707 assert!(matches!(validate_community_metadata(&over_name), Err(ControlError::OverCap("name"))));
708 let over_desc = CommunityMetadata {
709 name: "ok".into(),
710 description: Some("d".repeat(MAX_DESCRIPTION_BYTES + 1)),
711 ..Default::default()
712 };
713 assert!(matches!(validate_community_metadata(&over_desc), Err(ControlError::OverCap("description"))));
714 let multibyte = ChannelMetadata { name: "€".repeat(22), private: false, ..Default::default() };
716 assert!(matches!(validate_channel_metadata(&multibyte), Err(ControlError::OverCap("name"))));
717
718 let wire = r#"{"name":"Vector","relays":["wss://a"],"icon":{"url":"u","key":"k","nonce":"n","hash":"h","ext":"png"},"custom":{"rules":"Be excellent."},"future_field":{"deep":[1,2]}}"#;
721 let parsed: CommunityMetadata = serde_json::from_str(wire).unwrap();
722 let out = serde_json::to_string(&parsed).unwrap();
723 let reparsed: serde_json::Value = serde_json::from_str(&out).unwrap();
724 assert_eq!(reparsed["future_field"]["deep"][1], 2);
725 assert_eq!(reparsed["icon"]["ext"], "png");
726 assert_eq!(reparsed["custom"]["rules"], "Be excellent.");
727 }
728}