From 1f50adb6ab723425ce031b45ed0db80d404805f0 Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 22:41:06 +0000 Subject: signatures: a node signs at start what it kept unsigned when it stopped A node commits a change, keeps it in its document file, and may stop before any signature vouches for it. It starts again under a new actor and only ever signed its current actor's latest change: the old one stays unsigned for good. Peers pulling it then wait for a signature under a policy requiring them, and so does everything the node writes after, building on it. Signing every change at commit hid this under such a policy; signing what is sent, next, won't. At start, before anything leaves, a node signs each of its own changes at the document's heads that no signature vouches for: nothing it made since builds on them, so they are heads. --- src/document/signatures.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++ src/node.rs | 11 +++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/document/signatures.rs b/src/document/signatures.rs index 2f408d5..102cad2 100644 --- a/src/document/signatures.rs +++ b/src/document/signatures.rs @@ -443,6 +443,35 @@ impl Signatures { } } + /// This node's changes at `doc`'s heads that no signature vouches for, signed: + /// those it made and kept before it stopped, under an actor of an earlier run, and + /// never sent (it signs what it sends, as it sends it). Nothing it made since builds + /// on them, so they are heads; peers pulling them unsigned would wait for a + /// signature for good under a policy requiring them, and everything the node writes + /// after, building on them, with them. How many it signed. + pub(crate) fn sign_own_heads( + &mut self, + doc: &Document, + key: &crate::identity::NodeKey, + ) -> usize { + let node = key.id(); + let mut signed = 0; + for head in doc.get_heads() { + let Some(meta) = doc.get_change_meta_by_hash(&head) else { + continue; + }; + let own = meta + .author + .as_ref() + .is_some_and(|author| author.as_bytes() == node.as_bytes()); + if own && !self.verified(meta.actor.as_ref(), meta.seq) { + self.sign(doc, head, key); + signed += 1; + } + } + signed + } + /// Whether a valid signature of `hash` is kept: it vouches for its change and the /// changes of its actor it builds on. pub(crate) fn signed(&self, hash: &ChangeHash) -> bool { @@ -552,6 +581,26 @@ mod tests { .unwrap(); } + /// Changes of this node at the heads, of an earlier run, unsigned: signed at start; + /// another node's, and those vouched for, left. + #[test] + fn own_heads_signed_at_start() { + let key = NodeKey::from_seed([1; 32]); + let other = NodeKey::from_seed([2; 32]); + let mut doc = Document::new(); + write(&mut doc, &key.id(), &["a"], json!(1)); + // Another node's, concurrent: a head too. + let mut theirs = Document::new(); + write(&mut theirs, &other.id(), &["b"], json!(2)); + doc.merge(&mut theirs).unwrap(); + assert_eq!(doc.get_heads().len(), 2); + let mut signatures = Signatures::default(); + assert_eq!(signatures.sign_own_heads(&doc, &key), 1); + let ours = doc.get_last_local_change().unwrap(); + assert!(signatures.verified(ours.actor_id(), ours.seq())); + assert_eq!(signatures.sign_own_heads(&doc, &key), 0); + } + /// A signature verifies its actor's changes up to it, by the node its author names; /// another node's key, or another change, doesn't. #[test] diff --git a/src/node.rs b/src/node.rs index 91a9c62..314433c 100644 --- a/src/node.rs +++ b/src/node.rs @@ -126,6 +126,17 @@ impl Node { &replica.doc, config.initial_signatures, ); + // What this node kept unsigned when it stopped: signed before anything leaves. + if let Some(key) = &config.node_key { + let replica = &mut replica; + let signed = replica.signatures.sign_own_heads(&replica.doc, key); + if signed > 0 { + tracing::info!( + signed, + "Signed changes this node kept unsigned when it stopped" + ); + } + } let stamp = replica.stamps(); let state = AppState { doc: Arc::new(crate::document::lock::DocumentLock::new(replica)), -- cgit v1.3.1