The const comment claimed "v5 (0.7.0)" while GOSSIP_PROTO has been 5 since the
v0.6.0 tag (introduced by bca2ccd, "release 0.6.0"). Git confirms the value went
straight 3 -> 5 in that one release and a GOSSIP_PROTO == 4 build never existed.
Merge the two mislabeled v4/v5 bullets into one accurate v4-v5 (0.6.0) entry and
note the 3->5 jump + that this breaking gossip change correctly rode the
0.5.1 -> 0.6.0 MINOR bump per VERSIONING.md (0.6.1 is a wire-compatible PATCH,
still proto 5). Comment-only; no wire/behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
127 lines
5.8 KiB
Rust
127 lines
5.8 KiB
Rust
//! Single source of truth for PeerSpeak's on-wire protocol versions and the
|
||
//! per-plane ALPNs / gossip constants derived from them.
|
||
//!
|
||
//! See `VERSIONING.md`. The rule: each transport plane is versioned independently
|
||
//! (audio rarely changes, gossip changes often), and incompatible peers must fail
|
||
//! fast — never as a silent decode/signature error. iroh refuses a mismatched
|
||
//! ALPN at the QUIC handshake, so the audio/friends planes are self-isolating;
|
||
//! gossip can't use a custom ALPN (it rides iroh-gossip's `GOSSIP_ALPN`), so its
|
||
//! version is bound into the subscribed topic ([`versioned_topic`]) and the
|
||
//! signature domain ([`GOSSIP_SIG_DOMAIN`]).
|
||
//!
|
||
//! **Never hand-write an ALPN literal elsewhere — derive it here.** Bumping a
|
||
//! plane's protocol version is a breaking wire change → also bump `Cargo.toml`
|
||
//! MINOR (see `VERSIONING.md`).
|
||
|
||
/// Audio datagram plane version (Opus framing / sequencing). Bump on any audio
|
||
/// wire change. Mirrored in [`AUDIO_ALPN`].
|
||
pub const AUDIO_PROTO: u32 = 1;
|
||
/// Friends/presence plane version (`ControlMsg` ping-pong shape). Bump on any
|
||
/// change. Mirrored in [`FRIENDS_ALPN`].
|
||
pub const FRIENDS_PROTO: u32 = 1;
|
||
/// Gossip plane version (`GossipPayload`/`GossipMessage`/`PeerState`, signing,
|
||
/// freshness). Bump on any change. Mirrored in [`GOSSIP_SIG_DOMAIN`] and folded
|
||
/// into [`versioned_topic`].
|
||
///
|
||
/// v2 (0.3.0): `GossipMessage::Chat` gained an optional file attachment
|
||
/// (`ChatAttachment`), so a pre-v2 peer can't interpret/serve chat files — bumped
|
||
/// to fail fast rather than half-work.
|
||
///
|
||
/// v3 (0.4.0): `PeerState` gained an optional `game` presence field (the
|
||
/// `Playing <name>` status). The field is `#[serde(default)]`, so the bump isn't
|
||
/// strictly required for decoding — but per the versioning discipline a wire-shape
|
||
/// change is isolated into its own topic + signature domain so v2 and v3 peers
|
||
/// never share a swarm. Resync everyone, exactly like the W4 avatar bump.
|
||
///
|
||
/// v4–v5 (0.6.0): the W22 shared-listening / music presence work. `PeerState`
|
||
/// gained an optional `music` presence field (a current shared-listening track
|
||
/// descriptor + playback timeline; the audio bytes still ride the files plane by
|
||
/// id, gossip carries only the descriptor/timeline), and `MusicPresence` then
|
||
/// gained optional prefetch hints for the next track so tuned-in listeners can
|
||
/// fetch it before the DJ advances. Both shipped together in the **0.6.0** release
|
||
/// (commit `bca2ccd`), where the const advanced straight `3 → 5`: there was never
|
||
/// a `GOSSIP_PROTO == 4` build — 4 is a skipped step. (Per `VERSIONING.md` this
|
||
/// breaking gossip change rode the `0.5.1 → 0.6.0` MINOR bump, so the discipline
|
||
/// was honoured; 0.6.1 is a wire-compatible PATCH on top, still proto 5.)
|
||
pub const GOSSIP_PROTO: u32 = 5;
|
||
/// File-transfer plane version (chat attachment request/stream shape). Bump on
|
||
/// any change. Mirrored in [`FILES_ALPN`].
|
||
pub const FILES_PROTO: u32 = 1;
|
||
|
||
/// ALPN for the audio datagram plane: `peerspeak/audio/<AUDIO_PROTO>`.
|
||
pub const AUDIO_ALPN: &[u8] = b"peerspeak/audio/1";
|
||
/// ALPN for the friends/presence plane: `peerspeak/friends/<FRIENDS_PROTO>`.
|
||
pub const FRIENDS_ALPN: &[u8] = b"peerspeak/friends/1";
|
||
/// ALPN for the file-transfer plane: `peerspeak/files/<FILES_PROTO>`. Carries
|
||
/// chat attachment bytes via direct QUIC streams (not gossip).
|
||
pub const FILES_ALPN: &[u8] = b"peerspeak/files/1";
|
||
/// ed25519 gossip signature domain: `peerspeak-gossip-v<GOSSIP_PROTO>`. Carries
|
||
/// the gossip protocol version into every signed payload — a version mismatch
|
||
/// fails verification (cryptographic separation between gossip versions).
|
||
pub const GOSSIP_SIG_DOMAIN: &str = "peerspeak-gossip-v5";
|
||
|
||
/// Version-namespace a room topic so peers on different gossip protocol versions
|
||
/// derive **different subscription topics from the same ticket** and therefore
|
||
/// never share a swarm — the gossip analog of a versioned ALPN. The room's raw
|
||
/// `topic_id` (random 32 bytes, carried in the ticket) is the room identity and
|
||
/// is unchanged; only the *subscribed* topic is namespaced.
|
||
///
|
||
/// Deterministic and dependency-free; bijective for a fixed version, so distinct
|
||
/// rooms stay distinct after namespacing. This transform is for *isolation*, not
|
||
/// security — cryptographic separation between versions comes from
|
||
/// [`GOSSIP_SIG_DOMAIN`].
|
||
pub fn versioned_topic(topic_id: [u8; 32]) -> [u8; 32] {
|
||
let v = GOSSIP_PROTO.to_le_bytes();
|
||
let mut out = topic_id;
|
||
for (i, b) in out.iter_mut().enumerate() {
|
||
*b ^= v[i % v.len()];
|
||
}
|
||
out
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// The ALPN/domain strings must stay in lock-step with the integer versions
|
||
/// so a version bump can't silently forget to update the wire string.
|
||
#[test]
|
||
fn alpns_match_their_proto_versions() {
|
||
assert_eq!(
|
||
AUDIO_ALPN,
|
||
format!("peerspeak/audio/{AUDIO_PROTO}").as_bytes()
|
||
);
|
||
assert_eq!(
|
||
FRIENDS_ALPN,
|
||
format!("peerspeak/friends/{FRIENDS_PROTO}").as_bytes()
|
||
);
|
||
assert_eq!(
|
||
FILES_ALPN,
|
||
format!("peerspeak/files/{FILES_PROTO}").as_bytes()
|
||
);
|
||
assert_eq!(
|
||
GOSSIP_SIG_DOMAIN,
|
||
format!("peerspeak-gossip-v{GOSSIP_PROTO}")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn versioned_topic_is_deterministic_and_room_distinct() {
|
||
let a = [9u8; 32];
|
||
let mut b = a;
|
||
b[5] = 10;
|
||
assert_eq!(versioned_topic(a), versioned_topic(a), "deterministic");
|
||
assert_ne!(
|
||
versioned_topic(a),
|
||
versioned_topic(b),
|
||
"distinct rooms stay distinct"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn versioned_topic_actually_namespaces_for_current_version() {
|
||
// Guards against a no-op transform: GOSSIP_PROTO=1 must change the topic.
|
||
assert_ne!(versioned_topic([0u8; 32]), [0u8; 32]);
|
||
}
|
||
}
|