diff options
| -rw-r--r-- | src/access/rules.rs | 10 | ||||
| -rw-r--r-- | src/simtest/policy.rs | 33 | ||||
| -rw-r--r-- | src/state/host.rs | 11 | ||||
| -rw-r--r-- | src/swarm/refusals.rs | 4 |
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)) |
