aboutsummaryrefslogtreecommitdiffstats
path: root/src/remake
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
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')
-rw-r--r--src/remake/api.rs15
-rw-r--r--src/remake/vote.rs21
2 files changed, 27 insertions, 9 deletions
diff --git a/src/remake/api.rs b/src/remake/api.rs
index 79b551f..7aeb675 100644
--- a/src/remake/api.rs
+++ b/src/remake/api.rs
@@ -393,7 +393,13 @@ async fn start_kind<H: Host>(
.unwrap_or_default()
})
.await;
- if let Err(why) = super::vote::strict_taken(&host, prepared.content.as_deref(), &swarm) {
+ if let Err(why) = super::vote::strict_taken(
+ &host,
+ prepared.content.as_deref(),
+ &swarm,
+ &EpochRecord::from(host.current_epoch()),
+ request.base_heads.as_deref(),
+ ) {
return error_response(
StatusCode::FORBIDDEN,
format!("this node takes no such manifest: {why}"),
@@ -817,6 +823,7 @@ pub(crate) async fn strict_manifest<H: Host>(
) -> Response {
let at = host.now_millis().as_u64();
let caller = user.0.clone();
+ let epoch = EpochRecord::from(host.current_epoch());
let (manifest, values, heads) = host
.read_doc(move |doc| {
let read = |path: &[&str]| {
@@ -832,12 +839,14 @@ pub(crate) async fn strict_manifest<H: Host>(
_ => json!({"version": 1}),
};
policy["values"] = json!("signed");
- let manifest = crate::access::strict::manifest_of(doc, &swarm, policy, &caller, at);
+ let heads = doc.get_heads();
+ let manifest =
+ crate::access::strict::manifest_of(doc, &swarm, epoch, &heads, policy, &caller, at);
let values: Vec<JsonValue> = crate::access::strict::held(doc)
.into_iter()
.map(|(path, value)| json!({ "path": path, "value": value }))
.collect();
- let heads = records::encode_heads(&doc.get_heads());
+ let heads = records::encode_heads(&heads);
(manifest, values, heads)
})
.await;
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`.