update concord backend

This commit is contained in:
2026-09-16 17:35:33 +07:00
parent 66f75ad105
commit d926c1e3ea
8 changed files with 929 additions and 31 deletions
+300
View File
@@ -0,0 +1,300 @@
use anyhow::{Result, bail};
use nostr_sdk::prelude::{Event, Keys, PublicKey, Timestamp, UnsignedEvent};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::derive::{
community_id_of, control_group_key, control_signer_group_key, verify_community_id,
};
use crate::edition::{EditionFields, ParsedEdition, build_edition, parse_edition, vsk};
use crate::stream::{KIND_WRAP, SealForm, build_seal, open_wrap_at, wrap_seal_with};
use crate::{ChannelId, CommunityId, Epoch, GroupKey, random_32};
pub const MAX_NAME_BYTES: usize = 64;
pub const MAX_DESCRIPTION_BYTES: usize = 10_000;
pub const MAX_RELAYS: usize = 5;
pub const GENERAL_CHANNEL: &str = "general";
pub const ROOT_EPOCH: Epoch = Epoch(0);
/// The first edition every entity starts at. Genuinely 1, not 0: a version of
/// 0 is what the fold treats as a gap.
const GENESIS_VERSION: u64 = 1;
type Extra = Map<String, Value>;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ImageRef {
pub url: String,
pub key: String,
pub nonce: String,
pub hash: String,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct CommunityMetadata {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub relays: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<ImageRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub banner: Option<ImageRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom: Option<Extra>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ChannelMetadata {
pub name: String,
pub private: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub voice: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deleted: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom: Option<Extra>,
#[serde(flatten)]
pub extra: Extra,
}
/// A community's permanent identity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommunityIdentity {
pub community_id: CommunityId,
pub owner: PublicKey,
pub owner_salt: [u8; 32],
}
impl CommunityIdentity {
pub fn verify(&self) -> bool {
verify_community_id(&self.community_id, &self.owner.to_bytes(), &self.owner_salt)
}
}
/// Everything a creation mints:
///
/// - Identity
/// - Roots an invite will carry
/// - First channel
/// - Two genesis wraps to publish
#[derive(Debug, Clone)]
pub struct CommunityGenesis {
pub identity: CommunityIdentity,
pub community_root: [u8; 32],
pub control_root: [u8; 32],
pub channel_id: ChannelId,
pub wraps: Vec<Event>,
}
/// Mints a community and signs its two genesis editions.
pub fn genesis(
owner: &Keys,
metadata: &CommunityMetadata,
at_secs: u64,
) -> Result<CommunityGenesis> {
let mut metadata = metadata.clone();
if metadata.name.len() > MAX_NAME_BYTES {
bail!("community name exceeds {MAX_NAME_BYTES} bytes");
}
if metadata
.description
.as_ref()
.is_some_and(|description| description.len() > MAX_DESCRIPTION_BYTES)
{
bail!("community description exceeds {MAX_DESCRIPTION_BYTES} bytes");
}
metadata.relays.truncate(MAX_RELAYS);
let owner_salt = random_32()?;
let identity = CommunityIdentity {
community_id: community_id_of(&owner.public_key().to_bytes(), &owner_salt),
owner: owner.public_key(),
owner_salt,
};
let community_root = random_32()?;
let control_root = random_32()?;
let channel_id = ChannelId::from_bytes(random_32()?);
let read = control_group_key(&community_root, &identity.community_id, ROOT_EPOCH)?;
let signer = control_signer_group_key(&control_root, &identity.community_id, ROOT_EPOCH)?;
let metadata_content = serde_json::to_string(&metadata)?;
let channel_content = serde_json::to_string(&ChannelMetadata {
name: GENERAL_CHANNEL.to_owned(),
private: false,
..ChannelMetadata::default()
})?;
let editions = [
build_edition(EditionFields {
author: identity.owner,
subkind: vsk::COMMUNITY_METADATA,
entity: *identity.community_id.as_bytes(),
version: GENESIS_VERSION,
prev: None,
citation: None,
content: &metadata_content,
at_secs,
}),
build_edition(EditionFields {
author: identity.owner,
subkind: vsk::CHANNEL_METADATA,
entity: *channel_id.as_bytes(),
version: GENESIS_VERSION,
prev: None,
citation: None,
content: &channel_content,
at_secs,
}),
];
let mut wraps = Vec::with_capacity(editions.len());
for edition in &editions {
wraps.push(seal_edition(edition, owner, &read, &signer, at_secs)?);
}
Ok(CommunityGenesis {
identity,
community_root,
control_root,
channel_id,
wraps,
})
}
/// Opens a Control Plane wrap from its reading key alone.
pub fn open_edition(
wrap: &Event,
read: &GroupKey,
address: &PublicKey,
verify_wrap_signature: bool,
) -> Result<ParsedEdition> {
let opened = open_wrap_at(wrap, address, read.conversation(), verify_wrap_signature)?;
if opened.seal_form != SealForm::Plaintext {
bail!("control editions require a plaintext seal");
}
Ok(parse_edition(&opened.rumor)?)
}
fn seal_edition(
edition: &UnsignedEvent,
owner: &Keys,
read: &GroupKey,
signer: &GroupKey,
at_secs: u64,
) -> Result<Event> {
let seal = build_seal(edition, SealForm::Plaintext, read, owner)?;
let (wrap, _) = wrap_seal_with(
&seal,
read.conversation(),
signer.keys(),
KIND_WRAP,
Timestamp::from_secs(at_secs),
&[],
)?;
Ok(wrap)
}
#[cfg(test)]
mod tests {
use nostr_memory::MemoryDatabase;
use super::*;
use crate::edition::{EditionMeta, fold};
use crate::store::{CommunityState, load_state, save_state};
#[test]
fn genesis_reopens_for_a_second_holder() {
let owner = Keys::generate();
let metadata = CommunityMetadata {
name: "coop".to_owned(),
relays: vec!["wss://relay.example".to_owned()],
..CommunityMetadata::default()
};
let at_secs = 1_700_000_000;
let minted = genesis(&owner, &metadata, at_secs).expect("mints");
assert!(minted.identity.verify(), "identity is self-certifying");
// The second client holds only what an invite hands over: the roots,
// the community id and the owner salt.
let read = control_group_key(
&minted.community_root,
&minted.identity.community_id,
ROOT_EPOCH,
)
.expect("derives");
let address = control_signer_group_key(
&minted.control_root,
&minted.identity.community_id,
ROOT_EPOCH,
)
.expect("derives")
.pk();
let mut editions = Vec::new();
for wrap in &minted.wraps {
editions.push(open_edition(wrap, &read, &address, true).expect("opens"));
}
assert_eq!(editions.len(), 2);
let community = &editions[0];
assert_eq!(community.subkind, vsk::COMMUNITY_METADATA);
assert_eq!(community.entity, *minted.identity.community_id.as_bytes());
assert_eq!(community.author, owner.public_key());
assert_eq!((community.version, community.prev), (1, None));
assert_eq!(
serde_json::from_str::<CommunityMetadata>(&community.content)
.expect("parses")
.name,
"coop"
);
let channel = &editions[1];
assert_eq!(channel.subkind, vsk::CHANNEL_METADATA);
assert_eq!(channel.entity, *minted.channel_id.as_bytes());
for edition in &editions {
let folded = fold(&[EditionMeta::from(edition)], 0, None);
assert_eq!(folded.head, Some(0));
assert!(
folded.anchored && !folded.gap,
"genesis anchors at its floor"
);
}
let state =
CommunityState::from_genesis(&minted, &editions, at_secs * 1_000).expect("projects");
smol::block_on(async {
let database = MemoryDatabase::unbounded();
save_state(&database, &state).await.expect("saves");
let loaded = load_state(&database, &minted.identity.community_id)
.await
.expect("loads")
.expect("present");
assert_eq!(loaded.community_root, minted.community_root);
assert_eq!(loaded.control_root, Some(minted.control_root));
assert_eq!(loaded.channels.len(), 1);
assert_eq!(loaded.heads.len(), 2);
});
}
}