Skip to main content

syncweb_core/net/
membership_doc.rs

1use ed25519_dalek::SigningKey;
2use iroh_docs::{AuthorId, api::Doc};
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    Result, SyncwebError,
7    constants::{MEMBER_LIST_SIGNATURE_CONTEXT, NETWORK_MEMBERS_KEY},
8    node::{blob_store::BlobStore, docs_engine::DocsEngine},
9    parsing::current_unix_secs,
10};
11
12use super::network::Network;
13
14/// The canonical, owner-signed list of network members.
15#[derive(Clone, Debug, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct SignedMemberList {
18    pub network_id: String,
19    pub owner: String,
20    pub sequence: u64,
21    pub members: Vec<MemberEntry>,
22    pub updated_at: u64,
23    pub signature: String,
24}
25
26/// A single member entry in the member list.
27#[derive(Clone, Debug, Serialize, Deserialize)]
28#[non_exhaustive]
29pub struct MemberEntry {
30    pub key: String,
31    pub joined_at: u64,
32    pub role: MemberRole,
33}
34
35/// Role of a member in the network.
36#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
37#[non_exhaustive]
38pub enum MemberRole {
39    Admin,
40    Member,
41}
42
43impl SignedMemberList {
44    /// Create a new empty member list for the given network and owner.
45    #[must_use]
46    pub fn new(network_id: &str, owner: &str) -> Self {
47        Self {
48            network_id: network_id.to_owned(),
49            owner: owner.to_owned(),
50            sequence: 0,
51            members: Vec::new(),
52            updated_at: current_unix_secs(),
53            signature: String::new(),
54        }
55    }
56
57    /// Build the member list from a network's current membership.
58    ///
59    /// The owner is encoded as the hex of the node's ed25519 public key, which
60    /// matches what [`SignedMemberList::sign`] and [`SignedMemberList::verify`]
61    /// expect.
62    #[must_use]
63    pub fn from_network(network: &Network) -> Self {
64        let owner = hex::encode(network.owner.as_bytes());
65        let now = current_unix_secs();
66        let mut members = network
67            .members
68            .iter()
69            .map(|key| MemberEntry {
70                key: key.to_string(),
71                joined_at: now,
72                role: if *key == network.owner {
73                    MemberRole::Admin
74                } else {
75                    MemberRole::Member
76                },
77            })
78            .collect::<Vec<_>>();
79        members.sort_by(|left, right| left.key.cmp(&right.key));
80        Self {
81            network_id: network.id.to_string(),
82            owner,
83            sequence: 0,
84            members,
85            updated_at: now,
86            signature: String::new(),
87        }
88    }
89
90    /// Sign this member list with the given signing key.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if serialization fails.
95    pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
96        let owner_hex = crate::signature::public_key_hex(signing_key);
97        if self.owner != owner_hex {
98            return Err(SyncwebError::InvalidSignature(
99                "signing key does not match owner field".to_owned(),
100            ));
101        }
102        let message = self.serialize_unsigned()?;
103        let mut signed_bytes = Vec::new();
104        signed_bytes.extend_from_slice(MEMBER_LIST_SIGNATURE_CONTEXT);
105        signed_bytes.extend_from_slice(&message);
106        self.signature = crate::signature::sign_hex(signing_key, &signed_bytes);
107        Ok(())
108    }
109
110    /// Verify the signature on this member list.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the signature is missing, malformed, or invalid.
115    pub fn verify(&self) -> Result<()> {
116        let message = self.serialize_unsigned()?;
117        let mut signed_bytes = Vec::new();
118        signed_bytes.extend_from_slice(MEMBER_LIST_SIGNATURE_CONTEXT);
119        signed_bytes.extend_from_slice(&message);
120        crate::signature::verify_hex(&self.owner, &signed_bytes, &self.signature)
121    }
122
123    /// Serialize without the signature field.
124    fn serialize_unsigned(&self) -> Result<Vec<u8>> {
125        let mut unsigned = self.clone();
126        unsigned.signature = String::new();
127        serde_json::to_vec(&unsigned).map_err(|error| SyncwebError::operation("failed to serialize member list", error))
128    }
129}
130
131/// Network metadata stored in the membership doc.
132#[derive(Clone, Debug, Serialize, Deserialize)]
133#[non_exhaustive]
134pub struct NetworkInfo {
135    pub name: String,
136    pub label: String,
137    pub created_at: u64,
138}
139
140/// Sign and publish the current member list for a network into its membership
141/// doc under `sys/network/members`.
142///
143/// The writer is the network owner: `signing` must match `network.owner`.
144/// Returns the next list sequence number used.
145///
146/// # Errors
147///
148/// Returns an error if the previous list cannot be read, the list cannot be
149/// signed, or the entry cannot be written.
150pub async fn write_member_list(
151    docs: &DocsEngine,
152    blobs: &BlobStore,
153    doc: &Doc,
154    author: AuthorId,
155    signing: &SigningKey,
156    network: &Network,
157) -> Result<u64> {
158    let sequence = next_member_list_sequence(docs, blobs, doc).await?;
159    let mut list = SignedMemberList::from_network(network);
160    list.sequence = sequence;
161    list.sign(signing)?;
162    let bytes =
163        serde_json::to_vec(&list).map_err(|error| SyncwebError::operation("failed to serialize member list", error))?;
164    docs.set(doc, author, NETWORK_MEMBERS_KEY, bytes).await?;
165    Ok(sequence)
166}
167
168/// Return the sequence number to use for the next member list write.
169async fn next_member_list_sequence(docs: &DocsEngine, blobs: &BlobStore, doc: &Doc) -> Result<u64> {
170    let Some(entry) = docs.get_any(doc, NETWORK_MEMBERS_KEY).await? else {
171        return Ok(1);
172    };
173    let bytes = blobs.get(entry.content_hash()).await?;
174    let current: SignedMemberList = serde_json::from_slice(&bytes)
175        .map_err(|error| SyncwebError::operation("failed to deserialize member list", error))?;
176    Ok(current.sequence.saturating_add(1))
177}