refactor concord

This commit is contained in:
2026-09-19 07:24:01 +07:00
parent 91c40b0799
commit 9081675494
12 changed files with 669 additions and 258 deletions
-3
View File
@@ -300,9 +300,6 @@ async fn subscribe(
}
}
// Concord wraps share kind 1059 with NIP-59 gift wraps, so an automatic
// target sends gossip after the plane authors as if they were DM peers.
// The community's own relays are the routing relays, so target them.
let target = if relays.is_empty() {
ReqTarget::auto(vec![filter])
} else {
+39 -19
View File
@@ -3,8 +3,8 @@ use std::fmt;
use data_encoding::BASE64;
use nostr::nips::nip44::v2::{ConversationKey, decrypt_to_bytes, encrypt_to_bytes_with_nonce};
use nostr_sdk::prelude::{
Event, EventBuilder, EventId, FinalizeEvent, Keys, Kind, PublicKey, Tag, Timestamp,
UnsignedEvent,
AsyncGetPublicKey, AsyncNip44, AsyncSignEvent, Event, EventBuilder, EventId, FinalizeEvent,
FinalizeEventAsync, Keys, Kind, PublicKey, Tag, Timestamp, UnsignedEvent,
};
use crate::derive::GroupKey;
@@ -198,32 +198,52 @@ pub fn open_bytes(conversation: &ConversationKey, content: &str) -> Result<Vec<u
}
/// A member's own document (the Community List, the Invite List): NIP-44 to self.
pub fn seal_to_self(keys: &Keys, plaintext: &[u8]) -> Result<String, StreamError> {
seal_bytes(
&ConversationKey::derive(keys.secret_key(), &keys.public_key())
.map_err(|error| StreamError::Encrypt(error.to_string()))?,
plaintext,
)
pub async fn seal_to_self<S>(signer: &S, plaintext: &str) -> Result<String, StreamError>
where
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
{
check_plaintext_cap(plaintext.len())?;
let address = signer
.get_public_key_async()
.await
.map_err(|error| StreamError::Encrypt(error.to_string()))?;
signer
.nip44_encrypt_async(&address, plaintext)
.await
.map_err(|error| StreamError::Encrypt(error.to_string()))
}
pub fn open_to_self(keys: &Keys, content: &str) -> Result<Vec<u8>, StreamError> {
open_bytes(
&ConversationKey::derive(keys.secret_key(), &keys.public_key())
.map_err(|error| StreamError::Decrypt(error.to_string()))?,
content,
)
pub async fn open_to_self<S>(signer: &S, content: &str) -> Result<String, StreamError>
where
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
{
let address = signer
.get_public_key_async()
.await
.map_err(|error| StreamError::Decrypt(error.to_string()))?;
signer
.nip44_decrypt_async(&address, content)
.await
.map_err(|error| StreamError::Decrypt(error.to_string()))
}
pub fn build_seal(
pub async fn build_seal<S>(
rumor: &UnsignedEvent,
form: SealForm,
group: &GroupKey,
author: &Keys,
) -> Result<Event, StreamError> {
author: &S,
) -> Result<Event, StreamError>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let content = seal_content(rumor, form, group)?;
EventBuilder::new(Kind::Custom(form.kind()), content)
.custom_created_at(rumor.created_at)
.finalize(author)
.finalize_async(author)
.await
.map_err(|error| StreamError::Sign(error.to_string()))
}
@@ -450,7 +470,7 @@ mod tests {
}
fn sealed(rumor: &UnsignedEvent, form: SealForm, author: &Keys) -> Event {
build_seal(rumor, form, &group(0), author).expect("seals")
smol::block_on(build_seal(rumor, form, &group(0), author)).expect("seals")
}
fn wrapped(seal: &Event, kind: u16, at_secs: u64) -> Event {
+20 -9
View File
@@ -183,18 +183,21 @@ pub fn build_snapshot_chunks(
.collect()
}
pub fn seal_rumor(
pub async fn seal_rumor<S>(
rumor: &UnsignedEvent,
group: &GroupKey,
author: &Keys,
) -> Result<(Event, Keys), GuestbookError> {
author: &S,
) -> Result<(Event, Keys), GuestbookError>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let kind = rumor.kind.as_u16();
if !is_guestbook_kind(kind) {
return Err(GuestbookError::UnknownKind(kind));
}
let seal = build_seal(rumor, SealForm::Encrypted, group, author)?;
let seal = build_seal(rumor, SealForm::Encrypted, group, author).await?;
Ok(wrap_seal(&seal, group, KIND_WRAP, rumor.created_at, &[])?)
}
@@ -556,7 +559,9 @@ mod tests {
}
fn publish(rumor: &UnsignedEvent, author: &Keys) -> GuestbookRumor {
let wrap = seal_rumor(rumor, &group(), author).expect("seals").0;
let wrap = smol::block_on(seal_rumor(rumor, &group(), author))
.expect("seals")
.0;
open(&wrap, &group()).expect("opens").1
}
@@ -826,7 +831,9 @@ mod tests {
);
assert!(matches!(
open(
&seal_rumor(&bad_ms, &group(), &member).expect("seals").0,
&smol::block_on(seal_rumor(&bad_ms, &group(), &member))
.expect("seals")
.0,
&group()
),
Err(GuestbookError::Stream(StreamError::BadMs))
@@ -835,7 +842,9 @@ mod tests {
let bad_verb = build_rumor_ms(KIND_JOIN_LEAVE, member.public_key(), "maybe", vec![], AT);
assert!(matches!(
open(
&seal_rumor(&bad_verb, &group(), &member).expect("seals").0,
&smol::block_on(seal_rumor(&bad_verb, &group(), &member))
.expect("seals")
.0,
&group()
),
Err(GuestbookError::BadTag(TAG_CONTENT))
@@ -854,7 +863,7 @@ mod tests {
);
assert!(matches!(
open(
&seal_rumor(&ambiguous, &group(), &moderator)
&smol::block_on(seal_rumor(&ambiguous, &group(), &moderator))
.expect("seals")
.0,
&group()
@@ -876,7 +885,9 @@ mod tests {
);
assert!(matches!(
open(
&seal_rumor(&rumor, &group(), &moderator).expect("seals").0,
&smol::block_on(seal_rumor(&rumor, &group(), &moderator))
.expect("seals")
.0,
&group()
),
Err(GuestbookError::BadTag(TAG_SNAP))
+26 -13
View File
@@ -183,25 +183,32 @@ pub fn merge(held: CommunityList, incoming: CommunityList) -> CommunityList {
}
}
pub fn build_list_event(keys: &Keys, list: &CommunityList) -> Result<Event, ListError> {
pub async fn build_list_event<S>(keys: &S, list: &CommunityList) -> Result<Event, ListError>
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
{
list.fits()?;
let json = serde_json::to_string(list).map_err(json_error)?;
let content = cord01::seal_to_self(keys, json.as_bytes())?;
let content = cord01::seal_to_self(keys, &json).await?;
EventBuilder::new(Kind::Custom(KIND_COMMUNITY_LIST), content)
.finalize(keys)
.finalize_async(keys)
.await
.map_err(crypto_error)
}
pub fn parse_list_event(keys: &Keys, event: &Event) -> Result<CommunityList, ListError> {
pub async fn parse_list_event<S>(keys: &S, event: &Event) -> Result<CommunityList, ListError>
where
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
{
if event.kind.as_u16() != KIND_COMMUNITY_LIST {
return Err(ListError::Kind(event.kind.as_u16()));
}
let json = cord01::open_to_self(keys, &event.content)?;
let json = cord01::open_to_self(keys, &event.content).await?;
serde_json::from_slice(&json).map_err(json_error)
serde_json::from_str(&json).map_err(json_error)
}
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -419,18 +426,21 @@ mod tests {
extra: Extra::default(),
};
let event = build_list_event(&me, &mine).expect("builds");
let event = smol::block_on(build_list_event(&me, &mine)).expect("builds");
assert_eq!(event.kind, Kind::Custom(KIND_COMMUNITY_LIST));
assert_eq!(parse_list_event(&me, &event).expect("parses"), mine);
assert_eq!(
smol::block_on(parse_list_event(&me, &event)).expect("parses"),
mine
);
assert!(
!parse_list_event(&me, &event)
!smol::block_on(parse_list_event(&me, &event))
.expect("parses")
.is_live(&id(0x33))
);
// Only the member's own keys open it, and an unreadable list is "no news".
let stranger = Keys::generate();
assert!(parse_list_event(&stranger, &event).is_err());
assert!(smol::block_on(parse_list_event(&stranger, &event)).is_err());
// Unknown fields survive the round trip, so a republish cannot wipe them.
let mut held = mine.clone();
@@ -440,8 +450,11 @@ mod tests {
.current
.extra
.insert("held_roots".to_owned(), serde_json::json!([{"epoch": 1}]));
let rebuilt =
parse_list_event(&me, &build_list_event(&me, &held).expect("builds")).expect("parses");
let rebuilt = smol::block_on(parse_list_event(
&me,
&smol::block_on(build_list_event(&me, &held)).expect("builds"),
))
.expect("parses");
assert_eq!(rebuilt, held);
// The write gate refuses an over-cap or oversized List before publishing.
@@ -459,7 +472,7 @@ mod tests {
.collect(),
);
assert!(matches!(
build_list_event(&me, &crowded),
smol::block_on(build_list_event(&me, &crowded)),
Err(ListError::TooManyMemberships(n)) if n == MAX_MEMBERSHIPS + 1
));
+190 -146
View File
@@ -4,7 +4,9 @@ pub mod list;
use std::collections::{BTreeMap, BTreeSet};
use anyhow::{Result, bail};
use nostr_sdk::prelude::{Event, Keys, PublicKey, Timestamp, UnsignedEvent};
use nostr_sdk::prelude::{
AsyncGetPublicKey, AsyncSignEvent, Event, PublicKey, Timestamp, UnsignedEvent,
};
use serde::{Deserialize, Serialize};
use crate::cord01::{KIND_WRAP, SealForm, build_seal, open_wrap_at, wrap_seal_with};
@@ -114,17 +116,24 @@ pub struct CommunityGenesis {
pub wraps: Vec<Event>,
}
pub fn genesis(
owner: &Keys,
pub async fn genesis<S>(
owner: &S,
metadata: &CommunityMetadata,
at_secs: u64,
) -> Result<CommunityGenesis> {
) -> Result<CommunityGenesis>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let metadata_content = encode_metadata(metadata)?;
let owner_salt = random_32()?;
let owner_key = owner
.get_public_key_async()
.await
.map_err(|error| anyhow::anyhow!("signer: {error}"))?;
let identity = CommunityIdentity {
community_id: community_id_of(&owner.public_key().to_bytes(), &owner_salt),
owner: owner.public_key(),
community_id: community_id_of(&owner_key.to_bytes(), &owner_salt),
owner: owner_key,
owner_salt,
};
@@ -167,7 +176,7 @@ pub fn genesis(
let mut wraps = Vec::with_capacity(editions.len());
for edition in &editions {
wraps.push(seal_edition(edition, owner, &read, &signer, at_secs)?);
wraps.push(seal_edition(edition, owner, &read, &signer, at_secs).await?);
}
Ok(CommunityGenesis {
@@ -211,12 +220,15 @@ pub struct Edition<'a> {
}
impl ControlWriter {
pub fn publish(
pub async fn publish<S>(
&self,
keys: &Keys,
keys: &S,
edition: Edition<'_>,
at_secs: u64,
) -> Result<(Event, EntityHead)> {
) -> Result<(Event, EntityHead)>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let rumor = build_edition(EditionFields {
author: self.author,
subkind: edition.subkind,
@@ -231,20 +243,23 @@ impl ControlWriter {
});
let parsed = parse_edition(&rumor)?;
let wrap = seal_edition(&rumor, keys, &self.read, &self.signer, at_secs)?;
let wrap = seal_edition(&rumor, keys, &self.read, &self.signer, at_secs).await?;
Ok((wrap, EntityHead::from(&parsed)))
}
pub fn set_community_metadata(
pub async fn set_community_metadata<S>(
&self,
keys: &Keys,
keys: &S,
community_id: &CommunityId,
metadata: &CommunityMetadata,
head: Option<&EntityHead>,
citation: Option<AuthorityCitation>,
at_secs: u64,
) -> Result<(Event, EntityHead)> {
) -> Result<(Event, EntityHead)>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let content = encode_metadata(metadata)?;
self.publish(
@@ -258,17 +273,21 @@ impl ControlWriter {
},
at_secs,
)
.await
}
pub fn set_channel_metadata(
pub async fn set_channel_metadata<S>(
&self,
keys: &Keys,
keys: &S,
channel: &ChannelId,
metadata: &ChannelMetadata,
head: Option<&EntityHead>,
citation: Option<AuthorityCitation>,
at_secs: u64,
) -> Result<(Event, EntityHead)> {
) -> Result<(Event, EntityHead)>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
if metadata.name.len() > MAX_NAME_BYTES {
bail!("channel name exceeds {MAX_NAME_BYTES} bytes");
}
@@ -286,16 +305,20 @@ impl ControlWriter {
},
at_secs,
)
.await
}
pub fn set_role(
pub async fn set_role<S>(
&self,
keys: &Keys,
keys: &S,
role: &Role,
head: Option<&EntityHead>,
citation: Option<AuthorityCitation>,
at_secs: u64,
) -> Result<(Event, EntityHead)> {
) -> Result<(Event, EntityHead)>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
if role.name.len() > MAX_NAME_BYTES {
bail!("role name exceeds {MAX_NAME_BYTES} bytes");
}
@@ -313,17 +336,21 @@ impl ControlWriter {
},
at_secs,
)
.await
}
pub fn set_grant(
pub async fn set_grant<S>(
&self,
keys: &Keys,
keys: &S,
community_id: &CommunityId,
grant: &Grant,
head: Option<&EntityHead>,
citation: Option<AuthorityCitation>,
at_secs: u64,
) -> Result<(Event, EntityHead)> {
) -> Result<(Event, EntityHead)>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let content = grant.to_content()?;
self.publish(
@@ -337,17 +364,21 @@ impl ControlWriter {
},
at_secs,
)
.await
}
pub fn set_banlist(
pub async fn set_banlist<S>(
&self,
keys: &Keys,
keys: &S,
community_id: &CommunityId,
banned: &BTreeSet<PublicKey>,
head: Option<&EntityHead>,
citation: Option<AuthorityCitation>,
at_secs: u64,
) -> Result<(Event, EntityHead)> {
) -> Result<(Event, EntityHead)>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
if banned.len() > MAX_BANLIST {
bail!("banlist exceeds {MAX_BANLIST} entries");
}
@@ -366,19 +397,23 @@ impl ControlWriter {
},
at_secs,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub fn set_registry(
pub async fn set_registry<S>(
&self,
keys: &Keys,
keys: &S,
community_id: &CommunityId,
creator: &PublicKey,
links: &[PublicKey],
head: Option<&EntityHead>,
citation: Option<AuthorityCitation>,
at_secs: u64,
) -> Result<(Event, EntityHead)> {
) -> Result<(Event, EntityHead)>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let entries: Vec<String> = links
.iter()
.take(MAX_REGISTRY_LINKS)
@@ -397,20 +432,24 @@ impl ControlWriter {
},
at_secs,
)
.await
}
/// The whole Pin List, in whichever of CORD-04 §7's two forms the Channel calls for.
#[allow(clippy::too_many_arguments)]
pub fn set_pin_list(
pub async fn set_pin_list<S>(
&self,
keys: &Keys,
keys: &S,
community_id: &CommunityId,
channel: &ChannelId,
content: &str,
head: Option<&EntityHead>,
citation: Option<AuthorityCitation>,
at_secs: u64,
) -> Result<(Event, EntityHead)> {
) -> Result<(Event, EntityHead)>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
self.publish(
keys,
Edition {
@@ -422,6 +461,7 @@ impl ControlWriter {
},
at_secs,
)
.await
}
}
@@ -708,14 +748,17 @@ fn authorized_head<'a>(
selection.head.map(|index| authorized[index])
}
fn seal_edition(
async fn seal_edition<S>(
edition: &UnsignedEvent,
owner: &Keys,
owner: &S,
read: &GroupKey,
signer: &GroupKey,
at_secs: u64,
) -> Result<Event> {
let seal = build_seal(edition, SealForm::Plaintext, read, owner)?;
) -> Result<Event>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let seal = build_seal(edition, SealForm::Plaintext, read, owner).await?;
let (wrap, _) = wrap_seal_with(
&seal,
@@ -731,6 +774,8 @@ fn seal_edition(
#[cfg(test)]
mod tests {
use nostr_sdk::prelude::Keys;
use super::*;
use crate::cord03::{self, build_message, seal_rumor};
use crate::cord04::pins;
@@ -768,7 +813,7 @@ mod tests {
#[test]
fn metadata_and_channel_edits_reach_a_second_client() {
let owner = Keys::generate();
let minted = genesis(&owner, &metadata("coop"), AT).expect("mints");
let minted = smol::block_on(genesis(&owner, &metadata("coop"), AT)).expect("mints");
let community_id = minted.identity.community_id;
let owner_pk = owner.public_key();
let (read, signer) = holder(&minted);
@@ -797,33 +842,31 @@ mod tests {
.get(minted.channel_id.as_bytes())
.expect("head");
let (community_wrap, _) = writer
.set_community_metadata(
&owner,
&community_id,
&CommunityMetadata {
relays: vec!["wss://relay.example".to_owned()],
..metadata("coop two")
},
Some(community_head),
None,
AT + 1,
)
.expect("publishes");
let (channel_wrap, _) = writer
.set_channel_metadata(
&owner,
&minted.channel_id,
&ChannelMetadata {
name: "lobby".to_owned(),
private: false,
..ChannelMetadata::default()
},
Some(channel_head),
None,
AT + 2,
)
.expect("publishes");
let (community_wrap, _) = smol::block_on(writer.set_community_metadata(
&owner,
&community_id,
&CommunityMetadata {
relays: vec!["wss://relay.example".to_owned()],
..metadata("coop two")
},
Some(community_head),
None,
AT + 1,
))
.expect("publishes");
let (channel_wrap, _) = smol::block_on(writer.set_channel_metadata(
&owner,
&minted.channel_id,
&ChannelMetadata {
name: "lobby".to_owned(),
private: false,
..ChannelMetadata::default()
},
Some(channel_head),
None,
AT + 2,
))
.expect("publishes");
let mut edited = genesis_editions.clone();
edited.extend(open_all(
@@ -875,7 +918,7 @@ mod tests {
fn a_delegated_member_edits_metadata_only_under_its_own_grant() {
let owner = Keys::generate();
let member = Keys::generate();
let minted = genesis(&owner, &metadata("coop"), AT).expect("mints");
let minted = smol::block_on(genesis(&owner, &metadata("coop"), AT)).expect("mints");
let community_id = minted.identity.community_id;
let owner_pk = owner.public_key();
let (read, signer) = holder(&minted);
@@ -896,21 +939,20 @@ mod tests {
extra: Extra::default(),
};
let (role_wrap, _) = writer
.publish(
&owner,
Edition {
subkind: vsk::ROLE,
entity: *role_id.as_bytes(),
content: &role.to_content().expect("serializes"),
head: None,
citation: None,
},
AT + 1,
)
.expect("publishes");
let (grant_wrap, _) = writer
.publish(
let (role_wrap, _) = smol::block_on(writer.publish(
&owner,
Edition {
subkind: vsk::ROLE,
entity: *role_id.as_bytes(),
content: &role.to_content().expect("serializes"),
head: None,
citation: None,
},
AT + 1,
))
.expect("publishes");
let (grant_wrap, _) = smol::block_on(
writer.publish(
&owner,
Edition {
subkind: vsk::GRANT,
@@ -927,8 +969,9 @@ mod tests {
citation: None,
},
AT + 2,
)
.expect("publishes");
),
)
.expect("publishes");
let mut base = open_all(&minted.wraps, &read, &signer.pk());
base.extend(open_all(&[role_wrap, grant_wrap], &read, &signer.pk()));
@@ -959,36 +1002,34 @@ mod tests {
};
let content = serde_json::to_string(&metadata("coop by mod")).expect("serializes");
let (uncited, _) = member_writer
.publish(
&member,
Edition {
subkind: vsk::COMMUNITY_METADATA,
entity: *community_id.as_bytes(),
content: &content,
head: Some(head),
citation: None,
},
AT + 3,
)
.expect("publishes");
let (cited, _) = member_writer
.publish(
&member,
Edition {
subkind: vsk::COMMUNITY_METADATA,
entity: *community_id.as_bytes(),
content: &content,
head: Some(head),
citation: Some(AuthorityCitation {
entity: grant.entity,
version: grant.version,
hash: grant.self_hash,
}),
},
AT + 4,
)
.expect("publishes");
let (uncited, _) = smol::block_on(member_writer.publish(
&member,
Edition {
subkind: vsk::COMMUNITY_METADATA,
entity: *community_id.as_bytes(),
content: &content,
head: Some(head),
citation: None,
},
AT + 3,
))
.expect("publishes");
let (cited, _) = smol::block_on(member_writer.publish(
&member,
Edition {
subkind: vsk::COMMUNITY_METADATA,
entity: *community_id.as_bytes(),
content: &content,
head: Some(head),
citation: Some(AuthorityCitation {
entity: grant.entity,
version: grant.version,
hash: grant.self_hash,
}),
},
AT + 4,
))
.expect("publishes");
// Uncited, the edit claims an authority the member never showed.
let mut forged = base.clone();
@@ -1023,7 +1064,7 @@ mod tests {
#[test]
fn a_pin_list_folds_under_its_coordinate_for_a_second_client() {
let owner = Keys::generate();
let minted = genesis(&owner, &metadata("coop"), AT).expect("mints");
let minted = smol::block_on(genesis(&owner, &metadata("coop"), AT)).expect("mints");
let community_id = minted.identity.community_id;
let owner_pk = owner.public_key();
let (read, signer) = holder(&minted);
@@ -1042,7 +1083,7 @@ mod tests {
AT * 1_000,
None,
);
let (wrap, _) = seal_rumor(&rumor, &group, &author, false).expect("seals");
let (wrap, _) = smol::block_on(seal_rumor(&rumor, &group, &author, false)).expect("seals");
let opened = cord03::open(&wrap, &group, &channel, ROOT_EPOCH)
.expect("opens")
.0;
@@ -1064,17 +1105,16 @@ mod tests {
read: read.clone(),
signer: signer.clone(),
};
let (pin_wrap, _) = writer
.set_pin_list(
&owner,
&community_id,
&channel,
&content,
None,
None,
AT + 1,
)
.expect("publishes");
let (pin_wrap, _) = smol::block_on(writer.set_pin_list(
&owner,
&community_id,
&channel,
&content,
None,
None,
AT + 1,
))
.expect("publishes");
let mut editions = open_all(&minted.wraps, &read, &signer.pk());
editions.extend(open_all(&[pin_wrap], &read, &signer.pk()));
@@ -1109,7 +1149,7 @@ mod tests {
#[test]
fn the_timer_is_never_guessed_and_the_write_caps_hold() {
let owner = Keys::generate();
let minted = genesis(&owner, &metadata("coop"), AT).expect("mints");
let minted = smol::block_on(genesis(&owner, &metadata("coop"), AT)).expect("mints");
let community_id = minted.identity.community_id;
let owner_pk = owner.public_key();
let (read, signer) = holder(&minted);
@@ -1124,10 +1164,16 @@ mod tests {
&owner_pk,
&community_id,
&open_all(
&[writer
.set_community_metadata(&owner, &community_id, metadata, None, None, AT + 1)
.expect("publishes")
.0],
&[smol::block_on(writer.set_community_metadata(
&owner,
&community_id,
metadata,
None,
None,
AT + 1,
))
.expect("publishes")
.0],
&writer.read,
&writer.signer.pk(),
),
@@ -1163,8 +1209,7 @@ mod tests {
.map(|_| Keys::generate().public_key())
.collect();
assert!(
writer
.set_banlist(&owner, &community_id, &banned, None, None, AT + 2)
smol::block_on(writer.set_banlist(&owner, &community_id, &banned, None, None, AT + 2))
.is_err()
);
@@ -1179,20 +1224,19 @@ mod tests {
assert!(grant.to_content().is_err());
assert!(
writer
.set_channel_metadata(
&owner,
&minted.channel_id,
&ChannelMetadata {
name: "x".repeat(MAX_NAME_BYTES + 1),
private: false,
..ChannelMetadata::default()
},
None,
None,
AT + 3,
)
.is_err()
smol::block_on(writer.set_channel_metadata(
&owner,
&minted.channel_id,
&ChannelMetadata {
name: "x".repeat(MAX_NAME_BYTES + 1),
private: false,
..ChannelMetadata::default()
},
None,
None,
AT + 3,
))
.is_err()
);
}
}
+13 -7
View File
@@ -292,19 +292,22 @@ pub fn build_typing(
}
/// `ephemeral` picks the 21059 wrap, which relays must not store.
pub fn seal_rumor(
pub async fn seal_rumor<S>(
rumor: &UnsignedEvent,
group: &GroupKey,
author: &Keys,
author: &S,
ephemeral: bool,
) -> Result<(Event, Keys), ChatError> {
) -> Result<(Event, Keys), ChatError>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let kind = rumor.kind.as_u16();
if !is_chat_kind(kind) {
return Err(ChatError::UnknownKind(kind));
}
let seal = build_seal(rumor, SealForm::Encrypted, group, author)?;
let seal = build_seal(rumor, SealForm::Encrypted, group, author).await?;
let wrap_kind = if ephemeral {
KIND_WRAP_EPHEMERAL
} else {
@@ -674,7 +677,9 @@ mod tests {
}
fn sealed(rumor: &UnsignedEvent, group: &GroupKey, author: &Keys) -> Event {
seal_rumor(rumor, group, author, false).expect("seals").0
smol::block_on(seal_rumor(rumor, group, author, false))
.expect("seals")
.0
}
fn read(rumor: &UnsignedEvent, group: &GroupKey, author: &Keys, epoch: Epoch) -> ChatRumor {
@@ -949,7 +954,8 @@ mod tests {
// Chat is encrypted-seal only (CORD-02 §5), and a retired kind is not a
// chat rumor however well-formed it looks.
let seal = build_seal(&plain, SealForm::Plaintext, &group, &alice).expect("seals");
let seal =
smol::block_on(build_seal(&plain, SealForm::Plaintext, &group, &alice)).expect("seals");
let (wrap, _) = wrap_seal(
&seal,
&group,
@@ -971,7 +977,7 @@ mod tests {
AT,
);
assert!(matches!(
seal_rumor(&ghost, &group, &alice, false),
smol::block_on(seal_rumor(&ghost, &group, &alice, false)),
Err(ChatError::UnknownKind(3300))
));
+6 -4
View File
@@ -583,7 +583,7 @@ mod tests {
at_ms,
None,
);
let (wrap, _) = seal_rumor(&rumor, &group(), author, false).expect("seals");
let (wrap, _) = smol::block_on(seal_rumor(&rumor, &group(), author, false)).expect("seals");
open(&wrap, &group(), &channel(), Epoch(0)).expect("opens")
}
@@ -699,7 +699,7 @@ mod tests {
AT_MS + 5_000,
None,
);
let (wrap, _) = seal_rumor(&edit, &group(), &author, false).expect("seals");
let (wrap, _) = smol::block_on(seal_rumor(&edit, &group(), &author, false)).expect("seals");
let (edit_opened, _) = open(&wrap, &group(), &channel(), Epoch(0)).expect("opens");
let refreshed = with_proven_edit(&entry, &edit_opened, &group(), &channel());
@@ -718,7 +718,8 @@ mod tests {
AT_MS + 6_000,
None,
);
let (wrap, _) = seal_rumor(&hijack, &group(), &stranger, false).expect("seals");
let (wrap, _) =
smol::block_on(seal_rumor(&hijack, &group(), &stranger, false)).expect("seals");
let (hijack_opened, _) = open(&wrap, &group(), &channel(), Epoch(0)).expect("opens");
let unchanged = with_proven_edit(&entry, &hijack_opened, &group(), &channel());
@@ -805,7 +806,8 @@ mod tests {
None,
AT_MS + 1_000,
);
let (wrap, _) = seal_rumor(&delete, &group(), author_keys, false).expect("seals");
let (wrap, _) =
smol::block_on(seal_rumor(&delete, &group(), author_keys, false)).expect("seals");
let (_, rumor) = open(&wrap, &group(), &channel(), Epoch(0)).expect("opens");
assert_eq!(
+13 -6
View File
@@ -590,25 +590,32 @@ pub fn merge_invite_lists(held: InviteList, incoming: InviteList) -> InviteList
}
}
pub fn build_invite_list(keys: &Keys, list: &InviteList) -> Result<Event, InviteError> {
pub async fn build_invite_list<S>(keys: &S, list: &InviteList) -> Result<Event, InviteError>
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
{
list.fits()?;
let json = serde_json::to_string(list).map_err(json_error)?;
let content = cord01::seal_to_self(keys, json.as_bytes())?;
let content = cord01::seal_to_self(keys, &json).await?;
EventBuilder::new(Kind::Custom(KIND_INVITE_LIST), content)
.finalize(keys)
.finalize_async(keys)
.await
.map_err(crypto_error)
}
pub fn parse_invite_list(keys: &Keys, event: &Event) -> Result<InviteList, InviteError> {
pub async fn parse_invite_list<S>(keys: &S, event: &Event) -> Result<InviteList, InviteError>
where
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
{
if event.kind.as_u16() != KIND_INVITE_LIST {
return Err(InviteError::Kind(event.kind.as_u16()));
}
let json = cord01::open_to_self(keys, &event.content)?;
let json = cord01::open_to_self(keys, &event.content).await?;
serde_json::from_slice(&json).map_err(json_error)
serde_json::from_str(&json).map_err(json_error)
}
/// An entry is immutable once minted, so two copies should agree.
+61 -40
View File
@@ -4,7 +4,10 @@ use std::fmt;
use anyhow::Result;
use data_encoding::HEXLOWER;
use nostr::nips::nip44::v2::ConversationKey;
use nostr_sdk::prelude::{Event, Keys, PublicKey, SecretKey, Tag, Timestamp, UnsignedEvent};
use nostr_sdk::prelude::{
AsyncGetPublicKey, AsyncSignEvent, Event, Keys, PublicKey, SecretKey, Tag, Timestamp,
UnsignedEvent,
};
use serde::{Deserialize, Serialize};
use crate::cord01::{self, KIND_SEAL_PLAINTEXT, OpenedStream, SealForm, StreamError};
@@ -598,8 +601,8 @@ pub fn build_rekey_rumor(
}
#[allow(clippy::too_many_arguments)]
pub fn build_rekey_chunks(
rotator: &Keys,
pub async fn build_rekey_chunks<S>(
rotator: &S,
group: &GroupKey,
scope: RekeyScope,
new_epoch: Epoch,
@@ -609,7 +612,11 @@ pub fn build_rekey_chunks(
citation: Option<&AuthorityCitation>,
severed: bool,
at_secs: u64,
) -> Result<Vec<Event>, RekeyError> {
) -> Result<Vec<Event>, RekeyError>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let rotator_key = rotator.get_public_key_async().await.map_err(crypto_error)?;
let mut groups: Vec<&[RekeyBlob]> = blobs.chunks(MAX_REKEY_BLOBS_PER_EVENT).collect();
if groups.is_empty() {
@@ -621,7 +628,7 @@ pub fn build_rekey_chunks(
for (index, group_blobs) in groups.into_iter().enumerate() {
let rumor = build_rekey_rumor(
rotator.public_key(),
rotator_key,
scope,
new_epoch,
prev_epoch,
@@ -633,7 +640,7 @@ pub fn build_rekey_chunks(
at_secs,
)?;
let seal = cord01::build_seal(&rumor, SealForm::Encrypted, group, rotator)?;
let seal = cord01::build_seal(&rumor, SealForm::Encrypted, group, rotator).await?;
let (wrap, _) = cord01::wrap_seal(
&seal,
group,
@@ -731,14 +738,17 @@ pub fn dissolved_tombstone_rumor(
)
}
pub fn seal_dissolved(
pub async fn seal_dissolved<S>(
rumor: &UnsignedEvent,
community_id: &CommunityId,
owner: &Keys,
owner: &S,
at_secs: u64,
) -> Result<Event, RekeyError> {
) -> Result<Event, RekeyError>
where
S: AsyncGetPublicKey + AsyncSignEvent + ?Sized,
{
let group = dissolved_group_key(community_id).map_err(crypto_error)?;
let seal = cord01::build_seal(rumor, SealForm::Plaintext, &group, owner)?;
let seal = cord01::build_seal(rumor, SealForm::Plaintext, &group, owner).await?;
let (wrap, _) = cord01::wrap_seal(
&seal,
&group,
@@ -1059,7 +1069,7 @@ mod tests {
let group = rekey_group(scope, &ROOT, &community_id, epoch).expect("derives");
let prior_commit = epoch_key_commitment(Epoch(0), &PRIOR_KEY);
let chunks = build_rekey_chunks(
let chunks = smol::block_on(build_rekey_chunks(
&rotator,
&group,
scope,
@@ -1070,7 +1080,7 @@ mod tests {
None,
false,
AT,
)
))
.expect("builds");
assert_eq!(chunks.len(), 1);
@@ -1269,26 +1279,26 @@ mod tests {
content: String,
at_secs: u64,
) -> Event {
writer
.publish(
owner,
Edition {
subkind,
entity,
content: &content,
head: None,
citation: None,
},
at_secs,
)
.expect("publishes")
.0
smol::block_on(writer.publish(
owner,
Edition {
subkind,
entity,
content: &content,
head: None,
citation: None,
},
at_secs,
))
.expect("publishes")
.0
}
#[test]
fn a_rotation_needs_the_permission_and_must_strictly_outrank_every_target() {
let owner = Keys::generate();
let minted = genesis(&owner, &CommunityMetadata::default(), AT).expect("mints");
let minted =
smol::block_on(genesis(&owner, &CommunityMetadata::default(), AT)).expect("mints");
let community_id = minted.identity.community_id;
let read =
control_group_key(&minted.community_root, &community_id, ROOT_EPOCH).expect("derives");
@@ -1426,7 +1436,7 @@ mod tests {
})
.collect();
let chunks = build_rekey_chunks(
let chunks = smol::block_on(build_rekey_chunks(
&rotator,
&group,
scope,
@@ -1437,7 +1447,7 @@ mod tests {
None,
false,
AT,
)
))
.expect("builds");
assert_eq!(chunks.len(), 1, "a full send chunk is one event");
@@ -1452,7 +1462,7 @@ mod tests {
wrapped: "x".to_owned(),
});
let chunks = build_rekey_chunks(
let chunks = smol::block_on(build_rekey_chunks(
&rotator,
&group,
scope,
@@ -1463,7 +1473,7 @@ mod tests {
None,
false,
AT,
)
))
.expect("builds");
assert_eq!(chunks.len(), 2, "one over the cap splits across two events");
@@ -1485,8 +1495,13 @@ mod tests {
content: "{}",
at_secs: AT,
});
let seal =
cord01::build_seal(&rumor, SealForm::Plaintext, &prior_read, &owner).expect("seals");
let seal = smol::block_on(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");
@@ -1507,8 +1522,13 @@ mod tests {
assert_eq!(reopened.author, owner.public_key());
// Only a plaintext seal can be carried forward.
let encrypted =
cord01::build_seal(&rumor, SealForm::Encrypted, &prior_read, &owner).expect("seals");
let encrypted = smol::block_on(cord01::build_seal(
&rumor,
SealForm::Encrypted,
&prior_read,
&owner,
))
.expect("seals");
assert!(matches!(
compact(&[encrypted], &read, &signer, AT + 1),
Err(RekeyError::Stream(StreamError::NotRewrappable))
@@ -1527,7 +1547,8 @@ mod tests {
};
let rumor = dissolved_tombstone_rumor(owner.public_key(), &community_id, AT);
let wrap = seal_dissolved(&rumor, &community_id, &owner, AT).expect("seals");
let wrap =
smol::block_on(seal_dissolved(&rumor, &community_id, &owner, AT)).expect("seals");
assert!(verify_dissolved(&wrap, &identity));
assert_eq!(
@@ -1538,18 +1559,18 @@ mod tests {
// Anyone holding the community id finds the address, but only the committed
// owner's signature counts.
let impostor = Keys::generate();
let forged = seal_dissolved(
let forged = smol::block_on(seal_dissolved(
&dissolved_tombstone_rumor(impostor.public_key(), &community_id, AT),
&community_id,
&impostor,
AT,
)
))
.expect("seals");
assert!(!verify_dissolved(&forged, &identity));
// 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(
let zeroed = smol::block_on(seal_dissolved(
&cord01::build_rumor_secs(
KIND_CONTROL,
owner.public_key(),
@@ -1563,7 +1584,7 @@ mod tests {
&community_id,
&owner,
AT,
)
))
.expect("seals");
assert!(matches!(
open_dissolved(&zeroed, &community_id),
+5 -1
View File
@@ -461,7 +461,11 @@ mod tests {
at_ms,
None,
);
relay.insert(seal_rumor(&rumor, &group, &author, false).expect("seals").0);
relay.insert(
smol::block_on(seal_rumor(&rumor, &group, &author, false))
.expect("seals")
.0,
);
}
let mut seen = BTreeSet::new();