aboutsummaryrefslogtreecommitdiffstats
path: root/src/simtest
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 09:33:41 +0000
committertv <tv@krebsco.de>2026-11-08 10:35:31 +0100
commitb758c93a1887ea36b0f52ccd4a6f9824de77d819 (patch)
tree96f7f4ca3007e393b185739a66b0474323b50872 /src/simtest
parentaf3869df9ff3a0266857bd7c7534b75b36d04aa5 (diff)
access: an epoch's root is never dropped
A REFUSALS naming the swarm's first change made the node receiving it judge that change again in a document that isn't empty, refuse it as "a change without dependencies that isn't the epoch's root", and drop it with everything built on it: its whole document. The first epoch's root is now the first change a document holds, as it comes to an empty one or as it is in one; a change judged again on a peer's word is never refused for having no dependencies; and drop_taken refuses to drop an epoch's root, or anything that would leave nothing kept, logging an error instead. Review finding 1 (critical).
Diffstat (limited to 'src/simtest')
-rw-r--r--src/simtest/policy.rs33
1 files changed, 33 insertions, 0 deletions
diff --git a/src/simtest/policy.rs b/src/simtest/policy.rs
index 68f394f..da046bb 100644
--- a/src/simtest/policy.rs
+++ b/src/simtest/policy.rs
@@ -580,3 +580,36 @@ async fn a_policy_this_node_doesnt_know() {
"a write under a policy this node doesn't know"
);
}
+
+/// A REFUSALS naming the swarm's first change, as a member could send: the node
+/// receiving it keeps its document, as no rule refuses an epoch's root.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn a_refusal_naming_the_root() {
+ let sim = Sim::with_identity(78);
+ let a = sim.start_with("a", &[], Start::Genesis, |config| {
+ config.first_users = [("alice".to_owned(), Default::default())].into();
+ });
+ let b = sim.start("b", &["a"], Start::Join);
+ sim.wait_ready(&[&a, &b], SOON).await;
+ write_as(
+ &a,
+ "alice",
+ "_continuum/access/policy",
+ json!({"version": 1}),
+ )
+ .await;
+ write_as(&a, "alice", "notes/x", json!(1)).await;
+ sim.wait_converged(&[&a, &b], SOON).await;
+ let (root, epoch, epoch_seq) = {
+ let replica = b.doc.read().await;
+ (
+ replica.doc.get_changes(&[])[0].hash(),
+ replica.epoch,
+ replica.epoch_seq,
+ )
+ };
+ crate::swarm::refusals::tell(&a, b.local_id(), epoch, epoch_seq, vec![root]).await;
+ tokio::time::sleep(Duration::from_secs(3)).await;
+ assert!(holds(&b, "notes/x").await, "b dropped its document");
+ assert_eq!(b.remake.list_quarantine(|entry| entry.id.clone()).len(), 0);
+}