From 96ba15cf1a6f8725f70f429695a5a9661f706eb0 Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 18:17:14 +0000 Subject: strict: a manifest serves one remake, at its base Review 2, N1: a strict manifest wasn't bound to its base. Any member could propose an old, legitimately signed one again at the current heads; nodes would verify it and vote, and every value at a signed place changed or added since (a ban, a new user, a rotated key, a changed userSigned or sealed declaration) would be deleted, nothing after the base being left to replay. The manifest now names the epoch and the heads it was made at, and a node takes it (proposing or voting) only for a remake of that epoch with exactly that base. GET /admin/strict-manifest makes it so; continuum-signed strict proposes at those heads already. --- src/remake/vote.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'src/remake/vote.rs') diff --git a/src/remake/vote.rs b/src/remake/vote.rs index 073fcda..7e678e6 100644 --- a/src/remake/vote.rs +++ b/src/remake/vote.rs @@ -294,7 +294,7 @@ pub(super) async fn cast(host: &H) { return None; } let content = record.proposal.content.clone(); - Some((id, content, record.proposal.kind.clone())) + Some((id, content, record.proposal.clone())) }); let swarm = store::read_path( doc, @@ -319,7 +319,7 @@ pub(super) async fn cast(host: &H) { } } } - let Some((chosen, content, kind)) = due else { + let Some((chosen, content, proposal)) = due else { return; }; // Only with the content in hand: a committed remake must have it on a majority. @@ -331,8 +331,14 @@ pub(super) async fn cast(host: &H) { None => None, }; // A strict remake: only with its manifest signed by a key this node was given. - if kind == "strict" - && let Err(why) = strict_taken(host, bytes.as_deref().map(Vec::as_slice), &swarm) + if proposal.kind == "strict" + && let Err(why) = strict_taken( + host, + bytes.as_deref().map(Vec::as_slice), + &swarm, + &proposal.from_epoch, + proposal.base_heads.as_deref(), + ) { if host.remake().refuses_strict(&chosen) { warn!(remake = %chosen, why = %why, "Not voting for a strict remake"); @@ -375,11 +381,14 @@ pub(super) async fn cast(host: &H) { } /// Whether this node takes a strict remake's manifest (`content`, for `swarm`): signed -/// by one of its `--strict-keys`. Why not, if not. +/// by one of its `--strict-keys`, for the remake's epoch and base (`epoch`, `base`). Why +/// not, if not. pub(super) fn strict_taken( host: &H, content: Option<&[u8]>, swarm: &str, + epoch: &EpochRecord, + base: Option<&[String]>, ) -> Result<(), String> { let text = content .and_then(|bytes| std::str::from_utf8(bytes).ok()) @@ -387,7 +396,7 @@ pub(super) fn strict_taken( if host.strict_keys().is_empty() { return Err("this node has no --strict-keys".to_owned()); } - crate::access::strict::verify(text, host.strict_keys(), swarm).map(|_| ()) + crate::access::strict::verify(text, host.strict_keys(), swarm)?.for_base(epoch, base) } /// A random duration below `limit`. -- cgit v1.3.1