aboutsummaryrefslogtreecommitdiffstats
path: root/src/remake/vote.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 18:17:14 +0000
committertv <tv@krebsco.de>2026-11-08 19:29:10 +0100
commit96ba15cf1a6f8725f70f429695a5a9661f706eb0 (patch)
tree36feecb42a8bfa5317f27a052266f74489312dfc /src/remake/vote.rs
parent4ce3bde36bb8283d82b914f0f73e30d89c935113 (diff)
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.
Diffstat (limited to 'src/remake/vote.rs')
-rw-r--r--src/remake/vote.rs21
1 files changed, 15 insertions, 6 deletions
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<H: Host>(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<H: Host>(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<H: Host>(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<H: Host>(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<H: Host>(
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<H: Host>(
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`.