aboutsummaryrefslogtreecommitdiffstats
path: root/src/state
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/state
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/state')
-rw-r--r--src/state/host.rs11
1 files changed, 9 insertions, 2 deletions
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())