From d14290ab6d0ea40b97cdb72f5e07354cc2721c45 Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 18:21:55 +0000 Subject: refusals: a big drop waits for a second member only where there is one Review 2, N8: a node with one neighbour (a swarm of two, a leaf) never hears a second member name the change: its big drop was held for good, an error logged at every REFUSALS. It now waits only where it has another neighbour than the one telling it; with one, one word is all there is. access.md also says what the wait is: it guards against one peer, not two members saying the same (any node with the swarm key registers), and never makes a node drop what its own rules take. --- docs/access.md | 10 ++++++---- src/state/host.rs | 15 +++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/access.md b/docs/access.md index 4400b91..03b70b8 100644 --- a/docs/access.md +++ b/docs/access.md @@ -302,10 +302,12 @@ everything built on it, its document becoming what it would be without them, and its own writes among them in the quarantine (`GET /admin/quarantine`), to be replayed or discarded, and the others' among the refused (`GET /admin/refused`, "taken unjudged, dropped"); what it writes from then on goes everywhere. A drop taking more -than 100 changes, or more than ten and a tenth of the document, waits until a second member says so too: -its own rules and one peer's word could both be off (logged as an error, counted in -`/admin/stats` as `sync.dropsHeld`). If its rules take the -change, the two policies disagree, which it logs. A change whose +than 100 changes, or more than ten and a tenth of the document, waits until a second +member says so too, where the node has another neighbour to hear it from: its own +rules and one peer's word could both be off (logged as an error, counted in +`/admin/stats` as `sync.dropsHeld`). It guards against one peer, not two members +saying the same; and never makes a node drop what its own rules take. If its rules +take the change, the two policies disagree, which it logs. A change whose dependencies haven't come yet waits for them. What it doesn't do yet: diff --git a/src/state/host.rs b/src/state/host.rs index ac36171..e7fc61a 100644 --- a/src/state/host.rs +++ b/src/state/host.rs @@ -99,14 +99,20 @@ impl AppState { /// so). Every peer syncs anew. Nothing while a rejoin runs, or 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). A drop taking much (`BIG_DROP`) waits for a - /// second member to say so (`peer` the first, `why` its rules' reason): its rules and - /// one peer's word might both be off, the swarm's not. + /// second member to say so (`peer` the first, `why` its rules' reason), where it has + /// another neighbour: its rules and one peer's word might both be off, the swarm's + /// not. Two members colluding pass it; it guards against one. pub(crate) async fn drop_taken(&self, root: ChangeHash, peer: &str, why: &str) { /// Changes a drop takes, at most, on one peer's word: more, or more than a tenth /// of the document (past `SMALL_DROP`), wait for another's. const BIG_DROP: usize = 100; const SMALL_DROP: usize = 10; let own = self.remake.own_actors(); + // Another member to hear it from: a node with one neighbour, the one telling, has + // only its word (a swarm of two, a leaf), and waiting would hold the drop for good. + let another = crate::swarm::topology::our_neighbours(self) + .iter() + .any(|other| other != peer); let state = self.clone(); let peer = peer.to_owned(); let why = why.to_owned(); @@ -136,8 +142,9 @@ impl AppState { error!(change = %root, "Dropping a change would leave nothing; kept it"); return None; } - if tainted.len() > BIG_DROP - || (tainted.len() > SMALL_DROP && tainted.len() * 10 > changes.len()) + if another + && (tainted.len() > BIG_DROP + || (tainted.len() > SMALL_DROP && tainted.len() * 10 > changes.len())) { use crate::locks::MutexExt; let mut screening = state.screening.lock_or_recover(); -- cgit v1.3.1