From d322e7d4aa41d94232113cb7680cec1d65a11b0c Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:22 +0100 Subject: access: members refused as a whole With signatures required, a member whose own change broke a rule is refused as a whole: sent nothing, shown in /peers (refused), in stats (sync.refusedPeers) and on the dashboard, until a change of its is taken again or the epoch changes. Auth plan M6 (diverged standing). --- docs/access.md | 10 +++++++++- docs/dashboard.md | 2 +- docs/measurements.md | 2 ++ docs/openapi.yaml | 11 ++++++++++- 4 files changed, 22 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/access.md b/docs/access.md index 567cebd..31efe4e 100644 --- a/docs/access.md +++ b/docs/access.md @@ -238,7 +238,15 @@ change stays out, and so does whatever builds on it; the node logs it (`Refused change`) and lists the last thousand (`GET /admin/refused`). Offered again, as its writer's node keeps offering what is never taken, it is refused by its hash, not judged or logged again, and a peer offering nothing else is answered later each time, up to a -minute apart. A change whose +minute apart. + +With signatures required, a member whose own change broke a rule (its signature proves +it its own) is refused as a whole: every node sends it nothing more, as all it commits +from then on builds on what was refused, and logs it once. It shows in `GET /peers` +(`refused`, with the change and why), in `/admin/stats` (`sync.refusedPeers`) and on the +dashboard. A node that only passed a refused change on isn't: the committer is, whoever +brought it. It lasts until a change of the member's is taken again (it rejoined and +writes anew), or the epoch changes; forgetting or banning it is the operators'. A change whose dependencies haven't come yet waits for them. What it doesn't do yet: diff --git a/docs/dashboard.md b/docs/dashboard.md index df2d4f9..271592a 100644 --- a/docs/dashboard.md +++ b/docs/dashboard.md @@ -247,7 +247,7 @@ them: no HELLOs, keepalives or syncing of its own; their tooltip says so), with: | Column | Meaning | |---|---| | peer | Alias, or a shortened node ID (the full one in the tooltip). | -| status | `active`, `dormant` (for how long: in the tooltip) or `unknown` (not heard from since this node started). | +| status | `active`, `dormant` (for how long: in the tooltip) or `unknown` (not heard from since this node started); `refused`, above all, where this node refuses it as a whole, its own change having broken the swarm's rules (the change and why in the tooltip; [`access.md`](access.md)). | | seen | How long ago this node last heard from it. | | via | The address this node sends to it at, and whether `direct` or `relayed`: as much as the box's width leaves, cut short with an ellipsis; all of it in the tooltip. | | rtt | The round trip on that path. | diff --git a/docs/measurements.md b/docs/measurements.md index 4e1099f..8e36a7e 100644 --- a/docs/measurements.md +++ b/docs/measurements.md @@ -78,6 +78,7 @@ or caught up over many small messages. |---|---|---| | `sync.lastFromNeighbourSecs` | `last_sync_from_neighbour_seconds` | Since a sync message from any neighbour was applied. Growing without bound while others write: the node is cut off from the swarm's changes. | | `sync.divergedPeers` | `diverged_peers` | Peers whose heads differ from ours with no sync message in between. | +| `sync.refusedPeers` | `refused_peers` | Peers refused as a whole, their own changes having broken the swarm's rules ([`access.md`](access.md)): sent nothing. | | `sync.largestReceivedBytes` | `largest_sync_received_bytes` | The largest sync message received since start. Larger transfers come as deltas (blobs of 16 chunks or more, [`peers.md`](peers.md)), so this stays below 16 × `--blob-chunk` from nodes of this version. | | `relay.announceRounds` | `announce_rounds_total` | Announcement rounds: our new heads to the neighbours after applying their changes (the quick round to those writers' trees leave out, or to all for changes no tree carries, and the slow round to the others; [`peers.md`](peers.md)). | | `relay.treelessRounds` | `treeless_rounds_total` | Of those, quick rounds to all neighbours: some change came that no writer's tree carries (pulled, a delta, from a node with no part in its tree). Many, with every node on the member list: trees miss often. | @@ -281,6 +282,7 @@ In `/peers`, and as `peer_…` series with a `peer` label: | `destination` | `peer_destination_info{transport,proven,connection}` | Where messages to it go: the address (its transport), whether it proved itself there (else sends wait for its proof), and the connection there: libp2p's `direct` or `relayed`, Tor's `direct` (an onion circuit); none on Mycelium; while `relayed` after a failed hole punch, `punching`: punches failed in a row (`failed`) and when it's tried again (`nextInSecs`). | | `destination.paths[].rttMs`, `.down`, `.connection` | `peer_path_rtt_seconds{transport}`, `peer_path_down{transport}` | Every path to it, best first: its round-trip time (smoothed, from PINGs), whether it is down, and the transport's connection there (`direct` or `relayed`, where it has connections). The first is the one in use: the peer's round-trip time. | | `lastSyncSecsAgo` | `peer_last_sync_seconds` | Since a sync message from it was applied (or the epoch changed). | +| `refused` | | Refused as a whole, its own change having broken the swarm's rules ([`access.md`](access.md)): since when (`secsAgo`), the change, and why. | | `divergedForSecs` | `peer_diverged_seconds` | How long its heads have differed from ours with no sync message from it in between, nor both moving on; absent while they match. Long values precede a healing re-handshake ([`peers.md`](peers.md)). | | `syncThroughput` | `peer_sync_throughput_bytes_per_second` | Sync bytes per second over exchanges of at least 32 KiB, round trip taken off, smoothed; and over messages of at least 32 KiB we awaited nothing for (taken as delivered: no answer to time), and blob chunks (deltas), from how long they took to come (`hopDelay`), half the round trip taken off. Absent before the first large exchange; comes with its age (`secsAgo`) since large exchanges are rare. This is the rate the peer delivers at in practice (its apply time included), not the path's capacity. | | `clock.offsetMs`, `.withinMs` | `peer_clock_offset_seconds`, `peer_clock_offset_within_seconds` | How far its clock is ahead of ours (behind if negative), give or take. Each PONG says when the peer answered, by its clock, which lies between our PING going and the PONG coming: within half the round trip of its middle, however unevenly the two ways took. Of its paths and the last 5 minutes, the narrowest bound is kept; a sample it contradicts (a clock set anew) replaces it at once. Absent before a PONG from it. | diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 7c8bb84..832d941 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -2175,6 +2175,13 @@ components: divergedForSecs: type: [integer, 'null'] description: How long its heads have differed from ours with no sync message from it in between; null while they match. + refused: + type: [object, 'null'] + description: "Refused as a whole, its own change having broken the swarm's rules (`docs/access.md`): sent nothing. Null if not." + properties: + secsAgo: { type: integer } + change: { type: string, description: The change of its that broke the rules. } + why: { type: string } hasPendingDelivery: type: boolean epoch: @@ -2568,12 +2575,14 @@ components: type: integer sync: type: object - required: [divergedPeers, largestReceivedBytes] + required: [divergedPeers, refusedPeers, largestReceivedBytes] properties: lastFromNeighbourSecs: type: [integer, 'null'] divergedPeers: type: integer + refusedPeers: + type: integer largestReceivedBytes: type: integer relay: -- cgit v1.3.1