peaveil
A privacy-first peer-discovery protocol based on probabilistic peer sampling, gossip, and metadata-resistant discovery.
peaveil does not answer "find node X". It answers
"give me enough good peers, all the time." Discovery is
not an operation; it is a permanent background activity.
What it is not
- Not a DHT. There is no deterministic key lookup, no XOR metric, no finger table, no logarithmic routing.
- Not a content-discovery protocol.
peaveildiscovers peers; layer a DHT on top if you need content addressing. - Not a global-coverage guarantee. The view is bounded and randomly sampled, so the probability of any particular peer being in your view at any particular moment is a function of the view size and the network size, not a guarantee.
How it works
peaveil is a Newscast / Cyclon / PPS-style
probabilistic-peer-sampling protocol with a four-pool
view. Every node keeps a small, locally-known view of the
network partitioned into:
- Bootstrap — well-known addresses seeded as network
entry points; sticky and excluded from the view cap.
peaveildoes not dial them (see below). - Trusted — peers that have been seen often enough in gossip; the stable core of the view.
- Recent — peers that have been seen recently but not
yet promoted to
Trusted; the transient part of the view. - Random — a tiny set of long-range exploration peers, sampled with a small bias to keep the discovery traffic from collapsing onto the trusted core.
On every tick the explorer:
- Re-classifies and evicts (emitting
PeerEvicted). - Picks a uniformly-random peer from the live connection
set —
peaveilgossips only over links you have already opened. - Draws a random subset of the view (including the self-entry, so the receiver learns about the sender).
- Hands the subset to
peashape's cover-traffic scheduler for shaping and transmission.
On receive, the sample is decoded (or, if it is a random cover frame, dropped), merged into the view, and the explorer is poked to consider responding on its next tick.
Who opens connections?
You do — never peaveil. Its scope is discoverability: it
maintains the view and gossips over the connections you have
opened, and it surfaces what it learns via Node::view /
Node::known_peers and the DiscoveryEvent stream. Deciding
who and when to connect to — bootstrap entry points and
discovered peers alike — is yours, via Node::connect /
Node::disconnect. That is the pea-stack philosophy: a
library does strictly what you cannot do for yourself, and
opening a socket is something you can already do.
Composition with peashape and peasub
peaveil is a building block in the pea stack:
+---------------------------------------------------+
| application |
+---------------------------------------------------+
| peaveil peasub (pub/sub) | <- discovery + pub/sub
+---------------------------------------------------+
| peashape (cover + shaping) | <- constant-size, constant-rate
+---------------------------------------------------+
| pea2pea (transport) | <- TCP
+---------------------------------------------------+
Every byte peaveil puts on the wire goes through
peashape's scheduler, so the on-the-wire timing
distribution and size distribution are independent of
whether the explorer is actively sampling or idle. An
observer cannot tell "this node is exchanging peer
samples right now" from "this node is doing nothing at
all."
peasub is the gossip / pub-sub layer of the same
family. It runs on the same peashape substrate but
spreads application-level messages rather than peer
samples; the two can be used side-by-side, sharing the
connection set and the cover-traffic budget.
Threat model
peaveil is designed to defeat a passive global network
observer who can:
- observe every byte sent between every pair of nodes;
- observe the timing of every byte;
- but cannot break the cryptographic primitives
protecting the link (e.g. TLS via a
pea2peaHandshake).
Against such an observer, the cover-traffic schedule
provided by peashape ensures that the timing
distribution and size distribution of a node's outbound
traffic are independent of whether the explorer is
sampling or idle. The observer learns nothing about the
existence, frequency, or destination of peaveil's
discovery activity beyond the cover rate the node has
been configured for.
peaveil does not attempt to defeat:
- an observer that can compromise the node itself;
- an observer that controls a non-trivial fraction of the network's nodes and can correlate views across them (the Sybil attack against any sampling protocol);
- traffic content analysis:
peaveildoes not encrypt the contents of a peer sample. A passive observer who can read the wire learns the full list of peers this node has been talking to. End-to-end confidentiality of the sample is the application's responsibility; layer it via apea2peaHandshake(e.g. Noise / TLS), or encrypt the payload before submitting it topeashape. The constant size, constant timing, and per-tick cover thatpeashapeprovides still defeat the "is this node exchanging samples right now?" question regardless of whether the payload is encrypted.
peaveil does not ship its own encryption, by design
— that is the peaverse philosophy: a library does
strictly only what it is designed to do, and any
additional property (encryption, authentication, etc.) is
the caller's responsibility to layer on.
Quick start
use Duration;
use ;
# async
Measurements
Every claim about peaveil's behaviour is measurable in a
local simulation. The peaveil::sim::Simulation type is a
self-contained harness that spawns a configured number of
nodes, wires them into a topology, drives the network
forward in time, and exposes:
| Metric | How to measure |
|---|---|
| convergence time | wall-clock seconds to a target coverage threshold |
| peer diversity | the per-category distribution of view sizes |
| resilience to churn | coverage recovery after Simulation::kill of a fraction |
| bootstrap latency | wall-clock seconds for a cold start to reach view_size/2 |
| partition recovery | coverage after Simulation::heal_partition |
| bandwidth overhead | frame_size * cover_rate per peer-pair |
| discovery stability | view-size variance over time once steady state is reached |
All random choices (sample target, bootstrap order,
churn victim selection) are driven by a seeded RNG, so
re-running the simulation with the same seed and
configuration produces metrics in the same bands. Exact
bit-for-bit equality across runs is not guaranteed
because the explorer ticks on real wall-clock time; the
OS scheduler is allowed to interleave the per-node
background tasks slightly differently across runs.
Pin the explorer's clock with tokio::time::pause() (and
swap tokio::time::interval for a virtual-clock ticker)
to get bit-exact determinism.
use ;
use Duration;
# async
Run the demo
Run the tests
Architecture
peaveil
├── src/
│ ├── lib.rs # public API
│ ├── config.rs # NodeConfig, CoverStrategy
│ ├── view.rs # the four-pool View
│ ├── sample.rs # PeerSample wire codec
│ ├── explorer.rs # background exploration task
│ ├── node.rs # public Node type
│ ├── events.rs # DiscoveryEvent broadcast
│ ├── error.rs # Error enum
│ ├── config_bridge.rs # NodeConfig -> ShapeConfig translation
│ └── sim.rs # Simulation harness
├── examples/
│ ├── two_nodes.rs # minimal end-to-end
│ └── simulation_metrics.rs # measurement run
└── tests/
└── discovery.rs # integration + simulation tests
License
MIT OR CC0-1.0.