aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/access/rules.rs10
-rw-r--r--src/simtest/policy.rs33
-rw-r--r--src/state/host.rs11
-rw-r--r--src/swarm/refusals.rs4
4 files changed, 54 insertions, 4 deletions
diff --git a/src/access/rules.rs b/src/access/rules.rs
index f285328..2aed4b8 100644
--- a/src/access/rules.rs
+++ b/src/access/rules.rs
@@ -33,7 +33,7 @@ use crate::document::at::At;
use crate::document::inspect::{Action, Op, Path};
use crate::document::meta;
use crate::document::replica::Epoch;
-use crate::document::{ChangeHash, Document, store};
+use crate::document::{ChangeHash, Document, ReadDoc, store};
/// A change to judge, as its bytes say.
#[derive(Debug, Clone)]
@@ -75,8 +75,10 @@ impl std::fmt::Display for Refusal {
/// Judges `change`, about to be applied to `doc` (of `epoch`).
pub(crate) fn judge(doc: &Document, epoch: Epoch, change: &Change<'_>) -> Result<(), Refusal> {
if change.deps.is_empty() {
+ // The first epoch's: the first change a document holds, as it comes to an empty
+ // one, or as it is in one (judged again, `swarm::refusals`).
let root = if epoch.generation == 0 {
- doc.get_heads().is_empty()
+ doc.get_heads().is_empty() || doc.get_change_by_hash(&change.hash).is_some()
} else {
change.hash.0 == epoch.id
};
@@ -720,6 +722,10 @@ mod tests {
};
assert!(root(&Document::new(), Epoch::GENESIS, [1; 32]).is_ok());
assert!(refused(root(&swarm(false), Epoch::GENESIS, [1; 32])));
+ // The first epoch's, judged again in a document that holds it: its root.
+ let held = swarm(false);
+ let first = held.get_changes(&[])[0].hash();
+ assert!(root(&held, Epoch::GENESIS, first.0).is_ok());
let epoch = Epoch {
generation: 3,
id: [5; 32],
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);
+}
diff --git a/src/state/host.rs b/src/state/host.rs
index d23e07d..3210ee2 100644
--- a/src/state/host.rs
+++ b/src/state/host.rs
@@ -95,7 +95,8 @@ impl AppState {
/// without them (forked where none is), written by a fresh actor of this node's, as
/// the old one's later changes are among those dropped; this node's own writes among
/// them go to the quarantine. Every peer syncs anew. Nothing while a rejoin runs, or
- /// if the document doesn't hold `root`.
+ /// if the document doesn't hold `root`; nor if `root` is an epoch's root, or nothing
+ /// would be kept, which no rule can mean (logged as an error).
pub(crate) async fn drop_taken(&self, root: ChangeHash) {
let own = self.remake.own_actors();
let state = self.clone();
@@ -105,7 +106,9 @@ impl AppState {
return None;
}
let changes = replica.doc.get_changes(&[]);
- if !changes.iter().any(|change| change.hash() == root) {
+ let dropped = changes.iter().find(|change| change.hash() == root)?;
+ if dropped.deps().is_empty() {
+ error!(change = %root, "Asked to drop the epoch's root; kept it");
return None;
}
// In causal order: a change after those it builds on.
@@ -119,6 +122,10 @@ impl AppState {
.iter()
.filter(|change| !tainted.contains(&change.hash()))
.collect();
+ if kept.is_empty() {
+ error!(change = %root, "Dropping a change would leave nothing; kept it");
+ return None;
+ }
let built_on: std::collections::HashSet<ChangeHash> = kept
.iter()
.flat_map(|change| change.deps().iter().copied())
diff --git a/src/swarm/refusals.rs b/src/swarm/refusals.rs
index 2e5399a..fd857fa 100644
--- a/src/swarm/refusals.rs
+++ b/src/swarm/refusals.rs
@@ -99,6 +99,10 @@ fn judge_held(
hash: &ChangeHash,
) -> Option<Result<(), String>> {
let change = doc.get_change_by_hash(hash)?;
+ // An epoch's root is what the document is: never refused, whatever a peer says.
+ if change.deps().is_empty() {
+ return Some(Ok(()));
+ }
let decoded = change.decode();
let ops = crate::document::inspect::Inspector::new(doc)
.ops(std::slice::from_ref(&decoded))