chore: restructure concord crate (#50)

Reviewed-on: #50
This commit was merged in pull request #50.
This commit is contained in:
2026-09-17 06:16:01 +00:00
parent fdabfcf026
commit 874f103752
18 changed files with 446 additions and 378 deletions
@@ -6,13 +6,13 @@ use anyhow::Result;
use data_encoding::HEXLOWER;
use nostr_sdk::prelude::*;
use crate::edition::{
AuthorityCitation, TAG_CITATION, canonical_decimal, citation_from, citation_tag,
};
use crate::stream::{
use crate::cord01::{
KIND_WRAP, OpenedStream, SealForm, StreamError, build_rumor_ms, build_seal, open_wrap,
wrap_seal,
};
use crate::cord04::{
AuthorityCitation, TAG_CITATION, canonical_decimal, citation_from, citation_tag,
};
use crate::{GroupKey, decode_hex_32};
pub const KIND_JOIN_LEAVE: u16 = 3306;
@@ -532,8 +532,8 @@ fn pubkey(hex: &str, name: &'static str) -> Result<PublicKey, GuestbookError> {
#[cfg(test)]
mod tests {
use super::*;
use crate::cord01::build_rumor_secs;
use crate::derive::guestbook_group_key;
use crate::stream::build_rumor_secs;
use crate::{CommunityId, Epoch};
const ROOT: [u8; 32] = [0x5au8; 32];
@@ -5,8 +5,8 @@ use std::fmt;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use crate::invite::{ChannelGrant, CommunityInvite};
use crate::stream::{self, NIP44_MAX_PLAINTEXT};
use crate::cord01::{self, NIP44_MAX_PLAINTEXT};
use crate::cord05::{ChannelGrant, CommunityInvite};
use crate::{CommunityId, Epoch, Extra};
pub const KIND_COMMUNITY_LIST: u16 = 13302;
@@ -42,8 +42,8 @@ impl fmt::Display for ListError {
impl std::error::Error for ListError {}
impl From<stream::StreamError> for ListError {
fn from(error: stream::StreamError) -> Self {
impl From<cord01::StreamError> for ListError {
fn from(error: cord01::StreamError) -> Self {
ListError::Crypto(error.to_string())
}
}
@@ -187,7 +187,7 @@ pub fn build_list_event(keys: &Keys, list: &CommunityList) -> Result<Event, List
list.fits()?;
let json = serde_json::to_string(list).map_err(json_error)?;
let content = stream::seal_to_self(keys, json.as_bytes())?;
let content = cord01::seal_to_self(keys, json.as_bytes())?;
EventBuilder::new(Kind::Custom(KIND_COMMUNITY_LIST), content)
.finalize(keys)
@@ -199,7 +199,7 @@ pub fn parse_list_event(keys: &Keys, event: &Event) -> Result<CommunityList, Lis
return Err(ListError::Kind(event.kind.as_u16()));
}
let json = stream::open_to_self(keys, &event.content)?;
let json = cord01::open_to_self(keys, &event.content)?;
serde_json::from_slice(&json).map_err(json_error)
}
@@ -1,22 +1,25 @@
pub mod guestbook;
pub mod list;
use std::collections::{BTreeMap, BTreeSet};
use anyhow::{Result, bail};
use nostr_sdk::prelude::{Event, Keys, PublicKey, Timestamp, UnsignedEvent};
use serde::{Deserialize, Serialize};
use crate::cord01::{KIND_WRAP, SealForm, build_seal, open_wrap_at, wrap_seal_with};
use crate::cord04::roles::{
AuthorityEdition, CommunityRoles, Grant, MAX_BANLIST, Permissions, Role, Roster, citation_ok,
fold_roster,
};
use crate::cord04::{
AuthorityCitation, EditionFields, EditionMeta, EntityHead, Floors, ParsedEdition,
build_edition, fold_head, parse_edition, vsk,
};
use crate::derive::{
banlist_locator, community_id_of, control_group_key, control_signer_group_key, grant_locator,
invite_links_locator, pins_locator, verify_community_id,
};
use crate::edition::{
AuthorityCitation, EditionFields, EditionMeta, EntityHead, Floors, ParsedEdition,
build_edition, fold_head, parse_edition, vsk,
};
use crate::roles::{
AuthorityEdition, CommunityRoles, Grant, MAX_BANLIST, Permissions, Role, Roster, citation_ok,
fold_roster,
};
use crate::stream::{KIND_WRAP, SealForm, build_seal, open_wrap_at, wrap_seal_with};
use crate::{ChannelId, CommunityId, Epoch, Extra, GroupKey, random_32};
pub const MAX_NAME_BYTES: usize = 64;
@@ -731,12 +734,13 @@ mod tests {
use nostr_memory::MemoryDatabase;
use super::*;
use crate::chat::{self, build_message, seal_rumor};
use crate::cord03::{self, build_message, seal_rumor};
use crate::cord04::fold;
use crate::cord04::pins;
use crate::cord04::roles::{Grant, MAX_BANLIST, MAX_ROLES_PER_MEMBER, Role, RoleScope};
use crate::derive::{channel_group_key, grant_locator};
use crate::edition::fold;
use crate::roles::{Grant, MAX_BANLIST, MAX_ROLES_PER_MEMBER, Role, RoleScope};
use crate::store::{CommunityState, load_state, save_state};
use crate::{Extra, RoleId, pins};
use crate::{Extra, RoleId};
const AT: u64 = 1_700_000_000;
@@ -1102,7 +1106,7 @@ mod tests {
None,
);
let (wrap, _) = seal_rumor(&rumor, &group, &author, false).expect("seals");
let opened = chat::open(&wrap, &group, &channel, ROOT_EPOCH)
let opened = cord03::open(&wrap, &group, &channel, ROOT_EPOCH)
.expect("opens")
.0;
let entry = pins::build_entry(&opened, &group, &channel).expect("pins");
@@ -5,15 +5,15 @@ use std::fmt;
use anyhow::Result;
use nostr_sdk::prelude::*;
use crate::derive::channel_group_key;
use crate::edition::{
AuthorityCitation, TAG_CITATION, canonical_decimal, citation_from, citation_tag,
};
use crate::stream::{
use crate::cord01::{
KIND_WRAP, KIND_WRAP_EPHEMERAL, OpenedStream, SealForm, StreamError, build_rumor_ms,
build_seal, channel_binding_tags, check_channel_binding, open_wrap, resolve_ms_strict,
wrap_seal,
};
use crate::cord04::{
AuthorityCitation, TAG_CITATION, canonical_decimal, citation_from, citation_tag,
};
use crate::derive::channel_group_key;
use crate::{ChannelId, Epoch, GroupKey, decode_hex_32};
pub const KIND_MESSAGE: u16 = 9;
@@ -1,3 +1,6 @@
pub mod pins;
pub mod roles;
use std::cmp::Reverse;
use std::collections::BTreeMap;
use std::fmt;
@@ -7,8 +10,8 @@ use nostr_sdk::prelude::{EventId, PublicKey, Tag, UnsignedEvent};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::cord01::build_rumor_secs;
use crate::decode_hex_32;
use crate::stream::build_rumor_secs;
pub const KIND_CONTROL: u16 = 3308;
@@ -9,9 +9,9 @@ use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::chat::{ChatAction, ChatRumor, KIND_COMMENT, KIND_EDIT, KIND_MESSAGE};
use crate::edition::canonical_decimal;
use crate::stream::{self, OpenedStream, SealForm, resolve_ms_strict};
use crate::cord01::{self, OpenedStream, SealForm, resolve_ms_strict};
use crate::cord03::{ChatAction, ChatRumor, KIND_COMMENT, KIND_EDIT, KIND_MESSAGE};
use crate::cord04::canonical_decimal;
use crate::{ChannelId, Epoch, Extra, GroupKey, decode_hex_lower};
pub const PIN_MAX_ENTRIES: usize = 25;
@@ -315,7 +315,7 @@ fn disclosed_keys(opened: &OpenedStream, group: &GroupKey) -> Result<MessageKeys
pub fn verify_entry(entry: &PinEntry, channel: &ChannelId) -> Option<VerifiedPin> {
let seal = &entry.seal;
if seal.kind.as_u16() != stream::KIND_SEAL_ENCRYPTED || seal.verify().is_err() {
if seal.kind.as_u16() != cord01::KIND_SEAL_ENCRYPTED || seal.verify().is_err() {
return None;
}
@@ -376,7 +376,7 @@ fn verify_edit_bundle(
let seal = &bundle.seal;
// Checkable before any crypto: nobody else may revise another member's words.
if seal.kind.as_u16() != stream::KIND_SEAL_ENCRYPTED || seal.pubkey != *original_author {
if seal.kind.as_u16() != cord01::KIND_SEAL_ENCRYPTED || seal.pubkey != *original_author {
return None;
}
@@ -456,7 +456,7 @@ fn serialize_sealed(
}
let inner = encode_form(entries)?;
let sealed = stream::seal_bytes(group.conversation(), inner.as_bytes())
let sealed = cord01::seal_bytes(group.conversation(), inner.as_bytes())
.map_err(|error| PinError::Seal(error.to_string()))?;
let content = serde_json::json!({ "epoch": epoch.to_string(), "sealed": sealed }).to_string();
@@ -526,7 +526,7 @@ pub fn read_list(content: &str, unseal: impl Fn(Epoch) -> Option<GroupKey>) -> R
};
};
let Ok(inner) = stream::open_bytes(group.conversation(), sealed) else {
let Ok(inner) = cord01::open_bytes(group.conversation(), sealed) else {
return EMPTY;
};
@@ -554,7 +554,7 @@ mod tests {
use nostr::nips::nip44::v2::{self, ConversationKey};
use super::*;
use crate::chat::{ChatRumor, build_delete, build_edit, build_message, open, seal_rumor};
use crate::cord03::{ChatRumor, build_delete, build_edit, build_message, open, seal_rumor};
use crate::derive::channel_group_key;
const AT_MS: u64 = 1_700_000_000_000;
@@ -657,7 +657,7 @@ mod tests {
assert!(verify_entry(&forged, &channel()).is_none());
// A rumor carrying a claimed id that is not its own is refused.
let plaintext = stream::open_bytes(&conversation(), &opened.seal.content).expect("opens");
let plaintext = cord01::open_bytes(&conversation(), &opened.seal.content).expect("opens");
let mut value: serde_json::Value = serde_json::from_slice(&plaintext).expect("json");
value["id"] = serde_json::Value::String("00".repeat(32));
@@ -668,7 +668,7 @@ mod tests {
)
.expect("encrypts");
let content = BASE64.encode(&raw);
let seal = EventBuilder::new(Kind::Custom(stream::KIND_SEAL_ENCRYPTED), &content)
let seal = EventBuilder::new(Kind::Custom(cord01::KIND_SEAL_ENCRYPTED), &content)
.custom_created_at(opened.seal.created_at)
.finalize(&author)
.expect("signs");
@@ -4,10 +4,10 @@ use anyhow::{Result, bail};
use nostr_sdk::prelude::PublicKey;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::derive::{banlist_locator, grant_locator};
use crate::edition::{
use crate::cord04::{
AuthorityCitation, EditionMeta, EntityHead, Floors, ParsedEdition, fold_head, vsk,
};
use crate::derive::{banlist_locator, grant_locator};
use crate::{ChannelId, CommunityId, Extra, RoleId, decode_hex_32};
pub const MAX_ROLES_PER_COMMUNITY: usize = 100;
@@ -759,7 +759,7 @@ mod tests {
use nostr_sdk::prelude::Keys;
use super::*;
use crate::edition::{EditionFields, build_edition, parse_edition};
use crate::cord04::{EditionFields, build_edition, parse_edition};
const COMMUNITY: [u8; 32] = [0xc0; 32];
const AT: u64 = 1_700_000_000;
@@ -10,11 +10,11 @@ use nostr::nips::nip59::{GiftWrapBuilder, UnwrappedGift};
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use crate::control::{ImageRef, MAX_RELAYS};
use crate::cord01::{self, NIP44_MAX_PLAINTEXT, StreamError};
use crate::cord02::list::{canonical, union};
use crate::cord02::{ImageRef, MAX_RELAYS};
use crate::cord04::{TAG_SUBKIND, vsk};
use crate::derive::{TOKEN_LEN, verify_community_id};
use crate::edition::{TAG_SUBKIND, vsk};
use crate::list::{canonical, union};
use crate::stream::{self, NIP44_MAX_PLAINTEXT, StreamError};
use crate::{ChannelId, CommunityId, Epoch, Extra, decode_hex_32};
pub const KIND_BUNDLE: u16 = 33301;
@@ -594,7 +594,7 @@ pub fn build_invite_list(keys: &Keys, list: &InviteList) -> Result<Event, Invite
list.fits()?;
let json = serde_json::to_string(list).map_err(json_error)?;
let content = stream::seal_to_self(keys, json.as_bytes())?;
let content = cord01::seal_to_self(keys, json.as_bytes())?;
EventBuilder::new(Kind::Custom(KIND_INVITE_LIST), content)
.finalize(keys)
@@ -606,7 +606,7 @@ pub fn parse_invite_list(keys: &Keys, event: &Event) -> Result<InviteList, Invit
return Err(InviteError::Kind(event.kind.as_u16()));
}
let json = stream::open_to_self(keys, &event.content)?;
let json = cord01::open_to_self(keys, &event.content)?;
serde_json::from_slice(&json).map_err(json_error)
}
@@ -626,14 +626,14 @@ fn merge_entry(held: &InviteEntry, incoming: &InviteEntry) -> InviteEntry {
}
fn seal_bundle(bundle_key: &[u8; 32], json: &str) -> Result<String, InviteError> {
Ok(stream::seal_bytes(
Ok(cord01::seal_bytes(
&ConversationKey::new(*bundle_key),
json.as_bytes(),
)?)
}
fn open_bundle(bundle_key: &[u8; 32], content: &str) -> Result<String, InviteError> {
let plaintext = stream::open_bytes(&ConversationKey::new(*bundle_key), content)?;
let plaintext = cord01::open_bytes(&ConversationKey::new(*bundle_key), content)?;
String::from_utf8(plaintext).map_err(|_| InviteError::BadFragment("bundle is not utf8"))
}
@@ -941,7 +941,7 @@ mod tests {
assert!(unwrap_direct_invite(&wrap, &stranger).is_err());
// ...and a wrap that opens to some other kind is not an invite.
let rumor = EventBuilder::new(Kind::Custom(crate::chat::KIND_MESSAGE), "hello")
let rumor = EventBuilder::new(Kind::Custom(crate::cord03::KIND_MESSAGE), "hello")
.finalize_unsigned(recipient.public_key());
let wrap = GiftWrapBuilder::new(recipient.public_key(), rumor)
.finalize(&recipient)
@@ -7,17 +7,17 @@ use nostr::nips::nip44::v2::ConversationKey;
use nostr_sdk::prelude::{Event, Keys, PublicKey, SecretKey, Tag, Timestamp, UnsignedEvent};
use serde::{Deserialize, Serialize};
use crate::control::CommunityIdentity;
use crate::cord01::{self, KIND_SEAL_PLAINTEXT, OpenedStream, SealForm, StreamError};
use crate::cord02::CommunityIdentity;
use crate::cord04::roles::CommunityRoles;
use crate::cord04::{
AuthorityCitation, KIND_CONTROL, TAG_SUBKIND, canonical_decimal, citation_from, citation_tag,
vsk,
};
use crate::derive::{
base_rekey_group_key, channel_rekey_group_key, control_group_key, control_signer_group_key,
dissolved_group_key, epoch_key_commitment, recipient_locator,
};
use crate::edition::{
AuthorityCitation, KIND_CONTROL, TAG_SUBKIND, canonical_decimal, citation_from, citation_tag,
vsk,
};
use crate::roles::CommunityRoles;
use crate::stream::{self, KIND_SEAL_PLAINTEXT, OpenedStream, SealForm, StreamError};
use crate::{ChannelId, CommunityId, Epoch, GroupKey, random_32};
pub const KIND_REKEY: u16 = 3303;
@@ -325,7 +325,7 @@ pub fn open_blob(
) -> Result<KeyDelivery, RekeyError> {
let conversation =
ConversationKey::derive(recipient.secret_key(), rotator).map_err(crypto_error)?;
let plaintext = stream::open_bytes(&conversation, &blob.wrapped)?;
let plaintext = cord01::open_bytes(&conversation, &blob.wrapped)?;
parse_blob_plaintext(&plaintext, scope, epoch, community_id)
}
@@ -347,7 +347,7 @@ fn seal_to(
plaintext: &[u8],
) -> Result<String, RekeyError> {
let conversation = ConversationKey::derive(secret, recipient).map_err(crypto_error)?;
Ok(stream::seal_bytes(&conversation, plaintext)?)
Ok(cord01::seal_bytes(&conversation, plaintext)?)
}
#[derive(Debug, Clone)]
@@ -539,7 +539,7 @@ pub fn compact(
let mut wraps = Vec::with_capacity(seals.len());
for seal in seals {
wraps.push(stream::rewrap_seal(seal, read, signer, at)?.0);
wraps.push(cord01::rewrap_seal(seal, read, signer, at)?.0);
}
Ok(wraps)
@@ -592,7 +592,7 @@ pub fn build_rekey_rumor(
tags.push(Tag::custom(TAG_SEVER, ["1"]));
}
Ok(stream::build_rumor_secs(
Ok(cord01::build_rumor_secs(
KIND_REKEY, rotator, &content, tags, at_secs,
))
}
@@ -633,11 +633,11 @@ pub fn build_rekey_chunks(
at_secs,
)?;
let seal = stream::build_seal(&rumor, SealForm::Encrypted, group, rotator)?;
let (wrap, _) = stream::wrap_seal(
let seal = cord01::build_seal(&rumor, SealForm::Encrypted, group, rotator)?;
let (wrap, _) = cord01::wrap_seal(
&seal,
group,
stream::KIND_WRAP,
cord01::KIND_WRAP,
Timestamp::from_secs(at_secs),
&[],
)?;
@@ -704,7 +704,7 @@ pub fn parse_rekey_chunk(opened: &OpenedStream) -> Result<RekeyChunk, RekeyError
prev_commit,
chunk: parse_chunk(rumor)?,
blobs,
citation: tag(rumor, crate::edition::TAG_CITATION)?.and_then(citation_from),
citation: tag(rumor, crate::cord04::TAG_CITATION)?.and_then(citation_from),
severed,
})
}
@@ -719,7 +719,7 @@ pub fn dissolved_tombstone_rumor(
community_id: &CommunityId,
at_secs: u64,
) -> UnsignedEvent {
stream::build_rumor_secs(
cord01::build_rumor_secs(
KIND_CONTROL,
owner,
"",
@@ -738,11 +738,11 @@ pub fn seal_dissolved(
at_secs: u64,
) -> Result<Event, RekeyError> {
let group = dissolved_group_key(community_id).map_err(crypto_error)?;
let seal = stream::build_seal(rumor, SealForm::Plaintext, &group, owner)?;
let (wrap, _) = stream::wrap_seal(
let seal = cord01::build_seal(rumor, SealForm::Plaintext, &group, owner)?;
let (wrap, _) = cord01::wrap_seal(
&seal,
&group,
stream::KIND_WRAP,
cord01::KIND_WRAP,
Timestamp::from_secs(at_secs),
&[],
)?;
@@ -756,7 +756,7 @@ pub fn open_dissolved(
community_id: &CommunityId,
) -> Result<DissolvedTombstone, RekeyError> {
let group = dissolved_group_key(community_id).map_err(crypto_error)?;
let opened = stream::open_wrap(wrap, &group)?;
let opened = cord01::open_wrap(wrap, &group)?;
if !is_tombstone(&opened.rumor, community_id) {
return Err(RekeyError::NotADissolution);
@@ -859,13 +859,13 @@ mod tests {
use std::collections::BTreeSet;
use super::*;
use crate::control::{
use crate::cord01::KIND_WRAP;
use crate::cord02::{
CommunityMetadata, ControlWriter, Edition, ROOT_EPOCH, fold_control, genesis, open_edition,
};
use crate::cord04::roles::{Grant, Permissions, Role, RoleScope};
use crate::cord04::{EditionFields, Floors, build_edition};
use crate::derive::{community_id_of, grant_locator};
use crate::edition::{EditionFields, Floors, build_edition};
use crate::roles::{Grant, Permissions, Role, RoleScope};
use crate::stream::KIND_WRAP;
use crate::{Extra, RoleId};
const AT: u64 = 1_700_000_000;
@@ -1074,7 +1074,7 @@ mod tests {
.expect("builds");
assert_eq!(chunks.len(), 1);
let opened = stream::open_wrap(&chunks[0], &group).expect("opens");
let opened = cord01::open_wrap(&chunks[0], &group).expect("opens");
let chunk = parse_rekey_chunk(&opened).expect("parses");
assert_eq!(
chunk.rotator,
@@ -1486,7 +1486,7 @@ mod tests {
at_secs: AT,
});
let seal =
stream::build_seal(&rumor, SealForm::Plaintext, &prior_read, &owner).expect("seals");
cord01::build_seal(&rumor, SealForm::Plaintext, &prior_read, &owner).expect("seals");
let refounding = plan_refounding(Epoch(1)).expect("plans");
let read = refounding.read(&community_id).expect("derives");
@@ -1497,7 +1497,7 @@ mod tests {
compact(std::slice::from_ref(&seal), &read, &signer, AT + 1).expect("compacts");
assert_eq!(compacted.len(), 1);
let reopened = stream::open_wrap_at(&compacted[0], &signer.pk(), read.conversation(), true)
let reopened = cord01::open_wrap_at(&compacted[0], &signer.pk(), read.conversation(), true)
.expect("opens");
assert_eq!(
reopened.seal.sig, seal.sig,
@@ -1508,7 +1508,7 @@ mod tests {
// Only a plaintext seal can be carried forward.
let encrypted =
stream::build_seal(&rumor, SealForm::Encrypted, &prior_read, &owner).expect("seals");
cord01::build_seal(&rumor, SealForm::Encrypted, &prior_read, &owner).expect("seals");
assert!(matches!(
compact(&[encrypted], &read, &signer, AT + 1),
Err(RekeyError::Stream(StreamError::NotRewrappable))
@@ -1550,7 +1550,7 @@ mod tests {
// The spec's all-zero `eid` is refused: it would let one owner's genuine
// tombstone be re-wrapped at another of their communities and kill it.
let zeroed = seal_dissolved(
&stream::build_rumor_secs(
&cord01::build_rumor_secs(
KIND_CONTROL,
owner.public_key(),
"",
@@ -1580,10 +1580,10 @@ mod tests {
owner: owner.public_key(),
owner_salt: other_salt,
};
let seal = stream::open_wrap(&wrap, &dissolved_group_key(&community_id).expect("derives"))
let seal = cord01::open_wrap(&wrap, &dissolved_group_key(&community_id).expect("derives"))
.expect("opens")
.seal;
let replayed = stream::wrap_seal(
let replayed = cord01::wrap_seal(
&seal,
&dissolved_group_key(&other_id).expect("derives"),
KIND_WRAP,
+9
View File
@@ -0,0 +1,9 @@
//! One module per CORD document. CORD-07 (audio/video) is unimplemented, and
//! CORD-08's timer rides the Chat and Control planes it edits rather than owning a file.
pub mod cord01;
pub mod cord02;
pub mod cord03;
pub mod cord04;
pub mod cord05;
pub mod cord06;
+9 -143
View File
@@ -1,146 +1,12 @@
pub mod chat;
pub mod control;
pub mod derive;
pub mod edition;
pub mod guestbook;
pub mod invite;
pub mod list;
pub mod pins;
pub mod rekey;
pub mod roles;
mod cords;
mod types;
mod utils;
pub mod store;
pub mod stream;
use std::fmt;
use std::str::FromStr;
pub use cords::{cord01, cord02, cord03, cord04, cord05, cord06};
pub use types::{ChannelId, CommunityId, Epoch, RoleId};
pub use utils::derive::{self, GroupKey};
use anyhow::{Result, anyhow, bail};
use data_encoding::HEXLOWER;
pub use derive::GroupKey;
use rand::TryRng as _;
use rand::rngs::SysRng;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// Unknown fields a content struct does not model, so a republish cannot wipe them.
pub(crate) type Extra = serde_json::Map<String, serde_json::Value>;
macro_rules! hex_id {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name([u8; 32]);
impl $name {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn to_hex(&self) -> String {
HEXLOWER.encode(&self.0)
}
}
impl From<[u8; 32]> for $name {
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", stringify!($name), self.to_hex())
}
}
impl FromStr for $name {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self> {
Ok(Self(decode_hex_32(value)?))
}
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
value.parse().map_err(serde::de::Error::custom)
}
}
};
}
hex_id! {
/// A self-certifying commitment to the owner's key, never on the wire.
CommunityId
}
hex_id! {
ChannelId
}
hex_id! {
/// Both a Role's entity coordinate and the field it repeats in its own content.
RoleId
}
/// A key-rotation counter; it bumps only on a Rekey that removes somebody.
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize,
)]
pub struct Epoch(pub u64);
impl fmt::Display for Epoch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// Uppercase and other non-canonical spellings are rejected.
pub(crate) fn decode_hex_32(value: &str) -> Result<[u8; 32]> {
decode_hex_lower::<32>(value)
}
pub(crate) fn decode_hex_lower<const N: usize>(value: &str) -> Result<[u8; N]> {
let bytes = HEXLOWER
.decode(value.as_bytes())
.map_err(|error| anyhow!("invalid hex: {error}"))?;
let decoded: [u8; N] = bytes
.as_slice()
.try_into()
.map_err(|_| anyhow!("expected {N} bytes, got {}", bytes.len()))?;
if HEXLOWER.encode(&decoded) != value {
bail!("hex must be lowercase and canonical");
}
Ok(decoded)
}
pub(crate) fn fill_random(bytes: &mut [u8]) -> Result<()> {
SysRng
.try_fill_bytes(bytes)
.map_err(|error| anyhow!("os rng: {error}"))
}
pub(crate) fn random_32() -> Result<[u8; 32]> {
let mut bytes = [0u8; 32];
fill_random(&mut bytes)?;
Ok(bytes)
}
pub(crate) use types::Extra;
pub(crate) use utils::{decode_hex_32, decode_hex_lower, fill_random, random_32};
+11 -10
View File
@@ -6,13 +6,13 @@ use anyhow::{Result, anyhow};
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use crate::chat::{self, ChatRumor, plane_keys};
use crate::control::{
use crate::cord01::{KIND_WRAP_EPHEMERAL, OpenedStream};
use crate::cord02::{
ChannelMetadata, CommunityGenesis, CommunityMetadata, ControlFold, ROOT_EPOCH,
};
use crate::cord03::{self, ChatRumor, plane_keys};
use crate::cord04::{EntityHead, Floors, ParsedEdition, vsk};
use crate::derive::control_signer_group_key;
use crate::edition::{EntityHead, Floors, ParsedEdition, vsk};
use crate::stream::{KIND_WRAP_EPHEMERAL, OpenedStream};
use crate::{ChannelId, CommunityId, Epoch, GroupKey};
static LOCAL_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
@@ -31,7 +31,8 @@ pub async fn cache_rumor(
channel: &ChannelId,
opened: &OpenedStream,
) -> Result<bool> {
if chat::expiration_of(&opened.rumor)?.is_some_and(|expiration| expiration <= Timestamp::now())
if cord03::expiration_of(&opened.rumor)?
.is_some_and(|expiration| expiration <= Timestamp::now())
{
return Ok(false);
}
@@ -73,7 +74,7 @@ pub async fn purge_expired(
continue;
};
let Ok(Some(expiration)) = chat::expiration_of(&rumor) else {
let Ok(Some(expiration)) = cord03::expiration_of(&rumor) else {
continue;
};
@@ -374,7 +375,7 @@ fn advance(
continue;
};
let Ok((opened, rumor)) = chat::open(wrap, group, channel, *epoch) else {
let Ok((opened, rumor)) = cord03::open(wrap, group, channel, *epoch) else {
continue;
};
@@ -419,11 +420,11 @@ mod tests {
use super::*;
use crate::Epoch;
use crate::chat::{build_message, seal_rumor};
use crate::derive::channel_group_key;
use crate::stream::{
use crate::cord01::{
KIND_WRAP, SealForm, build_rumor_ms, build_seal, channel_binding_tags, open_wrap, wrap_seal,
};
use crate::cord03::{build_message, seal_rumor};
use crate::derive::channel_group_key;
const SECRET: [u8; 32] = [0x07u8; 32];
const NEXT_SECRET: [u8; 32] = [0x11u8; 32];
+98
View File
@@ -0,0 +1,98 @@
use std::fmt;
use std::str::FromStr;
use anyhow::Result;
use data_encoding::HEXLOWER;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::decode_hex_32;
/// Unknown fields a content struct does not model, so a republish cannot wipe them.
pub(crate) type Extra = serde_json::Map<String, serde_json::Value>;
macro_rules! hex_id {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name([u8; 32]);
impl $name {
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn to_hex(&self) -> String {
HEXLOWER.encode(&self.0)
}
}
impl From<[u8; 32]> for $name {
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", stringify!($name), self.to_hex())
}
}
impl FromStr for $name {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self> {
Ok(Self(decode_hex_32(value)?))
}
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
value.parse().map_err(serde::de::Error::custom)
}
}
};
}
hex_id! {
/// A self-certifying commitment to the owner's key, never on the wire.
CommunityId
}
hex_id! {
ChannelId
}
hex_id! {
/// Both a Role's entity coordinate and the field it repeats in its own content.
RoleId
}
/// A key-rotation counter; it bumps only on a Rekey that removes somebody.
#[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize,
)]
pub struct Epoch(pub u64);
impl fmt::Display for Epoch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
+40
View File
@@ -0,0 +1,40 @@
pub mod derive;
use anyhow::{Result, anyhow, bail};
use data_encoding::HEXLOWER;
use rand::TryRng as _;
use rand::rngs::SysRng;
/// Uppercase and other non-canonical spellings are rejected.
pub(crate) fn decode_hex_32(value: &str) -> Result<[u8; 32]> {
decode_hex_lower::<32>(value)
}
pub(crate) fn decode_hex_lower<const N: usize>(value: &str) -> Result<[u8; N]> {
let bytes = HEXLOWER
.decode(value.as_bytes())
.map_err(|error| anyhow!("invalid hex: {error}"))?;
let decoded: [u8; N] = bytes
.as_slice()
.try_into()
.map_err(|_| anyhow!("expected {N} bytes, got {}", bytes.len()))?;
if HEXLOWER.encode(&decoded) != value {
bail!("hex must be lowercase and canonical");
}
Ok(decoded)
}
pub(crate) fn fill_random(bytes: &mut [u8]) -> Result<()> {
SysRng
.try_fill_bytes(bytes)
.map_err(|error| anyhow!("os rng: {error}"))
}
pub(crate) fn random_32() -> Result<[u8; 32]> {
let mut bytes = [0u8; 32];
fill_random(&mut bytes)?;
Ok(bytes)
}