update
This commit is contained in:
@@ -9,7 +9,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use crate::cord01::{self, NIP44_MAX_PLAINTEXT};
|
||||
use crate::cord05::{ChannelGrant, CommunityInvite};
|
||||
use crate::utils::{base64_to_hex32, base64url, canonical, hex32_to_base64, union};
|
||||
use crate::{ChannelId, CommunityId, Epoch, Extra};
|
||||
use crate::{ChannelId, CommunityId, Epoch, Extra, decode_hex_32};
|
||||
|
||||
pub const KIND_COMMUNITY_LIST: u16 = 33302;
|
||||
pub const MAX_MEMBERSHIPS: usize = 50;
|
||||
@@ -171,6 +171,70 @@ impl CommunityList {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RetainedRoot {
|
||||
pub epoch: Epoch,
|
||||
pub key: [u8; 32],
|
||||
pub control_pk: Option<PublicKey>,
|
||||
/// The npub whose Refounding minted this epoch.
|
||||
pub refounder: Option<PublicKey>,
|
||||
/// The epoch-seconds the superseding rotation published.
|
||||
pub retired_at: Option<u64>,
|
||||
}
|
||||
|
||||
impl JoinMaterial {
|
||||
/// The npub whose Refounding minted `root_epoch`, when the document names one.
|
||||
pub fn refounder(&self) -> Option<PublicKey> {
|
||||
self.extra.get("refounder").and_then(named_public_key)
|
||||
}
|
||||
|
||||
/// The prior root epochs the entry retains, with what the document knows of each.
|
||||
pub fn held_roots(&self) -> Vec<RetainedRoot> {
|
||||
let Some(roots) = self
|
||||
.extra
|
||||
.get("held_roots")
|
||||
.and_then(|roots| roots.as_array())
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut retained = Vec::with_capacity(roots.len());
|
||||
|
||||
for root in roots {
|
||||
let Some(fields) = root.as_object() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(epoch) = fields.get("epoch").and_then(|epoch| epoch.as_u64()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(key) = fields
|
||||
.get("key")
|
||||
.and_then(|key| key.as_str())
|
||||
.and_then(|key| decode_hex_32(&key.to_ascii_lowercase()).ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
retained.push(RetainedRoot {
|
||||
epoch: Epoch(epoch),
|
||||
key,
|
||||
control_pk: fields.get("control_pk").and_then(named_public_key),
|
||||
refounder: fields.get("refounder").and_then(named_public_key),
|
||||
retired_at: fields.get("retired_at").and_then(|at| at.as_u64()),
|
||||
});
|
||||
}
|
||||
|
||||
retained
|
||||
}
|
||||
}
|
||||
|
||||
/// A hex npub the document names, or nothing when the field is not one.
|
||||
fn named_public_key(value: &serde_json::Value) -> Option<PublicKey> {
|
||||
PublicKey::from_hex(value.as_str()?).ok()
|
||||
}
|
||||
|
||||
pub fn join_material(invite: &CommunityInvite, control_root: Option<&[u8; 32]>) -> JoinMaterial {
|
||||
JoinMaterial {
|
||||
community_id: invite.community_id,
|
||||
@@ -644,6 +708,8 @@ fn crypto_error(error: impl fmt::Display) -> ListError {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn id(byte: u8) -> CommunityId {
|
||||
@@ -704,6 +770,62 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A peer's refounder and retained roots ride in the unknown-field map —
|
||||
/// they are extensions, not §8 named fields — so the round trip has to keep
|
||||
/// them, and the accessors have to read them out of it.
|
||||
#[test]
|
||||
fn the_wire_round_trips_a_refounder_and_the_retained_roots() {
|
||||
let owner = Keys::generate().public_key();
|
||||
let refounder = Keys::generate().public_key();
|
||||
let mut current = material(id(0x11), owner, "Room", 3);
|
||||
|
||||
current
|
||||
.extra
|
||||
.insert("refounder".to_owned(), Value::String(refounder.to_hex()));
|
||||
current.extra.insert(
|
||||
"held_roots".to_owned(),
|
||||
json!([
|
||||
{
|
||||
"epoch": 2,
|
||||
"key": "77".repeat(32),
|
||||
"refounder": refounder.to_hex(),
|
||||
"retired_at": 1_700_000_000,
|
||||
},
|
||||
{ "epoch": 1, "key": "88".repeat(32) },
|
||||
{ "epoch": 4 },
|
||||
]),
|
||||
);
|
||||
|
||||
let wire = serde_json::to_string(&WireSnapshot::encode(¤t).expect("encodes"))
|
||||
.expect("serializes");
|
||||
let decoded = serde_json::from_str::<WireSnapshot>(&wire)
|
||||
.expect("parses")
|
||||
.decode(id(0x11))
|
||||
.expect("decodes");
|
||||
|
||||
assert_eq!(decoded.refounder(), Some(refounder));
|
||||
assert_eq!(
|
||||
decoded.held_roots(),
|
||||
vec![
|
||||
RetainedRoot {
|
||||
epoch: Epoch(2),
|
||||
key: [0x77; 32],
|
||||
control_pk: None,
|
||||
refounder: Some(refounder),
|
||||
retired_at: Some(1_700_000_000),
|
||||
},
|
||||
RetainedRoot {
|
||||
epoch: Epoch(1),
|
||||
key: [0x88; 32],
|
||||
control_pk: None,
|
||||
refounder: None,
|
||||
retired_at: None,
|
||||
},
|
||||
],
|
||||
"a root without a key is not a root"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_keeps_the_earlier_seed_and_the_later_current_either_way_round() {
|
||||
let owner = Keys::generate().public_key();
|
||||
|
||||
Reference in New Issue
Block a user