aboutsummaryrefslogtreecommitdiffstats
path: root/src/simtest
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 17:07:03 +0000
committertv <tv@krebsco.de>2026-11-08 19:28:22 +0100
commitf91604095e6f0978c61f66d0b937e36271637e6b (patch)
treee05cd2a21dd4122c3a8a3a450b3605b5a094416b /src/simtest
parent5f69ecf6284a8061a2b8fae5b8f98d67a67dad5f (diff)
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.
Diffstat (limited to 'src/simtest')
-rw-r--r--src/simtest/mod.rs1
-rw-r--r--src/simtest/policy.rs66
2 files changed, 66 insertions, 1 deletions
diff --git a/src/simtest/mod.rs b/src/simtest/mod.rs
index 67ddaff..6921ed6 100644
--- a/src/simtest/mod.rs
+++ b/src/simtest/mod.rs
@@ -223,7 +223,6 @@ impl Sim {
challenge_lifetime: Duration::from_secs(300),
key_logins_per_user: 100,
strict_keys: Vec::new(),
- follow_strict_remakes: false,
cors_origins: Vec::new(),
files: Arc::new(Files::default()),
state_file: None,
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);
+}