From b758c93a1887ea36b0f52ccd4a6f9824de77d819 Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 09:33:41 +0000 Subject: 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). --- src/state/host.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src/state') 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 = kept .iter() .flat_map(|change| change.deps().iter().copied()) -- cgit v1.3.1