WIRE Protocol - Implementation Notes (Go)
All examples are non-normative. They cover the most error-prone
primitives; the normative specification is wire_spec-0_8.txt.
ISC license applies to all code in this file.
23.1 Key generation
import "crypto/ed25519"
// GenerateIdentityKey returns a fresh Ed25519 keypair.
// Store privKey securely; pubKey is shared freely.
func GenerateIdentityKey() (pubKey ed25519.PublicKey, privKey ed25519.PrivateKey, err error) {
return ed25519.GenerateKey(nil) // uses crypto/rand
}
23.2 UID derivation
import (
"crypto/ed25519"
"crypto/sha256"
"encoding/base32"
)
var enc = base32.NewEncoding("0123456789ABCDEFGHJKMNPQRSTVWXYZ").
WithPadding(base32.NoPadding)
// UIDFromPublicKey derives the canonical wire uid from an Ed25519 public key.
// The uid is globally stable: same keypair produces the same uid on every server.
// Algorithm: SHA-256(pubkey_bytes)[0:16] encoded as Crockford base32, 26 chars.
func UIDFromPublicKey(pub ed25519.PublicKey) string {
sum := sha256.Sum256(pub)
return enc.EncodeToString(sum[:16]) // 26 chars
}
// KeyFingerprint returns the human-readable fingerprint for key verification UIs.
// Format: lowercase hex of SHA-256(pubkey), colon-grouped in four 16-char groups.
func KeyFingerprint(pub ed25519.PublicKey) string {
sum := sha256.Sum256(pub)
h := fmt.Sprintf("%x", sum)
return h[0:16] + ":" + h[16:32] + ":" + h[32:48] + ":" + h[48:64]
}
23.3 Payload unescaping
import "strings"
// UnescapePayload decodes the wire payload escape sequences.
// Rules (Sect. 2.4): \n LF, \t TAB, \\ backslash.
// Unknown sequences (\x for any other x) are preserved verbatim.
// Single-pass, left-to-right. Servers relay payload verbatim and never call this.
func UnescapePayload(s string) string {
if !strings.ContainsRune(s, '\\') {
return s // fast path: no escapes present
}
var b strings.Builder
b.Grow(len(s))
i := 0
for i < len(s) {
if s[i] != '\\' || i+1 >= len(s) {
b.WriteByte(s[i])
i++
continue
}
switch s[i+1] {
case 'n':
b.WriteByte('\n')
case 't':
b.WriteByte('\t')
case '\\':
b.WriteByte('\\')
default:
// preserve unknown escape verbatim
b.WriteByte('\\')
b.WriteByte(s[i+1])
}
i += 2
}
return b.String()
}
23.4 Line parsing
import (
"strings"
"net/url"
)
// Line is a parsed wire protocol line.
type Line struct {
Verb string // e.g. "MSG", "KVAL", "OK"
Tags map[string]string // decoded tag values
Payload *string // nil if no ' :' delimiter; "" if empty payload
Raw []byte // original raw bytes (for relay without re-encode)
}
// ParseLine parses one wire line. The LF terminator must have been stripped.
// Returns an error if the line is malformed.
// Servers that relay lines opaquely use line.Raw and never call ParseLine.
func ParseLine(raw string) (Line, error) {
if len(raw) == 0 {
return Line{}, fmt.Errorf("empty line")
}
var payload *string
body := raw
// Split payload: find first ' :' sequence.
if idx := strings.Index(raw, " :"); idx >= 0 {
p := raw[idx+2:]
payload = &p
body = raw[:idx]
}
parts := strings.Fields(body)
if len(parts) == 0 {
return Line{}, fmt.Errorf("missing verb")
}
verb := parts[0]
tags := make(map[string]string, len(parts)-1)
for _, part := range parts[1:] {
eq := strings.IndexByte(part, '=')
if eq < 0 {
return Line{}, fmt.Errorf("malformed tag (no '='): %q", part)
}
key := part[:eq]
val := part[eq+1:]
decoded, err := url.QueryUnescape(strings.ReplaceAll(val, "+", "%2B"))
if err != nil {
return Line{}, fmt.Errorf("bad percent-encoding in tag %q: %w", key, err)
}
tags[key] = decoded
}
return Line{
Verb: verb,
Tags: tags,
Payload: payload,
Raw: []byte(raw),
}, nil
}
// TagOK returns the tag value and whether it was present.
func (l *Line) TagOK(key string) (string, bool) {
v, ok := l.Tags[key]
return v, ok
}
23.5 PresenceStore
import "sync"
// PresenceStore holds per-uid online status.
// Never flushed to disk; empty on server restart.
// The same memory structure is used for TypingStore (per-cid, per-uid).
type PresenceStore struct {
mu sync.RWMutex
store map[string]PresenceEntry // key: uid
}
type PresenceEntry struct {
Status string // "online" | "idle" | "dnd" | "invisible" | "offline"
UpdatedAt int64 // unix-ms
}
func (ps *PresenceStore) Set(uid, status string, ts int64) {
ps.mu.Lock()
defer ps.mu.Unlock()
if ps.store == nil {
ps.store = make(map[string]PresenceEntry)
}
ps.store[uid] = PresenceEntry{Status: status, UpdatedAt: ts}
}
// Get returns the presence entry and whether it is known.
// Returns ("offline", false) for unknown uids.
func (ps *PresenceStore) Get(uid string) (PresenceEntry, bool) {
ps.mu.RLock()
defer ps.mu.RUnlock()
e, ok := ps.store[uid]
return e, ok
}
23.6 KVALStore
KVALStore is the durable server-side key-value store backing all KVAL
scopes. Each (scope, id, key) triple maps to a value and a last-modified
timestamp.
import (
"sync"
"fmt"
)
// KVALKey is the composite store key.
type KVALKey struct {
Scope string // "user" | "comm" | "chan" | "mute"
ID string // uid (user/mute), gid (comm), cid (chan)
Key string // e.g. "bio", "banner", "notice"
}
// KVALEntry holds the current value and the unix-ms of the last op=set.
type KVALEntry struct {
Value string
TS int64 // unix-ms of most recent op=set (Sect. 2.6 ts= invariant)
}
// KVALStore is the authoritative store for all KVAL scopes.
// Implementations MUST flush this to durable storage; it is not ephemeral.
type KVALStore struct {
mu sync.RWMutex
store map[KVALKey]KVALEntry
}
func NewKVALStore() *KVALStore {
return &KVALStore{store: make(map[KVALKey]KVALEntry)}
}
// Set writes a key. ts is the server commit unix-ms.
// The caller MUST persist before returning OK to the client (Sect. 2.6 commit-before-OK).
func (s *KVALStore) Set(scope, id, key, value string, ts int64) {
s.mu.Lock()
defer s.mu.Unlock()
s.store[KVALKey{scope, id, key}] = KVALEntry{Value: value, TS: ts}
}
// Del removes a key. Returns (true, entry) if the key existed,
// (false, zero) if it was already absent (idempotent: caller returns OK, no broadcast).
func (s *KVALStore) Del(scope, id, key string) (existed bool, entry KVALEntry) {
s.mu.Lock()
defer s.mu.Unlock()
k := KVALKey{scope, id, key}
entry, existed = s.store[k]
if existed {
delete(s.store, k)
}
return
}
// Get retrieves a single key. Returns (entry, true) if found, (zero, false) if not.
func (s *KVALStore) Get(scope, id, key string) (KVALEntry, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
e, ok := s.store[KVALKey{scope, id, key}]
return e, ok
}
// GetAll returns all set keys for (scope, id), in unspecified order.
// An empty slice means no keys are set (OK count=0 total=0; no NOTFOUND).
func (s *KVALStore) GetAll(scope, id string) []struct {
Key string
Entry KVALEntry
} {
s.mu.RLock()
defer s.mu.RUnlock()
var out []struct {
Key string
Entry KVALEntry
}
for k, v := range s.store {
if k.Scope == scope && k.ID == id {
out = append(out, struct {
Key string
Entry KVALEntry
}{k.Key, v})
}
}
return out
}
// DeleteByID removes all keys for a given (scope, id).
// Called on COMM op=delete (scope=comm, id=gid) to cascade-delete community branding.
// Returns the count of keys removed.
func (s *KVALStore) DeleteByID(scope, id string) int {
s.mu.Lock()
defer s.mu.Unlock()
count := 0
for k := range s.store {
if k.Scope == scope && k.ID == id {
delete(s.store, k)
count++
}
}
return count
}
Validation helpers
// defined key sets per scope. servers MUST reject unknown keys with ERR BADARG.
var kvallAllowedKeys = map[string]map[string]bool{
"user": {
"bio": true,
"banner": true,
"color": true,
"status_text": true,
"bot": true, // implementation extension; not in Sect. 7.4
},
"comm": {
"description": true,
"icon": true,
"banner": true,
"color": true,
"rules": true,
},
"chan": {
"notice": true, // implementation extension; not in Sect. 7.5b scope registry
},
"mute": {
"mute_channels": true,
"mute_users": true,
"mute_roles": true,
"ignore_channels": true,
"ignore_users": true,
"ignore_roles": true,
},
}
// ValidateKVAL checks scope, id type, and key legality.
// Returns a non-nil error string for ERR BADARG payload.
func ValidateKVAL(scope, id, key string) error {
keys, ok := kvallAllowedKeys[scope]
if !ok {
return fmt.Errorf("unknown kval scope %q", scope)
}
if key != "" && !keys[key] {
return fmt.Errorf("unknown key %q for scope %q", key, scope)
}
// key=name is forbidden for scope=comm (Sect. 7.5b).
if scope == "comm" && key == "name" {
return fmt.Errorf("key=name is not valid for scope=comm; use COMM op=update name=")
}
return nil
}
23.7 Server dispatch sketch (KVAL handler)
// HandleKVAL is a non-normative dispatch sketch for the KVAL verb.
// conn is the authenticated connection; line is the parsed client line.
// store and kvstore are the server's durable stores.
func HandleKVAL(conn *Conn, line Line, kvstore *KVALStore) {
op, _ := line.TagOK("op")
scope, _ := line.TagOK("scope")
id, _ := line.TagOK("id")
key, _ := line.TagOK("key")
// Validate scope and key before permission checks.
if err := ValidateKVAL(scope, id, key); err != nil {
conn.SendERR("BADARG", err.Error())
return
}
// op=info is server-only; clients MUST NOT send it.
if op == "info" {
conn.SendERR("BADARG", "clients must not send KVAL op=info")
return
}
switch op {
case "set":
checkWritePermission(conn, scope, id) // scope-specific; sends NOPERM and returns on failure
val := ""
if line.Payload != nil {
val = *line.Payload
}
ts := NowMS()
kvstore.Set(scope, id, key, val, ts)
conn.SendOK("KVAL", "op=set", "scope="+scope, "id="+id, "key="+key)
BroadcastKVALInfo(conn.Server, scope, id, key, val, ts)
case "del":
checkWritePermission(conn, scope, id)
ts := NowMS()
existed, _ := kvstore.Del(scope, id, key)
conn.SendOK("KVAL", "op=del", "scope="+scope, "id="+id, "key="+key)
if existed {
BroadcastKVALDel(conn.Server, scope, id, key, ts)
// no broadcast if key did not exist (idempotent, Sect. 7.5d)
}
case "get":
checkReadPermission(conn, scope, id) // sends NOPERM and returns on failure
if key != "" {
// single-key form
entry, ok := kvstore.Get(scope, id, key)
if !ok {
conn.SendERR("NOTFOUND", "key not set")
return
}
conn.SendKVALInfo(scope, id, key, entry.Value, entry.TS)
conn.SendOK("KVAL", "op=get", "scope="+scope, "id="+id, "key="+key)
} else {
// all-keys form
entries := kvstore.GetAll(scope, id)
for _, e := range entries {
conn.SendKVALInfo(scope, id, e.Key, e.Entry.Value, e.Entry.TS)
}
n := len(entries)
conn.SendOK("KVAL", fmt.Sprintf("op=get scope=%s id=%s count=%d total=%d", scope, id, n, n))
}
default:
conn.SendERR("BADARG", fmt.Sprintf("unknown KVAL op %q", op))
}
}
Broadcast scope routing
// BroadcastKVALInfo sends a KVAL op=info event to the correct delivery scope.
// Routing is scope-dependent (Sect. 7.5b):
// scope=user: all connections sharing at least one channel with id (uid)
// scope=comm: all members of the community id (gid)
// scope=chan: all current subscribers of the channel id (cid)
// scope=mute: all sessions of the subject uid (multi-device sync)
func BroadcastKVALInfo(srv *Server, scope, id, key, val string, ts int64) {
line := fmt.Sprintf("KVAL op=info scope=%s id=%s key=%s ts=%d :%s\n",
scope, id, key, ts, val)
switch scope {
case "user":
srv.BroadcastToSharedChannelPeers(id, line)
case "comm":
srv.BroadcastToCommunityMembers(id, line)
case "chan":
srv.BroadcastToChannelSubscribers(id, line)
case "mute":
srv.BroadcastToUID(id, line)
}
}
func BroadcastKVALDel(srv *Server, scope, id, key string, ts int64) {
line := fmt.Sprintf("KVAL op=del scope=%s id=%s key=%s ts=%d\n",
scope, id, key, ts)
switch scope {
case "user":
srv.BroadcastToSharedChannelPeers(id, line)
case "comm":
srv.BroadcastToCommunityMembers(id, line)
case "chan":
srv.BroadcastToChannelSubscribers(id, line)
case "mute":
srv.BroadcastToUID(id, line)
}
}
23.8 ULID generation
import (
"crypto/rand"
"encoding/binary"
"strings"
"time"
)
var ullidEnc = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
// NewULID generates a server-assigned ULID (Sect. 3).
// Time component is the current unix millisecond.
// Random component is 10 bytes from crypto/rand.
// Servers MUST use this for mid, cid, gid, eid, etc.; never client-provided IDs.
func NewULID() string {
now := uint64(time.Now().UnixMilli())
var rnd [10]byte
if _, err := rand.Read(rnd[:]); err != nil {
panic("crypto/rand unavailable: " + err.Error())
}
var b [26]byte
// 10-char time component (48 bits, big-endian, base32)
t := now
for i := 9; i >= 0; i-- {
b[i] = ullidEnc[t&0x1f]
t >>= 5
}
// 16-char random component (80 bits, base32)
val := binary.BigEndian.Uint64(rnd[:8])
val2 := uint16(rnd[8])<<8 | uint16(rnd[9])
for i := 25; i >= 18; i-- {
b[i] = ullidEnc[val&0x1f]
val >>= 5
}
extra := uint64(val2)
for i := 17; i >= 10; i-- {
b[i] = ullidEnc[extra&0x1f]
extra >>= 5
}
return string(b[:])
}
// IsValidID returns true if s is a valid 26-char Crockford base32 ID.
func IsValidID(s string) bool {
if len(s) != 26 {
return false
}
const valid = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
for _, c := range s {
if !strings.ContainsRune(valid, c) {
return false
}
}
return true
}
Notes on the KVAL store and Sect. 2.6 invariants
commit-before-OK
kvstore.Set / kvstore.Del MUST complete (including any durable write
flush) before conn.SendOK is called. An in-memory implementation
satisfies this trivially; BoltDB and similar persistent stores must flush
before sending the OK line.
op=del idempotency
DeleteByID (cascade on COMM op=delete) and the single-key Del are both
idempotent. The existed return value gates the broadcast, so a retry after
a dropped connection will not send a spurious deletion event.
scope=comm cascade
Call kvstore.DeleteByID("comm", gid) in the COMM op=delete handler,
after the OK is sent to the acting uid and before or alongside closing
subscriptions. No broadcast is needed; MEMBER op=delete already signals
community destruction to all members.
scope=chan cascade
Call kvstore.DeleteByID("chan", cid) in the COMM op=channel.delete
handler, atomically with the channel deletion. Same pattern as scope=comm;
CHAN op=delete already signals channel destruction.
scope=user GC on ERASE
When a uid is erased, call kvstore.DeleteByID("user", uid) before sending
OK verb=ERASE. No broadcast is needed; MEMBER op=erase already signals
the erasure.
23.9 ERASE handler sketch
// HandleERASE manages the two-step account erasure flow.
// Step 1: client sends ERASE (no tags). Server issues CHALLENGE.
// Step 2: client sends ERASE confirm=<sig>. Server validates and executes.
func HandleERASE(conn *Conn, line Line) {
confirm, hasConfirm := line.TagOK("confirm")
if !hasConfirm {
// Step 1: issue challenge.
// Store nonce in connection state tagged as "erase" context
// so the AUTH handler does not accept it for AUTH.
nonce := GenerateNonce() // 32 bytes CSPRNG, hex-encoded to 64 chars
conn.PendingEraseNonce = nonce
conn.Send(fmt.Sprintf("CHALLENGE nonce=%s\n", nonce))
return
}
// Step 2: validate signature.
nonce := conn.PendingEraseNonce
if nonce == "" {
conn.SendERR("BADARG", "no pending erase challenge on this connection")
return
}
conn.PendingEraseNonce = "" // consume nonce (single-use)
uid := conn.AuthUID
pubkey := conn.Server.IdentityStore.GetPubKey(uid)
if pubkey == nil {
conn.SendERR("GONE", "account already erased")
return
}
// erase_msg = nonce_bytes || uid_bytes || "erase"
nonceBytes, _ := hex.DecodeString(nonce)
eraseMsg := append(nonceBytes, []byte(uid)...)
eraseMsg = append(eraseMsg, []byte("erase")...)
sigBytes, _ := base64.RawURLEncoding.DecodeString(confirm)
if !ed25519.Verify(pubkey, eraseMsg, sigBytes) {
conn.SendERR("AUTHFAIL", "invalid erase signature")
return
}
// Guard: last server.admin cannot erase.
if conn.Server.IsLastServerAdmin(uid) {
conn.SendERR("FORBIDDEN", "cannot erase the last server.admin")
return
}
ts := NowMS()
// Commit erasure atomically:
// 1. Delete identity content (pubkey, name, avatar). Retain uid tombstone.
conn.Server.IdentityStore.MarkErased(uid, ts)
// 2. Delete KVAL scope=user keys.
conn.Server.KVALStore.DeleteByID("user", uid)
// 3. Delete MEMBER records, subscriptions, tokens, READSTATE, PINGs,
// invite codes, KEYROTATE alias chain.
conn.Server.MemberStore.DeleteAllForUID(uid)
conn.Server.SessionStore.RevokeAll(uid)
conn.Server.ReadStateStore.DeleteAllForUID(uid)
conn.Server.PingStore.DeleteAllForUID(uid)
conn.Server.InviteStore.InvalidateAllByUID(uid)
conn.Server.KeyRotateStore.DeleteAliasChain(uid)
// Send OK before closing connection.
conn.Send(fmt.Sprintf("OK verb=ERASE ts=%d\n", ts))
// Broadcast MEMBER op=erase to each community.
gids := conn.Server.MemberStore.CommunitiesForUID(uid) // collected before deletion
for _, gid := range gids {
line := fmt.Sprintf("MEMBER op=erase gid=%s uid=%s ts=%d\n", gid, uid, ts)
conn.Server.BroadcastToCommunityMembers(gid, line)
}
// Broadcast MEMBER op=erase (no gid=) to each DM partner.
dmPartners := conn.Server.ChannelStore.DMPartnersForUID(uid)
for _, partnerUID := range dmPartners {
line := fmt.Sprintf("MEMBER op=erase uid=%s ts=%d\n", uid, ts)
conn.Server.SendToUID(partnerUID, line)
}
// Terminate all sessions for this uid, including this connection.
conn.Server.TerminateAllSessions(uid)
}
If ERASE purge=1 was sent (and cap=purge is advertised), the server
iterates every channel and purges messages authored by the erasing uid
before sending OK. A HIST op=purged broadcast with a uid= tag fires
per channel to let clients evict only that uid's messages from their local
cache. The OK and connection termination follow after all purges commit.
This is the expensive path; the uid explicitly opts in.
23.10 HIST op=purge -- permission check and broadcast
// CheckPurgePermission returns an error string for ERR NOPERM,
// or nil if the requesting uid may execute the purge.
// Permission model: Sect. 29.1.
func CheckPurgePermission(conn *Conn, cid, filterUID string) error {
isSelfContent := filterUID != "" && filterUID == conn.AuthUID
isDM := conn.Server.ChannelStore.IsDM(cid)
if isSelfContent {
// Self-content purge: any non-banned member may proceed.
// delete_policy does NOT apply (Sect. 29.1).
if isDM {
// Either DM participant may always purge their own messages.
return nil
}
// Community channel: check uid is non-banned member.
if conn.Server.MemberStore.IsBanned(conn.AuthUID, conn.Server.ChannelStore.GIDForCID(cid)) {
return fmt.Errorf("banned members may not purge their own content")
}
return nil
}
// Other-content or unfiltered purge: requires msg.delete (community)
// or delete_policy consent (DM).
if isDM {
// delete_policy enforcement is handled by the caller's state machine.
return nil
}
// Community: require msg.delete (or board.manage on type=board).
if !conn.HasChannelPerm(cid, "msg.delete") {
return fmt.Errorf("msg.delete permission required")
}
return nil
}
After the purge commits, broadcast HIST op=purged to all channel
subscribers (Sect. 29.1); echo before= if the request included it.
The uid= tag in the broadcast is an implementation extension for the
ERASE purge=1 path: it lets clients evict only that uid's messages rather
than flushing the entire channel cache. It is not part of the spec grammar
for this verb.
// EmitPurgedBroadcast sends HIST op=purged to all cid subscribers,
// echoing all filters that were active (Sect. 29.1).
func EmitPurgedBroadcast(srv *Server, cid, byUID, filterUID, before string, ts int64) {
var b strings.Builder
b.WriteString(fmt.Sprintf("HIST op=purged cid=%s", cid))
if before != "" {
b.WriteString(" before=" + before)
}
if filterUID != "" {
b.WriteString(" uid=" + filterUID)
}
b.WriteString(fmt.Sprintf(" by=%s ts=%d\n", byUID, ts))
srv.BroadcastToChannelSubscribers(cid, b.String())
}
23.11 HIST / LIST default direction
Bare HIST cid=X limit=50 with no anchor returns the newest limit=
events in ascending mid order. LIST uses the same default.
before= and after= are mutually exclusive; reject both-present requests
at the top of the handler:
func validateHISTAnchors(before, after string) error {
if before != "" && after != "" {
return errors.New("BADARG: before= and after= are mutually exclusive")
}
return nil
}
The three anchor forms translate to distinct queries:
// Default (no anchor): return the newest limit rows in ascending order.
// Implemented as DESC LIMIT n, then reverse in-memory.
func queryHISTDefault(db *DB, cid string, limit int) ([]Event, bool, error) {
rows, err := db.Query(
`SELECT * FROM events WHERE cid = ? AND deleted = 0
ORDER BY mid DESC LIMIT ?`, cid, limit+1)
if err != nil {
return nil, false, err
}
events := collectRows(rows)
hasMore := len(events) > limit
if hasMore {
events = events[:limit]
}
// reverse to ascending mid order for the client
slices.Reverse(events)
return events, hasMore, nil
}
// before=<mid>: return limit rows strictly before mid, ascending.
func queryHISTBefore(db *DB, cid, before string, limit int) ([]Event, bool, error) {
rows, err := db.Query(
`SELECT * FROM events WHERE cid = ? AND mid < ? AND deleted = 0
ORDER BY mid DESC LIMIT ?`, cid, before, limit+1)
if err != nil {
return nil, false, err
}
events := collectRows(rows)
hasMore := len(events) > limit
if hasMore {
events = events[:limit]
}
slices.Reverse(events)
return events, hasMore, nil
}
// after=<mid>: return limit rows strictly after mid, ascending.
func queryHISTAfter(db *DB, cid, after string, limit int) ([]Event, bool, error) {
rows, err := db.Query(
`SELECT * FROM events WHERE cid = ? AND mid > ? AND deleted = 0
ORDER BY mid ASC LIMIT ?`, cid, after, limit+1)
if err != nil {
return nil, false, err
}
events := collectRows(rows)
hasMore := len(events) > limit
if hasMore {
events = events[:limit]
}
return events, hasMore, nil
}
For default and before= responses, has_more=1 means older history
exists; the client scrolls back using before=<oldest-returned-mid>.
For after= responses, has_more=1 means newer messages follow the
returned window.