Routing & federation
chIRpChat’s topology is deliberately cellular, not mesh-egalitarian: clients register with routers; routers form a backbone. Meshtastic floods because every node is a peer; MeshCore floods to discover then routes directly. chIRpChat goes further: flooding is reserved for discovery only, and the backbone is allowed to use the internet when it has it.
Roles (two binaries, one mental model)
Section titled “Roles (two binaries, one mental model)”| Binary | Runs on | Does |
|---|---|---|
lrc-node |
XIAO, other ESP32/nRF52 | client gateway: USB/WiFi IRC server, registers with routers, relays opportunistically |
lrc-router |
high-placed ESP32/nRF52, or lrcd on Linux/Pi with a LoRa HAT |
sequencing, store, federation, lane scheduling. Build flag RELAY_ONLY strips it down to a lane-translator |
No 30-role taxonomy. A relay is a router with the chat brain compiled out.
lrcd is the same router code with the embedded HAL swapped for
sockets/files — see ARCHITECTURE.md.
v2 credit routing and the edge mesh (Wave 2)
Section titled “v2 credit routing and the edge mesh (Wave 2)”Everything below this section describes the v1 wire mechanics still in
force today (FLAGS.PATH source routing, TH TTL/hops). LPP v2
(PROTOCOL.md §Versioning) replaces those specifically for the edge mesh —
the opportunistic multi-hop relay layer between a client and its router —
without changing anything about routers, federation, or the backbone
described in the rest of this document:
TH(TTL+hops) →AC(airtime credit). The loop guard is the existing(SRC,MSGID)dedup ring plus a new gradient-descent forwarding predicate (relay only if it strictly lowers router-distance); the resource bound isAC, an airtime-denominated budget debited per hop at the relay’s actual time-on-air. PROTOCOL.md §TheACbyte (v2); CREDIT.md is the normative host decision layer (core/include/lrc/credit.h).- The Path block → the Edge Routing Block (ERB).
rprog(router-distance progress),HINT(soft contention bias),RACK_RTR1, andTRAIL[](diagnostics-only relay breadcrumbs — never a trust input) replace the 1-byte-per-hop pinned path. PROTOCOL.md §Edge Routing Block; design rationale indocs/research/routing-redesign/CONSOLIDATED_ROUTING_PLAN.md. - Return path is a Return Descriptor, not a reversed path. The router
that terminates a client’s uplink owns a per-client RD
(BROADCAST/RELAY_VIA/LANE_SCHED/UNKNOWN,
core/include/lrc/return_path.h) that only ever entersRELAY_VIAafter the router has observed a specific relay actually deliver — never seeded from the unauthenticatedTRAIL[]bytes. This is the “observed-delivery gate” that keeps a poisoned forward discovery from seeding a trusted return. RACK(PROGRESS-ACK) is the suppression-collapse control frame (PROTOCOL.md §RACK) a router emits once it has ingested a frame, so relays still holding it pending can cancel (if they hold the issuing router’s SK) or shorten (if they don’t) their contention-window wait instead of always burning the full window.
Status: the wire layout (ERB, AC, RACK’s frame format) is landed and
golden-byte-frozen; Node’s relay-side credit debit/gradient decision and
RACK authentication are wired into core/src/node.cpp. The pending-forward
suppression scheduler that actually cancels/shortens/fires a queued
retransmit using that classification is not yet built — it needs TX-timing
behavior that belongs to firmware and needs human RF sign-off
(AGENTS.md rule 5) before it drives real transmit timing. Constants
(AC’s unit, per-class budgets) are provisional pending the scenario-testbed
sweep gate.
Client ↔ router: the access network
Section titled “Client ↔ router: the access network”- Router emits
BEACONon the anchor lane (slowest preset, widest reach) every 60–600 s (config + load adaptive), carrying: RK fingerprint,rtr_short(2-byte id, rendered as e.g.rt1), lane plan, current load, federation id, and the routerboot_idincarnation. - Client picks the best-heard router, registers (IDENTITY.md §2), probing upward through the lane ladder during the exchange so both ends learn the fastest preset that actually works between them (RADIO.md §Lane probing).
- Downlink: the router owns delivery. It knows exactly which channels its
clients joined and the last
SEQ3each client acked (piggy-backed on client uplinks), so it can replay gaps without being asked, batching during its scheduled lane slots. - Uplink: client → router, possibly via relays (path block). The client learns its uplink path from the first successful exchange (path recording, MeshCore-style: relays append their UID byte to a discovery packet, the WELCOME carries the reverse path back). The path is then pinned — no flooding after discovery — and re-discovered only after the relevant control-plane retry budget expires.
The core enforces pinned path blocks today: a relay forwards a FLAGS.PATH
packet only when path[hops_taken] matches the first byte of its router UID,
then increments hops_taken. Discovery recording is also built: a discovery
packet carries path_len == hops_taken, and each relay appends its UID byte
before forwarding. Online DMs now use that path policy: an unpinned DM starts
with an empty path block, the recipient ACK returns the discovered path in
reverse, and the sender pins the reversed-back path for the next DM to that
UID. Registration uses the same policy: HELLO starts discovery with an empty
path block or uses the pinned router path, WELCOME returns the discovered path
in reverse, and the client pins the reversed-back path only after validating
the signed WELCOME. If a pinned DM exhausts its ACK retry budget, or a pinned
HELLO exhausts its retry budget, the sender clears the stale pin and the next
packet starts discovery again. A path-pinned online DM can fail over sooner
when router liveness has already marked the pinned router unreachable and a
different reachable same-UID attachment is known: the pending retry is retargeted
to that attachment and its mutable path block is compacted back to discovery
form, preserving the original (SRC, MSGID) reliability identity. A relay that
is allowed to forward a path frame but has no current peer/RF egress keeps a
bounded, TTL-limited copy of the already-mutated relay frame and forwards it when
an egress returns; expired or oldest entries are dropped rather than changing
PATH semantics or generating relay ACKs. The default buffer is 8 frames for 30
s, and signed Admin safe keys can adjust or disable it with
relay.store_forward_limit and relay.store_forward_ttl_ms.
Asymmetry is embraced: downlink may be 0 hops (router is loud, on a hill) while uplink takes 2 relay hops. The two directions maintain independent paths.
Roaming
Section titled “Roaming”A moving client that starts hearing rt7 better than its primary rt1
HELLOs to rt7 while still registered to rt1 (make-before-break). rt7
announces the registration in RTRSYNC; rt1 forwards queued DMs over the
backbone and demotes itself to standby; the IRC layer notices nothing — the
user keeps their nick and channels because identity is the key, not the
attachment point. When the new attachment registers, the router restores
channels learned from reachable same-UID RTRSYNC directory records through the
ordinary IRC JOIN path, then the existing max-seq-vector/CHANSYNC recovery loop
backfills any channel messages missed before the roam completed.
Router ↔ router: the backbone
Section titled “Router ↔ router: the backbone”Routers hold links to each other over whatever they have, in preference order: TCP (internet/WiFi/Ethernet) > fast LoRa lane > anchor lane.
- A TCP link carries the same LPP packets as the air. When
lrcdis started with one or more--peer-pin UID8HEXentries, the socket first exchanges authenticated control frames ("LA", little-endian magic0x414C):HELLOcarries(rtr, RK pubkey, nonce), andPROOFsignsLRC-PEER-AUTH-v1 || signer_pub || verifier_pub || signer_nonce || verifier_nonce. The peer’s RK UID must match a configured pin. Both sides derive the base key fromX25519(local_RK, peer_RK)and the two nonces ordered by RK UID, then expand directional SipHash keys forA->BandB->A. After that, data frames are"LR"(little-endian magic0x524C),u16 len,u32 seq,packet bytes,Tag8; the tag covers the header, sequence, and payload.len=0is a MACed link-local keepalive and is not an LPP packet. Unpinnedlrcdsockets and ESP32 firmware lanes keep the legacy unauthenticatedLR | len | packetframing for lab/back-compat only. ESP32 firmware redials a bounded list of stored TCP targets; when its stored peer policy contains pins, outbound lanes complete the sameLAHELLO/PROOF exchange, reject unpinned RK UIDs, then send only sequenced/MACedLRframes. No TLS, no encryption — authenticated plaintext, consistent with project policy. Operators crossing hostile middleboxes should wrap the TCP socket outsidelrcd. - No MQTT, no broker, no SPOF. The backbone is a peer mesh: each router
config lists
peers = [host:port or UID-via-LoRa], links gossip their liveness inRTRSYNC, and the routing rule is dumb and robust: prefer the best link class first (TCP > fast LoRa lane > anchor lane), then the lowest measured RTT inside that class.lrcdreconnects configured TCP peers with per-target jittered exponential backoff, sends idle TCP keepalives, echoes received keepalives, measures RTT from those MACed zero-length frames, times out silent peer sockets, and sends only once per directly connected peer router using the best sampled duplicate link. It reports authenticated or legacy-compatible link-up events throughfed.links, RTT observations throughfed.rtt_sample, last-link drops throughfed.link_down, and rejected auth/MAC/sequence attempts throughfed.auth_fail. Link selection is fully transport-agnostic core policy: each link carries an explicitBackboneLinkClass(TCP today;FastRf/AnchorRffor a future RF backbone lane), and the host-testedbackbone_best_link/backbone_router_reachable/backbone_reachable_routershelpers pick the best ready link to a router and decide reachability — a peer is marked down only after its last ready link drops, so a router reachable both by TCP and by RF survives either one flapping.lrcdcarries the class on every peer link and routes single-target peer sends throughbackbone_best_link; the RF backbone transport (actually shipping a framed peer frame over a radio lane) remains device work. The same policy module now has a host-tested bounded anti-entropy fanout selector for RTRSYNC-style gossip: it caps the number of ready peers per interval, prioritizes never-synced or stale peers, and rotates the remaining candidates by deterministic epoch score.lrcdapplies that selector to RTRSYNC peer broadcasts after first choosing the best duplicate TCP link per router; normal channel, DM, ACK, admin, and DCC traffic still goes to every directly connected peer router. Core RTRSYNC senders now retain changed optional directory, channel-record, tombstone, liveness, identity, and revoke rows for a boundedrtrsync_delta_ttl_mswindow, send those compact rows between full snapshots, and use periodic full snapshots as the repair path for peers that miss a delta.fed.gossip_defercounts skipped ready routers, whilefed.sync_deltacounts compact RTRSYNC sends. FLAGS.VIA_TCPenforces the user’s rule: a packet that traveled by TCP is not re-emitted on LoRa except by the destination’s own access router (the egress), which clears the flag and schedules it on the proper lane. Today the host-testable egress predicate is deliberately narrow: a channel packet may egress only where that scope has local members, and a DM may egress only where the destination identity is locally online. Unknown-version packets can be blind-relayed over TCP/path, but never RF-egressed because the destination semantics are unknowable. Suppressed RF transits incrementtx.via_tcp_suppress. RF spectrum is spent exactly once per access network, never for transit that copper already did.
Federation: shared channels without collisions
Section titled “Federation: shared channels without collisions”Channel identity is the pair (CHAN4, scope):
- Solo scope (default):
#pizzaon routerrt1is rendered to IRC clients as#pizza.rt1. The same name onrt2is#pizza.rt2— a different channel. No global coordination needed, no collisions, ever. The gateway enforces explicit.rtNisolation: a local member of#pizza.rt2does not render or chasert1traffic even though both names share the same suffix-freeCHAN4. Solo deployments can accept the friendly/join #pizzaform and canonicalize it to their local primary router scope before the IRC channel is created./msg *lrc namespacereports the active local policy and examples; adding a channel argument, e.g./msg *lrc namespace #pizza, shows how that name resolves and which local federated or.rtNviews currently exist for the same suffix-freeCHAN4./msg *lrc namespace views [#pizza]lists the currently open local views across all channels, or filters that listing to one suffix-freeCHAN4./msg *lrc namespace join #pizzaapplies that resolution and performs the ordinary local JOIN to the exact view, without changing routing state./msg *lrc namespace merge #from #topreviews a possible local migration: it resolves both exact views, reports local member counts and whether the current IRC session is already in each view, and reminds operators that history, topic, modes, keys, and channel records stay separate./msg *lrc namespace move #from #tofirst joins the policy-resolved target view for the current IRC session and then parts the source view, giving users an explicit, reversible local migration between solo and.rtNviews without merging channel history or router ownership. A solo scope is deliberately owned by its router: ifrt1dies,#pizza.rt1is unavailable untilrt1returns with its persisted sequence/channel state or users explicitly move to another view such as#pizza.rt7. Other routers do not auto-promote themselves to sequence#pizza.rt1, because that would fork the (CHAN4,RTR2) stream and make backfill ambiguous. - Federated scope: routers in the same
federation_id(a 4-byte value + shared roster of RK fingerprints in config) host channels jointly: the channel renders without suffix (#pizza), every member router sequences its own local members’ messages in its own (CHAN4,RTR2) stream, and members merge all streams. Total order doesn’t exist (this is physics, not a bug); display order is(origin timestamp, RTR2)with the IRC layer emitting in arrival order like any netsplit-healing IRC network. History replay exposesserver-timeonly when the local clock is disciplined and the stored origin timestamp is within the default 10-minute skew guard; otherwise the timestamp is hidden andtime.skew_dropcounts the guard. Reconciliation is the per-stream max-seq vector from PROTOCOL.md §Recovery — each router’s stream is linear, so “what am I missing” is a handful of integers compared per RTRSYNC, then ranged backfill. (IBLT/minisketch is reserved for DM mailboxes, below, where no sequencer exists.) Router failure in a federated channel is a netsplit, not a sequencer election: reachable routers keep sequencing their own streams, liveness records mark the missing router unreachable, and CHANSYNC fills that router’s stream only after it returns or another node with retained history can serve the exact range.
RTRSYNC cadence: 15 s over TCP links, 120 s over LoRa backbone links,
immediate flush on roaming events and channel-admin changes. Optional
directory, channel-record, tombstone, liveness, identity, and revoke rows may
be sent as TTL-retained compact deltas between full snapshots; receivers treat
omitted rows as “unchanged in this packet” and rely on the next full snapshot
to repair missed deltas.
The access-network spine gossips a bounded online directory/member snapshot
in RTRSYNC: one record per (UID,current router) online attachment, with a
liveness generation, current nick, and channel display names where that
attachment is presently seen. This is enough for cross-router DMs to route
without a shared-channel warm-up, for make-before-break roaming to keep two
routers live for one UID, and for a router that missed a JOIN or PART edge to
repair the presence state used by later BYE/QUIT rendering. The same channel
lists are used to restore a reconnecting same-UID roaming session on a new
router; JOIN validation remains centralized in the IRC layer, and sequenced
channel history still arrives through CHANSYNC rather than the directory record
itself. Each node bounds this per-federation cache with directory_limit
(default 1024 remote attachments; 0 disables the cap). When the cap is
exceeded the node evicts unreachable-router attachments first, older
same-UID attachments when a fresher reachable attachment exists, then the
least-recently-refreshed attachment by the local touch order. fed.directory_drop
counts those local evictions, and later RTRSYNC gossip can rehydrate the
entry. Eviction is not a BYE or tombstone; local IRC clients that had seen the
attachment in a channel may still receive QUIT cleanup so their presence view
matches the bounded cache. RTRSYNC also carries short-lived tombstones for
missed BYE edges; a
tombstone suppresses directory snapshots for the same UID and router at the
same or lower liveness generation, while another router’s live attachment
remains routable.
When multiple reachable attachments match the same nick or UID, outbound
lookup paths prefer the most recently heard attachment, the local refresh order
when no clock timestamp is available, then the higher liveness generation. This
steers path pins, ACK tracking, DCC control/data, KEYREQ, listener adverts, and
WHOIS toward the fresh router while preserving make-before-break fanout for
the same UID.
Router liveness records in RTRSYNC carry monotone reachability generations:
an unreachable router attachment renders as an IRC QUIT/netsplit locally
without being deleted from the directory, and a newer reachable record renders
the corresponding JOIN/netjoin and makes that attachment eligible for DM
egress again. The gateway also emits one *lrc channel NOTICE per affected
channel (rtN.lrc netsplit / rtN.lrc netjoin, with the liveness reason
when present) so clients have a stable split/heal label even though IRC JOIN
has no reason field.
Trusted TCP peer ingress can also carry RTRSYNC revoke-state rows. They are
generationed UID denylist state, not presence state: receivers ignore them on
radio ingress and for local identities, then apply only newer
(generation, writer_rtr) rows. Accepted revoke rows clear any cached identity
record and suppress later KEYRESP/RTRSYNC cache refreshes or chat from that
UID. Accepted clear rows persist as tombstones, so stale revoke rows from an
old peer snapshot cannot re-denylist a UID after key unrevoke or signed
Admin identity.unrevoke has converged.
Channel administration (the “who owns #foo” problem)
Section titled “Channel administration (the “who owns #foo” problem)”IRC’s classic failure: ops evaporate when the last op leaves. chIRpChat routers are services:
- First
JOINof a nonexistent channel creates it; the creator’s IK (not nick!) is recorded as founder in the router’s channel record. The access-network spine persists this record today so a rebooted router restores founder ownership, op grants, ban lists, simple modes, topic, and channel key before admitting the first post-boot JOIN. - Founder/ops manage the channel with normal IRC commands (
/mode +o,/kick,/topic); the gateway translates these toCHANCTLops that the client signs (SIGNED_FULLfor grants, Tag8 for routine kicks) and the router enforces by identity. Op rights survive disconnects, reboots, and device changes, because they bind to the key. When the node has a signedIdentityRecordcached for a loaded founder/op UID, that record is pinned against identity-cache eviction so acceptedROTATEoverlays continue to validate authority traffic under cache churn. - Channel records (founder, op list as UIDs, modes
+t +m +i +A +S +k, topic, keyed-JOIN secret, ban list) are persisted by the owning router and replicate across the federation in bounded RTRSYNC snapshots. Receivers apply only newer(record_rev, record_writer_rtr)snapshots and persist them under their local router scope before first JOIN enforcement, so a banned UID is silenced at the edge and never wastes backbone airtime. The gateway enforces+ttopic control, blocks non-op local speakers under+m, requires a pending operatorINVITEor durable op/founder status for local joins under+i, and suppresses non-op remoteCHANMSGframes under+musing the sender UID in the channel record. Under+A, remoteCHANMSGframes are rendered only after the sender has a learned router-attested directory attachment for that router. Under+S, remoteCHANMSGframes are rendered only after the sender’s latest retained checkpoint archive row is verified and non-mismatching. Under+k, non-op local joins must present the matching JOIN key; founders and durable ops can still recover a keyed channel after reboot without typing the key. Newer replicated channel records fan out topic, simple mode, and currently local op/ban nick changes to already-open IRC channel state; replicated keys update the live JOIN gate without rendering the secret back to clients. /modeextensions surfaced through standard IRC syntax:+Srequires Tier-3 checkpoints for remote speakers;+A(router-attested users only) drops unregistered drive-by traffic today.
DM delivery and mailboxes
Section titled “DM delivery and mailboxes”DMs route sender → sender's router → (backbone) → recipient's router → recipient using registration records gossiped in RTRSYNC (UID → current
router(s), a DHT-free directory that fits comfortably in a Pi’s RAM and in
an ESP32 router’s PSRAM LRU for the active subset). The current
implementation gossips online attachments keyed by (UID,current_rtr), so
the same UID can stay reachable through another router when one attachment
quits; tombstones remove only the matching attachment. Online DMs use explicit
ACK: the sender retries the same MSGID if either the DM frame or the ACK is
lost, while the receiver re-ACKs duplicates without rendering a second IRC
line. A router that can deliver the DM to a local attachment still relays the
frame when it has learned another online attachment for the same UID, so a
make-before-break roam can receive on both routers. If the recipient is
offline but known locally and no reachable remote attachment for that UID is
known, the recipient’s router now mailboxes the DM with TTL (default 7
days, config), ACKs only after storage, and flushes it when that IRC identity
reconnects. An old standby router that still knows the identity suppresses a
new mailbox copy when RTRSYNC has already shown a reachable newer attachment;
the active attachment is left to ACK the DM. The core has a MailboxStore
seam, so a configured warm-tier adapter can reload those records after reboot;
lrcd --state-dir stores them in per-recipient mailbox files. Duplicate
retries are deduped by (SRC, MSGID) and re-ACKed without delivering a second
IRC line. Reloaded mailbox snapshots are normalized the same way before
delivery: the destination UID is restored from the snapshot key, duplicate
live rows collapse first-wins, and any tombstone for (SRC, MSGID) wins over
a live row. Delivered DMs become bounded deletion tombstones retained until
their mailbox TTL horizon; live rows that expire locally become tombstones with
a fresh suppression horizon, while expired tombstones are pruned. Standby
routers now reconcile dirty mailboxes over target-aware authenticated peer links
with the MAILBOXSYNC control packet. The sender targets a reachable same-UID
remote attachment learned from RTRSYNC; no broadcast peer egress is used,
radio ingress is dropped, and the receiver processes only a locally known UID or
already materialized mailbox. Dirty mailboxes first send an LCMI inventory:
row key, expiry, tombstone state, and 64-bit row digest, with no message text.
The peer answers with LCMR for rows it is missing or where the remote row is
newer, and the sender responds with an LCMB row subset. Node::import_mailbox_snapshot
merges those rows into the local mailbox using the same deterministic set-union
rule, persists the result when a mailbox store is configured, delivers imported
live rows once on reconnect, and keeps replayed stale live rows suppressed by
tombstones. Large inventories, requests, or row subsets use bounded LPP
fragmentation over the same target peer path.
Anti-congestion (“virtual radio traffic jams”)
Section titled “Anti-congestion (“virtual radio traffic jams”)”- Single-sequencer channels mean fan-out is a router broadcast per access network — one transmission serves every local member. This is the single biggest airtime win over flood meshes: N members cost 1 TX, not N.
- Routers shape per-lane duty cycle (regulatory + configured budget) with
a token bucket;
PRIOpackets (ACK, admin, DCC control) preempt. - Channel-activity detection (CAD) before every TX; CAD-fail backoff is randomized 120–480 ms (we copied MeshCore’s constants, they’re field-proven).
- Relays add jitter
U(0, airtime×2.5)before re-TX so parallel relays don’t collide twice (MeshCore’s delay model). - Backpressure: a router whose TX queue passes high-water emits
BEACONwith abusybit; clients stretch their uplink pacing (and the TUI shows it, so humans stop typing into a wall). - Anchor offload (client lane promotion). The anchor lane is the one
channel every client must hear, so it is also the scaling chokepoint. The
core
Nodeimplements both ends of promotion so it is identical onlrcdand firmware (and host-tested intest_node.cpp); the decision is the pureLaneSchedule::promote_clientpolicy described in RADIO.md §Lane promotion, which this layer only carries:- Router side. A promotion-enabled router
Nodeowns aRegistrar(the HELLO/WELCOME state machine) and an advertised lane plan (Node::set_lane_plan). The embedding records the SNR it measured on each client’s uplink —Node::record_client_uplink_snr(uid, snr_x10, now)on firmware from the radio, onlrcdfrom a link-quality estimate — or a failed/absent uplink (Node::record_client_uplink_fail(uid, now)), which stamps the per-clientRegistration(SNR + the recent-uplink health runs the hysteresis reads back) and immediately re-runs the lifecycle for that client. A periodic sweep (Node::promote_clients, also driven fromtick) re-evaluates every measured client.- The decision is the full promotion lifecycle
(
LaneSchedule::promote_lifecycle, RADIO.md §Lane promotion), not a one-shot promote: GRANT a first fast lane, RENEW the live grant once the clock enters the renew lead window (default 30 s before its TTL lapses) so a sustained client never silently falls back, DEMOTE a decaying client back to the anchor (a sustained uplink-SNR drop past the lane’s sustain floor with hysteresis, a run of failed/absent uplinks, or a lapsed lease), or HOLD. The lifecycle thresholds areNode::Config::lane_lifecycle_policy(ClientPromotionLifecyclePolicy): renew lead, demote SNR margin + run, failure run. - GRANT/RENEW emit a
LANEGRANTunicast Tag8’d with that client’s session key (lane.grant_tx) and re-stamp the live grant on theRegistration(lane, TTL, expiry) so the next sweep can renew or demote it. The lease length is the embedding’slane_grant_ttl_s; the controller’s per-decision TTL is only a floor/term marker. - DEMOTE does not put any new frame on the air. The wire
LANEGRANTonly ever carries a promotion lane, and “return to the anchor” is exactly the absence of a live grant: the router clears the client’s promoted state (Registrar::note_demote), and the client’s own TTL countdown drops it back to the anchor lane. Silence is the correct, lowest-airtime demotion; a demote bumpslane.retunefor visibility. - A router on the anchor-only plan, below the congestion floor, or with promotion disabled emits nothing.
- The decision is the full promotion lifecycle
(
- Client side. The embedding drives its own
RegClienthandshake and, once registered, hands theNodethe derived 16-byte session key and the issuing router’s UID (Node::set_session_key(local_uid, sk, router_uid)). TheNodeaccepts an inboundLANEGRANTonly when it is addressed to a local identity, itssrcis the router that issued that session key, and its Tag8 verifies under that identity’s recorded session key; it then records the granted lane + TTL (Node::granted_lane, plus an optionalLaneGrantSink) and bumpslane.grant_rx. An off-target grant, a grant from a sender other than the issuing router, a bad/missing tag, or a grant for an identity with no recorded key is dropped silently — promotion can never be forced onto a client. The router-srccheck is defense-in-depth: a session key is shared with exactly one router, so a frame tagged with a leaked/forged key from any other sender is still rejected. Applying the granted lane to the radio (the actual retune) stays in firmware behind itslane retune onRF opt-in; the grant simply lapses back to the anchor when the TTL expires. - Presence (BYE) authenticity. A
BYEevicts a presence, so a forged one is a denial-of-service against the named user. When theBYE’ssrcis a client this router registered, the router holds its session key (via theRegistrar) and requires a valid Tag8 before acting — a forged or untagged eviction is dropped (rx_badtag). ABYEfor a UID this router did not register carries no key it could check; it is a federation announcement from the router that owns that user, and its authority is the federation link itself (the same trust boundary every other cross-router frame rides), so it is acted on as before. The tightening applies only to the locally-registered case.
- Router side. A promotion-enabled router
What deliberately does NOT exist
Section titled “What deliberately does NOT exist”- No global flood routing of chat traffic. Discovery floods are TTL≤3 and rate-limited per node.
- No DHT. Directory = federation gossip; it’s small data.
- No store-everything-forever; stores are rings with TTLs (STORAGE.md).
- No automatic transmission of GPS position, ever. (
/msg *lrc loc set …exists for operators who want a router to advertise siting info.)