Initial release: v0.1.0
Passphrase-based peer rendezvous for iroh-gossip swarms over the public Mainline DHT. Two nodes that share a passphrase + application salt find each other and join the same gossip topic — no bootstrap server, no prior knowledge of peer addresses. The algorithm is a clock-free variant of HyParView-style partition healing on BEP 44 mutable items, with K-sharded slots, logical aging with random-tie eviction, vouching of up to V neighbors at age 1, and a two-layer AEAD record envelope. Full spec in PROTOCOL.md. Ships with: * 119 tests (unit / property / integration / doctest) all green * 100% mutation-testing catch rate (cargo-mutants) * cargo-fuzz harnesses for the parse surface * Kani proofs of no-UB / no-panic on crypto::epoch_of and wire::decode_entries across their full symbolic input spaces * Stateright distributed-system model: safety invariants S1 + S2 proved exhaustively for N ≤ 6, K ≤ 3; liveness witness under weak fairness * Miri green on the synchronous modules * cargo-deny + cargo-audit clean * criterion benches * Full-matrix CI (Linux + macOS + Windows × stable + MSRV 1.93.0) * Dedicated nightly lane for Miri / mutation / fuzz / Kani / Stateright Pinned iroh 0.98.x. License MIT. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
54 files changed
+16897
No files matched your search
+130
@@ -0,0 +1,130 @@
|
||||
# Architecture
|
||||
|
||||
A map of where each piece of `iroh-gossip-rendezvous` lives. For the
|
||||
algorithm itself see [`PROTOCOL.md`](PROTOCOL.md).
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Caller["Rendezvous::join(passphrase, app_salt)"]
|
||||
Builder[Builder]
|
||||
Keys[keys: HKDF per-slot]
|
||||
GossipGlue["gossip_glue<br/>(iroh Endpoint + gossip actor +<br/>event fan-out)"]
|
||||
ProtocolLoops["protocol<br/>HealLoop + WriteLoop"]
|
||||
Merge[merge]
|
||||
Wire[wire]
|
||||
Crypto[crypto<br/>seal / open]
|
||||
DHT[("DHT backend<br/>(DhtSlots trait)")]
|
||||
Mainline[dht::mainline]
|
||||
InMemory[dht::memory]
|
||||
Rendezvous[Rendezvous handle]
|
||||
|
||||
Caller --> Builder --> Keys
|
||||
Builder --> GossipGlue
|
||||
Builder --> ProtocolLoops
|
||||
Builder --> Rendezvous
|
||||
ProtocolLoops --> Merge
|
||||
ProtocolLoops --> Wire
|
||||
ProtocolLoops --> Crypto
|
||||
ProtocolLoops --> DHT
|
||||
DHT --> Mainline
|
||||
DHT --> InMemory
|
||||
Rendezvous -.holds.-> GossipGlue
|
||||
Rendezvous -.holds.-> ProtocolLoops
|
||||
```
|
||||
|
||||
## Module responsibilities
|
||||
|
||||
| Module | Pure? | Async? | What it does |
|
||||
|---|---|---|---|
|
||||
| `keys` | ✓ | ✗ | HKDF-SHA256 derivation of topic id + per-slot Ed25519 keypairs + wrapper master keys |
|
||||
| `wire` | ✓ | ✗ | Entry list `(id, age)` encode/decode, byte-budget checks |
|
||||
| `crypto` | ✓ | ✗ | Two-layer AEAD (ChaCha20-Poly1305) with per-epoch wrapper ratchet |
|
||||
| `merge` | ✓ | ✗ | Record merge: age-up, vouch, evict by max-age with random tie-break |
|
||||
| `dht/mod` | ✓ | ✓ | `DhtSlots` trait + types (`SlotKey`, `SlotRecord`, `DhtError`) |
|
||||
| `dht/memory` | ✓ | ✓ | In-memory `DhtSlots` — tests + simulator + `test-support` |
|
||||
| `dht/mainline` | — | ✓ | Production `DhtSlots` over the public Mainline DHT (BEP 44) |
|
||||
| `protocol` | — | ✓ | `initial_join`, `spawn_loops` (HealLoop + WriteLoop), `ProtoState`, `GossipView` trait |
|
||||
| `gossip_glue` | — | ✓ | iroh `Endpoint` build, gossip subscription, `Event` fan-out via `tokio::broadcast` |
|
||||
| `rendezvous` | — | ✓ | Public `Rendezvous` handle; owns the `JoinSet` of background tasks and the `CancellationToken` |
|
||||
| `builder` | — | ✓ | Public `Builder` with parameter validation + defaults (§2 of PROTOCOL.md) |
|
||||
| `state` | ✓ | ✗ | Observable `RendezvousState` + `DhtStatus` enum |
|
||||
| `error` | ✓ | ✗ | `thiserror` Error enum |
|
||||
| `sim_impl` | — | ✓ | Discrete-event simulator (feature-gated `sim`); re-exported as `crate::sim` |
|
||||
|
||||
"Pure" = no I/O, no tokio, Miri-compatible.
|
||||
|
||||
## Lifetime invariants
|
||||
|
||||
- `Rendezvous` is the **keep-alive anchor**. It owns the `CancellationToken`
|
||||
and the `JoinSet` of all background tasks. Drop it, and the DHT
|
||||
maintenance loops cancel.
|
||||
- Any `GossipSender` or `broadcast::Receiver<Event>` extracted from a
|
||||
`Rendezvous` must be used only while the `Rendezvous` is alive. After
|
||||
drop, the internal event fan-out task terminates and new subscribers
|
||||
receive `RecvError::Closed`.
|
||||
- The iroh `Endpoint` is closed during `Rendezvous::shutdown`. Callers who
|
||||
need the endpoint past that point should construct it themselves and
|
||||
inject it via `Builder::endpoint`.
|
||||
|
||||
## Background tasks
|
||||
|
||||
Started by `Builder::build`, stored in two `JoinSet<()>`s inside
|
||||
`Rendezvous`:
|
||||
|
||||
1. **Gossip** (`gossip_glue::build`):
|
||||
- `spawn_accept_loop` — handles incoming iroh connections for the
|
||||
gossip ALPN.
|
||||
- `spawn_event_task` — drains the iroh-gossip `GossipReceiver`,
|
||||
updates the `RendezvousState.neighbor_count`, and broadcasts every
|
||||
`Event` to caller-side subscribers.
|
||||
2. **Protocol** (`protocol::spawn_loops`):
|
||||
- HealLoop — every `T_h · jitter`, read `s = max(1, K − |N|)` random
|
||||
slots and hand discovered peer IDs to `GossipView::join_peers`.
|
||||
- WriteLoop — first iteration publishes immediately (completes §5 Join
|
||||
from the spec); thereafter sleeps `T_w · jitter` and writes with
|
||||
probability `1/(|N|+1)`.
|
||||
|
||||
Both loops cancel on `Rendezvous::shutdown` or on drop.
|
||||
|
||||
## DHT abstraction
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
trait DhtSlots: Send + Sync + 'static {
|
||||
async fn read(&self, slot: SlotKey) -> Result<Option<SlotRecord>, DhtError>;
|
||||
async fn write(&self, slot: SlotKey, record: SlotRecord) -> Result<(), DhtError>;
|
||||
}
|
||||
```
|
||||
|
||||
Two impls:
|
||||
|
||||
- `dht::mainline::MainlineDht` — BEP 44 puts/gets over the public Mainline
|
||||
DHT via the [`mainline`] crate. Re-signs via `MutableItem::new` so
|
||||
signatures match the BEP 44 canonical form remote nodes verify against.
|
||||
- `dht::memory::InMemoryDht` — process-local `HashMap<SlotKey, SlotRecord>`,
|
||||
signature-verifying, LWW at equal seq. Used by tests, simulator, and the
|
||||
`test-support` feature's `Builder::dht_backend` injection.
|
||||
|
||||
The trait is `pub(crate)` by default; the `sim` feature exposes it publicly
|
||||
for third-party DHT backends.
|
||||
|
||||
[`mainline`]: https://docs.rs/mainline
|
||||
|
||||
## Feature flags
|
||||
|
||||
See [README](README.md#features) for the user-facing table. Implementation
|
||||
notes:
|
||||
|
||||
- `sim` exposes `crate::sim::*` from `sim_impl`; re-exports `InMemoryDht`,
|
||||
`DhtSlots`, `ProtoState`, and merge/wire helpers.
|
||||
- `kani` adds `#[kani::proof]` functions to `src/kani_proofs.rs` (invisible
|
||||
to regular builds).
|
||||
- `miri-compat` adds `#[cfg_attr(miri, ignore)]` on tests that touch tokio
|
||||
I/O or real DHT.
|
||||
- `mainline-live` enables live-network integration tests, run only in the
|
||||
nightly CI lane.
|
||||
- `test-support` (implies `sim`) adds `Builder::dht_backend(Arc<dyn DhtSlots>)`
|
||||
so integration tests can run multi-node scenarios against `InMemoryDht`
|
||||
without a real DHT.
|
||||
Reference in new issue
Block a user