Files
peerspeak/VERSIONING.md
T
molluskandClaude Opus 4.8 3ec09de87e Adopt versioning standard + migrate to versioned protocol planes (0.2.0)
Establishes VERSIONING.md: SemVer 0.x (MINOR = breaking wire change) for the
release version, and per-plane protocol versions enforced on the wire so
incompatible peers fail fast and legibly instead of via silent decode/signature
errors.

⚠️ BREAKING WIRE CHANGE — all peers must run >= 0.2.0 to interoperate (ALPNs and
gossip subscription topics changed). A pre-0.2.0 peer (e.g. an un-resynced
dopedart) can no longer connect, by design, and now fails at the handshake.

- New src/protocol.rs: single source of truth for AUDIO/FRIENDS/GOSSIP_PROTO,
  the derived ALPNs (peerspeak/audio/1, peerspeak/friends/1), GOSSIP_SIG_DOMAIN,
  and versioned_topic(). Unit tests assert ALPN/domain strings match their
  integer versions (no silent drift) + that topic namespacing is deterministic.
- Unified ALPNs: audio was b"peerspeak-audio" (unversioned, and duplicated in
  iroh_impl.rs + core/mod.rs) -> peerspeak/audio/1 from protocol.rs; friends
  re-exports protocol::FRIENDS_ALPN (was peerspeak/friends/0 -> /1).
- Gossip: subscribe to versioned_topic(ticket.topic_id) so different gossip
  versions never share a swarm; the raw topic_id stays the room identity and
  what signatures bind. GOSSIP_SIG_DOMAIN centralized into protocol.rs.
- Cargo.toml 0.1.0 -> 0.2.0.

316 lib tests / clippy --all-targets clean. VERSIONING.md documents the bump
rules, the "I changed X -> what do I bump" table, and a release checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 05:20:27 -04:00

7.0 KiB

PeerSpeak Versioning Standard

PeerSpeak is a full-mesh P2P voice app. Its "API contract" is not a library surface — it is the wire protocol two nodes use to talk. So versioning here tracks one question above all others:

Can a node on build X talk to a node on build Y?

There are two distinct version layers. Keep them straight.


Layer 1 — Release version (Cargo.toml)

The human-facing label you put on a build ("install this one").

Scheme: SemVer, pre-1.0 (0.MINOR.PATCH).

While we are pre-1.0 (friends-only, no stability promise yet):

Change Bump Example
Breaking wire/protocol change — peers on the old build can no longer interoperate; everyone must update MINOR 0.4.2 → 0.5.0
Compatible change — bug fix, internal refactor, or a feature that does not change the wire (UI, local-only behavior, additive logic that old peers ignore safely) PATCH 0.4.2 → 0.4.3
  • Reaching 1.0.0: when PeerSpeak is first shared beyond the trusted-friends circle (a "public" release), and we are willing to commit to wire stability. After 1.0, MAJOR = wire break, MINOR = compatible feature, PATCH = fix (normal SemVer).
  • Bump version in Cargo.toml as part of the change that warrants it, in the same commit. The number in Cargo.toml is the source of truth; surface it in the UI (e.g. an About/Settings line) so a user can read their build.

Rule of thumb: if you find yourself writing "all peers must rebuild" or "breaking gossip wire change" in a commit message (as S2 and W4 did), that is a MINOR bump, and it must also bump the relevant protocol version in Layer 2.


Layer 2 — Protocol compatibility (the one that actually breaks calls)

Wire incompatibility must fail fast and legibly — never as a silent signature/decode error that looks like a bug or an attack. We achieve this by embedding a protocol version into each transport plane, so incompatible peers are rejected at connect/subscribe time instead of mid-conversation.

PeerSpeak has three independent planes, each versioned separately — bump only the plane whose wire format actually changed (audio rarely changes; gossip changes often; they must not be forced to bump together).

ALPN naming convention

All peerspeak ALPNs use the form peerspeak/<plane>/<N> where <N> is that plane's protocol version (an integer, starts at 1). iroh refuses a connection whose ALPN does not match exactly, so two peers on different <N> for a plane simply cannot open that connection → we map that to a clean "peer is running an incompatible version" instead of garbage.

