mergedhumanPR #148Registry
bogacsmz: byzantine-resistant gossip registry — signed-equivocation quarantine + eclipse resistance
Persona for this review: trust/honesty auditor. I built the adversarial
Author
@bogacsmz
github profile →- Status
- Merged
- Merged on
- Jul 10
- Branch
- hackathon/bogacsmz-registry-byzantine-gossip
Description
The pitch.
# feat(registry): byzantine-resistant gossip -- signed cards, signed-equivocation quarantine, eclipse-resistant sampling **Persona for this review: trust/honesty auditor.** I built the adversarial validators *before* trusting my own plugin's guarantees, and I am reporting every limitation I found while building it, not just the attacks it catches. If you are reviewing this for a security-sensitive merge, read [`VERIFICATION.md`](nest_plugins_reference/registry/VERIFICATION.md) in full, not just this summary. ## Headline: a validly-signed equivocator still poisons the network The other prior art in this space (`#67`, registration-only card signing) answers "did the claimed publisher actually sign this card?" -- necessary, but not sufficient. It does not answer, and *cannot* answer by construction: "did this publisher sign **two different, individually valid** cards at the same version?" Both cards pass every check a registration-time signature can run, in isolation. `#67` would accept either one without complaint. The only way to catch it is to compare a publisher's writes to each other, not to verify a signature against itself -- that is the invariant this PR adds (`scenarios/gossip_signed_equivocation.yaml`, the novelty proof scenario). This defense is **mesh-wide, not topology-bounded**: even an equivocator that never sends its two conflicting cards to any common recipient -- delivering card1 only to one group of peers and card2 only to a disjoint group, so no single node ever directly witnesses both -- is still caught at **every** honest node. Getting this genuinely mesh-wide took two rounds of work, and the honest story is in [`VERIFICATION.md`](nest_plugins_reference/registry/VERIFICATION.md#honest-limitations-every-one-gathered-across-the-build) (limitation 4). A first fix carried a per-entry **content hash** in the `OP_DIGEST` payload so `_compute_missing` hands over a same-version-but-different-content entry rather than treating the peer as "in sync" -- enough for N=2 / a single equivocator (`test_disjoint_delivery_equivocation_is_caught`). But that is **not sufficient** at N>2 honest nodes with multiple equivocators: the instant a node witnesses the conflict it evicts the card, dropping it from its digest, so it stops relaying the conflicting copy -- "eviction halts relay" -- and a node that only ever saw one side, whose neighbors holding the other side all evict first, strands permanently (reproduced at N=10 / five equivocators, `test_disjoint_multi_equivocator_no_stranding`). The real fix gossips the equivocation **proof** -- the two conflicting, individually-signature-valid writes for `(E, v)` -- independently of card eviction: the proof survives eviction (`self._equivocation_proofs`), rides anti-entropy as its own `OP_EQPROOF` wire item advertised via a known-byzantine digest section, and is **independently re-verified on receipt** (both signatures + same-`(E, v)`-different-hash, `_verify_proof`/`_ingest_proof`) so a relay cannot forge one to frame an honest publisher. A stranded node therefore learns `E` is byzantine from any honest neighbor, and every honest node converges on quarantining and evicting `E` regardless of delivery topology (`test_disjoint_multi_equivocator_no_stranding`, `test_fabricated_equivocation_proof_does_not_frame_honest_publisher`). The one residual honest condition: the honest sub-network must be connected (not partitioned) for the proof to reach everyone, and convergence is eventual (a transient in-flight window), never instantaneous. ## Threat model `gossip` (`#24`, merged) gives eventually-consistent discovery under honest-but-partitioned failures: every agent gossips its local view, and the simulator's partition logic naturally blocks cross-partition propagation. It assumes every participant plays by the rules. This PR removes that assumption on three specific, named fronts: | # | Attack | `gossip`'s exposure | This plugin's defense | |---|---|---|---| | 1 | Forged/impersonated …
Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/148/head:pr-148
git checkout pr-148