Milestone 6: DiscoveryProvider seam, signed announcements, redoal sketch

The discovery module defines the seam: DiscoveryProvider (subscribe/
announce over 32-byte TopicKeys) and a signed Announcement carrying
root hash, ed25519 author, metadata and provider addresses. Signatures
cover a deterministic postcard encoding including the topic (no cross-
channel replay) and the provider identities (addresses stay refreshable
hints). LanDiscovery conforms to the trait — mdns sightings become
locally-authored announcements, one per pinned root — and the daemon's
auto-sync now runs entirely through it: verify, index unconditionally,
fetch only for trusted authors from allowlisted providers on already-
pinned incomplete roots. The milestone-4 real-mdns sync test passes
unchanged through the new path. Verification unit tests cover round
trip, tampered root, wrong topic, forged author, mismatched signing
key, and serde survival. docs/redoal-integration.md sketches the
gesture-topic gossip provider against this contract.

Also: fix a flaky hang in the socket-activation test (dup2(3,3) leaves
CLOEXEC set when the listener already sits on fd 3; parent's listener
copy masked daemon death), and give the test client a read-timeout hang
guard. Add a top-level README.

Dependencies: ed25519-dalek (Signature type; same implementation iroh
keys use), postcard (deterministic signed encoding, iroh's canonical
compact codec).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:51:57 +02:00
parent 871280553e
commit 258fa072aa
11 changed files with 617 additions and 13 deletions
+37
View File
@@ -0,0 +1,37 @@
# Milestone 6 — Discovery provider seam
## What landed
- `varde_daemon::discovery` with the spec's `DiscoveryProvider` trait,
`TopicKey`, and the signed `Announcement` type. Signatures are ed25519
(the same key type as iroh NodeIds) over a deterministic postcard
encoding that includes the topic, so announcements can't be replayed
onto other channels. Verification tests cover round trip, tampered
root, wrong topic, forged author, mismatched signing key, and
serde-then-verify.
- `LanDiscovery` implements the trait: mDNS sightings become
locally-authored, signed announcements ("peer N may hold pinned root
R"), one per pinned root. `announce()` is a no-op on the LAN — mDNS
already advertises presence, and varde never broadcasts content lists.
- The daemon's auto-sync was refactored *onto* the seam: sightings feed
presence tracking and the LanDiscovery channel; a single
`on_announcement` path verifies, indexes, and applies the consent
tiers (trusted author → pinned + incomplete → trusted providers →
fetch). The milestone-4 mdns integration test passes unchanged through
the new path, which is the proof the seam carries real traffic.
- Announcements surface on the event stream (`announcement` events) and
are indexed in memory per root.
- `docs/redoal-integration.md` sketches the gesture-topic gossip
provider against this contract, including the one extension it will
need (topic-scoped trust) and why nothing else changes.
## Deviations from the spec sketch
- The trait deals in `SignedAnnouncement` rather than bare
`Announcement`: the spec says announcements are signed, so the wire
type carries its signature and the subscriber can't forget to check.
- `Announcement.author` is an iroh `PublicKey` (ed25519, as specified);
provider addresses are excluded from the signed payload (they're
refreshable hints) while provider identities are covered.
- Signature bytes travel as a length-checked `Vec<u8>` because serde
cannot derive for `[u8; 64]`.
+93
View File
@@ -0,0 +1,93 @@
# redoal integration sketch
How a gesture-derived discovery provider plugs into varde without
touching the daemon. Nothing here is implemented; this documents the
contract the v1 seam already enforces.
## The seam
`varde_daemon::discovery` defines:
```rust
pub trait DiscoveryProvider: Send + Sync {
fn subscribe(&self, topic: TopicKey) -> BoxStream<SignedAnnouncement>;
fn announce(&self, topic: TopicKey, ann: SignedAnnouncement) -> Result<()>;
}
```
- `TopicKey` is an opaque 32-byte channel id.
- `Announcement { root, author, meta, providers }` is signed (ed25519,
the same key type as iroh NodeIds) over a deterministic postcard
encoding that *includes the topic*, so announcements can't be replayed
across channels. Provider socket addresses are excluded from the
signature (relays may refresh them); provider *identities* are covered.
The daemon consumes any provider identically (`Daemon::on_announcement`):
1. verify the signature — drop on failure;
2. index the announcement (all of them — discovery is the open tier);
3. auto-fetch only when **all** of these hold:
- the author key is trusted (or is the local daemon),
- the root is already pinned locally and incomplete,
- at least one announced provider is on the peer allowlist.
v1 ships one implementation, `LanDiscovery`: mDNS sightings are turned
into locally-authored announcements ("peer N may hold pinned root R").
It proves the seam because the daemon's entire auto-sync path runs
through `subscribe()` — swap the provider and nothing above changes.
## The redoal provider (future)
redoal turns a shared physical gesture (two phones shaken together, a
tap pattern, ...) into a high-entropy shared secret between the people
present. That secret derives a gossip topic:
```
TopicKey = BLAKE3-derive_key("redoal/v1/topic", gesture_secret)
```
`RedoalDiscovery` would:
- join the iroh-gossip swarm for that TopicId (iroh-gossip rides the
same iroh endpoint varde already has — no new transport);
- `subscribe(topic)`: yield gossip messages decoded as
`SignedAnnouncement`s. Verification and consent stay in the daemon —
the provider is a dumb pipe by design;
- `announce(topic, ann)`: broadcast to the swarm. Unlike the LAN
provider (where announce is a no-op because mDNS already advertises
presence), gossip announce actively publishes — the daemon must only
call it for content the user shared to that topic.
### Trust bootstrap
The gesture is the consent ceremony. Deriving from the same secret:
```
author trust: the gesture exchange includes both parties' public keys
(signed with the gesture key), so each side adds the
other to the *author* allowlist for that topic.
peer trust: announcements carry provider NodeIds; on a gestured
topic, providers named by a trusted author get scoped
peer trust (serve/fetch for that topic's roots only).
```
That last point needs one extension to the v1 model: today peer trust
is global (`meta.json` allowlist). Topic-scoped trust would add
`trusted_for: {topic → keys}`, checked in the same two places the
global list is checked now (provider accept gate, fetch candidate
filter). The seam anticipates this: both checks already live in the
daemon, not in the provider.
### What stays true regardless of provider
- Discovery open, replication explicit: indexing an announcement never
moves bytes; only the trust checks above do.
- All fetched data is BLAKE3-verified by iroh-blobs.
- Metered/rate-limit posture applies unchanged — providers sit above
the transport, the limiters below it.
## Sizing
`RedoalDiscovery` is roughly: iroh-gossip dependency, ~200 lines of
provider glue, the topic-scoped trust extension in `meta.json`, and a
`varde-ctl topic join <secret>` command. No daemon architecture changes.