SpawnDev.RTC inherits its room and peer identifier conventions from BitTorrent / WebTorrent. Both are 20-byte (160-bit) values exchanged on the wire as latin1 binary strings (see 01-tracker-signaling.md §3 for encoding details).
This document is the single reference for what those 20 bytes mean to SpawnDev.RTC and how to generate / interpret them.
A RoomKey is a 20-byte opaque identifier for a signaling room. The tracker treats it as a flat namespace — peers announcing the same RoomKey can meet each other; peers with different RoomKeys are isolated.
The tracker imposes no structural requirements on RoomKey bytes. SpawnDev.RTC consumers commonly use:
| Pattern | Example C# | When to use |
|---|---|---|
| Random | RandomNumberGenerator.GetBytes(20) |
Private rooms (game lobbies, voice calls) — share via out-of-band invite |
| SHA-1 of a name | SHA1.HashData(Encoding.UTF8.GetBytes("multiplayer.lobby.alice")) |
Discoverable rooms — anyone who knows the name can join |
| BitTorrent infohash | The infohash of a .torrent file | WebTorrent compatibility — peers find each other by torrent identity |
| App-defined hash | SHA1.HashData(myCustomBlob) |
Domain-specific identity (game UUID, document ID, agent-swarm task ID, etc.) |
// Random (private room):
var roomKey = new RoomKey(RandomNumberGenerator.GetBytes(20));
// SHA-1 of a string (named room):
var roomKey = new RoomKey(SHA1.HashData(Encoding.UTF8.GetBytes("game.123.lobby")));RoomKey (a SpawnDev.RTC type) wraps the 20-byte value and provides:
Bytes— the raw byte arrayToHex()— hex string for logging / URLsFromHex(string)— parse back from hex
If two unrelated SpawnDev.RTC apps independently choose the RoomKey SHA1("default") — a real risk for naive apps — they'll share a room on any public WebTorrent tracker. Peers from app A would receive offers from app B and vice versa, and the WebRTC connection attempts would fail (different application logic).
Mitigations:
- Always namespace RoomKey inputs (
SHA1("myapp:room:" + roomName)) - Or use random RoomKeys and share them via your own out-of-band channel
- Or run your own tracker (
SpawnDev.RTC.Server) and don't expose it publicly
On the wire, info_hash is the field name. For BitTorrent applications, the bytes are the SHA-1 hash of the torrent's info dict. For non-torrent SpawnDev.RTC applications, the bytes can be anything. The protocol doesn't care — and a public WebTorrent tracker will happily route signaling for a RoomKey that's not a real torrent.
A PeerId is a 20-byte identifier for a single peer-connection within a room. It must be unique within that room for the duration of the connection, and stable across re-announces from the same connection.
WebTorrent inherits BitTorrent's peer-id convention (BEP 20):
-XX0000-yyyyyyyyyyyy
│ │ │ └── 12 random bytes
│ │ └────── 4-character version (e.g., "0001")
│ └───────── 2-character client ID (e.g., "WT" for WebTorrent, "NV" for native)
└────────── '-' literal
SpawnDev.RTC's default convention:
-RT0100-<12 random bytes> // SpawnDev.RTC native
-NV0001-<12 random bytes> // SpawnDev.WebTorrent ("NV" historic, see WebTorrent Research/01-wire-protocol §2.5)
Generated by:
public static byte[] CreatePeerId(string clientPrefix = "-RT0100-")
{
var bytes = new byte[20];
Encoding.ASCII.GetBytes(clientPrefix, 0, 8, bytes, 0);
RandomNumberGenerator.Fill(bytes.AsSpan(8));
return bytes;
}The first 8 bytes identify the client and version (useful for debugging and tracker stats). The last 12 bytes are random.
A peer-id MUST remain stable across re-announces in the same logical session. If a peer reconnects with a different peer-id, the tracker treats it as a new peer and does not preserve any state.
If a peer reconnects with the same peer-id, the server cleanly overwrites the prior socket binding (see 01-tracker-signaling.md §5.6). This is important for resuming a session after a transient WebSocket drop.
The same peer (same physical client) can use the same peer-id across multiple rooms — these are tracked independently per room. Or it can use different peer-ids per room — also fine. The tracker doesn't correlate.
For tracker-gated TURN (see 04-stun-turn.md §5), the embedded TURN credential username encodes the peer-id; if a peer uses different ids per room, it must also mint different TURN credentials per room.
A common bug: assuming the WebRTC data-channel label assigned at CreateDataChannel time can be used as a cross-side peer identifier. It cannot. Labels are creator-side names; the answerer sees them in OnDataChannel, but they're not guaranteed to be cross-side stable in race scenarios. Always use the BitTorrent peer-id from the BEP-10 handshake (or a peer-id sent in an application handshake message on the channel) as the cross-side stable identifier. See 03-data-channels.md §1 and Torrent.OnHandshake's labelsComparable guard.
The right scope depends on the application:
| Scope | When | Storage |
|---|---|---|
| Per-session | Default for ephemeral connections (game lobby, voice call) | In-memory only; new id on every page load |
| Per-instance | An app instance that wants to be recognizable across reloads but not tied to a user | LocalStorage / OPFS keyed by app namespace |
| Per-user | An app that wants peers to recognize the same human across sessions | Tied to authenticated user identity (token-bound, signed by app backend) |
Trade-offs:
- Per-session: max privacy, but no continuity (other peers see you as "new" every time)
- Per-instance: privacy from random observers, continuity within the same browser/device
- Per-user: continuity across devices, but a privacy-aware app should let users opt out
For applications that need cryptographic peer identity (e.g., agent swarms where messages must be authenticated), use SpawnDev.BlazorJS.Cryptography's Ed25519 signing. Pattern:
- Generate or load an Ed25519 keypair on the peer (long-term identity)
- Derive peer-id from the public key:
peerId = SHA1(publicKey)[..20] - Sign every outbound message with the private key
- Verify inbound messages against the sender's public key (which can be advertised via BEP-44 mutable items in the DHT, or distributed out-of-band)
This is exactly the pattern WebTorrent's BEP-46 mutable subscription uses. See SpawnDev.WebTorrent's IDhtSigner and Ed25519Signer.
The tracker does not authenticate peer-ids — anyone can announce with any peer-id. To prevent peer-id squatting / impersonation:
- Use random peer-ids (high collision resistance, no semantic meaning to squat)
- Pair every peer-id with an out-of-band identity proof (signed message on the data channel, OAuth token, etc.)
- For sensitive applications, use the cryptographic-identity pattern above
The room itself is also unauthenticated — anyone with the RoomKey can join. Apps that need access control should verify identity at the application layer after the data channel opens, not rely on the tracker.
Always namespace your RoomKey input. Bad:
var roomKey = new RoomKey(SHA1.HashData(Encoding.UTF8.GetBytes("lobby")));Good:
var roomKey = new RoomKey(SHA1.HashData(Encoding.UTF8.GetBytes($"myapp.{appVersion}.lobby.{lobbyName}")));The bad version collides with every other app on a public tracker. The good version is uniquely yours.
The wire protocol assumes exactly 20 bytes. Do not pass shorter or longer values — the tracker will reject the announce with failure reason: "invalid peer_id" (or accept and behave undefinedly).
A peer-id is logically the same as a session id. If you have multiple users on the same browser instance (multi-tab), each tab needs its own peer-id, even if they're in the same room. Otherwise the second tab's announce overwrites the first tab's binding (see 01-tracker-signaling.md §5.6).
The 20 raw bytes are non-printable. Convert to hex (40 chars) or to the BitTorrent prefix-plus-hex format (8 ASCII chars + hex of the random suffix) for readable logs:
var idAscii = Encoding.ASCII.GetString(peerId, 0, 8);
var idHex = Convert.ToHexString(peerId, 8, 12);
Console.WriteLine($"peer = {idAscii}{idHex}");
// e.g., "peer = -RT0100-A1B2C3D4E5F6A7B8C9D0E1F2"A peer-id is public — it's broadcast to every peer in the room. Do not encode user identity, auth tokens, or anything sensitive into the peer-id. Use the application's data-channel handshake for those.