Plane ALPN / mechanism Bump when…
Audio ALPN peerspeak/audio/<N> the Opus/datagram framing, sequencing, or audio-handshake changes
Friends/presence ALPN peerspeak/friends/<N> the ControlMsg / presence ping-pong shape changes
Gossip (see below — cannot use a custom ALPN) GossipPayload / GossipMessage / PeerState shape, signing, or freshness rules change

Gossip is special

The gossip plane runs over iroh-gossip's own GOSSIP_ALPN, which we do not control, so we cannot version it via the ALPN. Instead, the gossip protocol version is bound in two places:

  1. Topic namespacing (primary, fail-fast): the room's topic_id is a random 32 bytes carried in the ticket, but the topic we actually subscribe to is protocol::versioned_topic(topic_id) — a deterministic, dependency-free transform that folds GOSSIP_PROTO into the bytes. Peers on different gossip versions therefore derive different subscription topics from the same ticket and never share a swarm — the same isolation a versioned ALPN gives the other planes. The ticket format and the room identity (topic_id) are unchanged; only the subscribed topic is namespaced. (The transform is for isolation, not security — cryptographic separation is the signature domain below.)
  2. Signature domain (cryptographic separation): the signing domain string (peerspeak-gossip-v<N>, bound into every signed payload) carries the version, so two versions that somehow met on a topic would fail each other's verification rather than misread it.

Bumping the gossip version = bump protocol::GOSSIP_PROTO (drives versioned_topic) and protocol::GOSSIP_SIG_DOMAIN together (a unit test in protocol.rs asserts the domain string matches GOSSIP_PROTO, so they can't drift).

Single source of truth for protocol versions

All protocol versions, ALPNs, the gossip signature domain, and versioned_topic live in src/protocol.rs. Every call site derives from there (e.g. crate::protocol::AUDIO_ALPN); never hand-write an ALPN literal inline. A unit test asserts each ALPN/domain string matches its integer version so a bump can't half-apply.


"I changed X — what do I bump?" (quick reference)

You changed… Layer 2 (plane version) Layer 1 (Cargo.toml)
Opus framing / audio datagram layout peerspeak/audio/NN+1 MINOR
ControlMsg / presence shape peerspeak/friends/NN+1 MINOR
GossipPayload/PeerState/signing GOSSIP_PROTO_VERSION + sig domain → next MINOR
UI, local config, recording, a fix that doesn't touch any wire nothing PATCH
An additive gossip field that old peers safely ignore judgement call — if old peers misbehave without it, treat as breaking (MINOR + gossip bump); if truly ignorable, PATCH PATCH or MINOR

When in doubt about "is this additive-safe?", assume breaking and bump. A false MINOR bump costs a coordinated rebuild; a false PATCH costs silent broken calls in the field.


Release checklist (per build handed to anyone)

  1. Decide MINOR vs PATCH from the table above; bump Cargo.toml.
  2. If MINOR for a wire reason, confirm the matching Layer-2 plane version(s) were bumped in the same change.
  3. Note the version + "breaking?" in the commit / handoff.
  4. Tag the commit (v0.x.y) so a given binary maps to a known commit.
  5. Rebuild every peer that must interoperate (e.g. dopedart, staged friend releases) when the bump was a MINOR/wire break.

Current baseline (standard adopted + migrated, 2026-06-18, 0.2.0)

  • Cargo.toml: 0.2.0 — the MINOR bump for the (deliberately breaking) migration to this standard. All peers must run ≥ 0.2.0 to interoperate (the ALPNs and gossip topics changed); the pre-standard 0.1.0-era build (e.g. an un-resynced dopedart) cannot talk to a 0.2.0 peer — by design, and it now fails cleanly at the handshake instead of silently.
  • Protocol versions (all at 1): peerspeak/audio/1, peerspeak/friends/1, gossip peerspeak-gossip-v1 + versioned_topic. All sourced from src/protocol.rs.
  • Remaining nicety (not blocking): surface env!("CARGO_PKG_VERSION") in the UI (an About/Settings line) and/or log it at startup, so a running build is self-identifying in the field. Small follow-up.