From f91604095e6f0978c61f66d0b937e36271637e6b Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 17:07:03 +0000 Subject: remake: no --follow-strict-remakes; a strict remake failing says who A strict remake takes a majority of members whose --strict-keys take its manifest's signer: no node votes for one without, so --follow-strict-remakes (1173) goes. A node that doesn't vote still switches once the others commit, as with any remake. So that an operator can tell why one fails: a strict remake aborted for want of a majority (the voting period over, or a majority out of reach) says in its reason (GET /admin/remakes/{id}, and the coordinator's log) which members didn't vote, and that a member votes for one only with its manifest signed by a key of its --strict-keys. Each of those logs why itself (`Not voting for a strict remake`). The sim a_strict_remake_too_few_take: only a takes the manifest; the remake aborts, naming b and c. --- src/simtest/policy.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) (limited to 'src/simtest/policy.rs') diff --git a/src/simtest/policy.rs b/src/simtest/policy.rs index 0d590d7..7e881e7 100644 --- a/src/simtest/policy.rs +++ b/src/simtest/policy.rs @@ -1012,3 +1012,69 @@ async fn a_swarm_turning_strict_through_a_remake() { assert!(crate::access::policy::of(&replica.doc).strict()); } } + +/// A strict remake only one member's `--strict-keys` take: no majority, aborted, its +/// reason naming the members that didn't vote, for the operator to tell why. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn a_strict_remake_too_few_take() { + use crate::access::{sshsig, strict}; + let key = ed25519_dalek::SigningKey::from_bytes(&[13; 32]); + let public = sshsig::PublicKey(key.verifying_key().to_bytes()); + let sim = Sim::with_identity(93); + let a = sim.start_with("a", &[], Start::Genesis, move |config| { + config.first_users = [("alice".to_owned(), Default::default())].into(); + config.strict_keys = vec![public]; + }); + let b = sim.start("b", &["a"], Start::Join); + let c = sim.start("c", &["a"], Start::Join); + sim.wait_ready(&[&a, &b, &c], SOON).await; + let nodes = [a.clone(), b.clone(), c.clone()]; + sim.wait_until("every node is registered", SOON, || { + let nodes = nodes.clone(); + async move { + for node in &nodes { + if crate::swarm::members::members(&node.doc.read().await.doc).len() < 3 { + return false; + } + } + true + } + }) + .await; + let text = { + let replica = a.doc.read().await; + let swarm = store::read_path(&replica.doc, &path("_continuum/swarm")) + .unwrap() + .unwrap(); + let policy = json!({"version": 1, "values": "signed"}); + let line = strict::manifest_of(&replica.doc, swarm.as_str().unwrap(), policy, "alice", 1); + let signed = sshsig::sign(&key, strict::NAMESPACE, line.as_bytes()); + format!("{line}\n{signed}") + }; + let (status, report) = crate::remake::testing::start( + a.clone(), + json!({"kind": "strict", "spec": {"manifest": text}, "settle": "3s"}), + None, + ) + .await; + assert!(status.is_success(), "proposal refused: {status} {report}"); + let id = report["id"].as_str().expect("remake id").to_owned(); + let (a_ref, id_ref) = (&a, id.as_str()); + sim.wait_until("the strict remake is aborted", SOON, move || async move { + crate::remake::testing::report(a_ref, id_ref) + .await + .is_some_and(|report| report["phase"] == "aborted") + }) + .await; + let report = crate::remake::testing::report(&a, &id).await.unwrap(); + let reason = report.to_string(); + assert!( + reason.contains("a strict remake, not voted for by"), + "{reason}" + ); + assert!( + reason.contains(b.local_id()) && reason.contains(c.local_id()), + "{reason}" + ); + assert_eq!(a.local_epoch().generation, 0); +} -- cgit v1.3.1