From 0dbe34b11cea93e1a4d46f88729ee3a2501c9fc8 Mon Sep 17 00:00:00 2001 From: bahdotsh Date: Wed, 30 Sep 2026 20:53:45 +0530 Subject: [PATCH 1/2] docs(spec): the backbone is a gateway property The gateway contract's backbone section said "between gateways, the wide-area carrier is Reticulum" and stated the Verdict rule for "a Reticulum gateway". The device never touches a backbone: it speaks the daemon contract over local IP, stores the tokens the gateway advertises, and reads none of them on any decision. Restate the section as what any backbone owes a gateway (gateway-to-gateway presence, arbitrary-size framing, a provisioned peer list, a declared rate class), with Reticulum as the reference backbone in a subsection of its own. The token becomes the family backbone__v1: a client ignores a kind it does not know, a gateway may advertise none, and the token is advisory and never a routing input. backbone_reticulum_v1 keeps its spelling as the first member. A device attaches to one gateway daemon at a time, because every gateway answer is recorded against the one daemon carrier and a second daemon would overwrite the first one's facts. ADR 0026 records the decision and why two daemons need two transport types. The reticulum_* entry points, the transport variant and the manager files keep their names; their doc comments now say what they name, the gateway daemon carrier after its reference backbone. Threat model R10's abuse-budget argument is per rate class. No frame, verb or token changes. --- CHANGELOG.md | 15 ++ .../src/reticulum.rs | 23 +- crates/offline-protocol-uniffi/src/lib.rs | 21 +- .../src/offline_protocol.udl | 15 +- crates/offline-protocol/src/protocol/mod.rs | 10 +- crates/offline-protocol/src/protocol/send.rs | 3 +- crates/offline-protocol/src/protocol/types.rs | 7 +- ...0026-the-backbone-is-a-gateway-property.md | 129 +++++++++++ docs/adr/README.md | 1 + docs/reticulum.md | 2 +- docs/security/threat-model.md | 16 +- docs/spec/gateway-contract.md | 200 ++++++++++++++---- 12 files changed, 373 insertions(+), 69 deletions(-) create mode 100644 docs/adr/0026-the-backbone-is-a-gateway-property.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 551396f87..8fd7c3f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -343,6 +343,21 @@ archived by series under [docs/changelog/](docs/changelog/); see the `ProtocolManager` already passes it, so only code that builds an `InternetManager` directly needs `app_id=`. +- **The backbone is a gateway property.** The gateway contract's backbone + section is restated as what any backbone owes a gateway (gateway-to-gateway + presence, arbitrary-size framing, a provisioned peer list, a declared rate + class), with Reticulum as the reference backbone in a subsection of its + own. A gateway advertises each backbone as a `backbone__v1` + capability token; a client ignores a kind it does not know, a gateway may + advertise none, and the token is advisory and never a routing input. A + device attaches to one gateway daemon at a time, because every gateway + answer is recorded against the one daemon carrier. No frame, verb or + token spelling changes: `backbone_reticulum_v1` is the family's first + member. The `reticulum_*` entry points keep their names and their doc + comments now say what they name, the gateway daemon carrier after its + reference backbone. [ADR 0026](docs/adr/0026-the-backbone-is-a-gateway-property.md) + records the decision. + ## [0.27.0] — 2026-09-25 > **Replicated documents can be removed, narrowed, and carry their bytes diff --git a/crates/offline-protocol-transport/src/reticulum.rs b/crates/offline-protocol-transport/src/reticulum.rs index 21941d68d..39630f247 100644 --- a/crates/offline-protocol-transport/src/reticulum.rs +++ b/crates/offline-protocol-transport/src/reticulum.rs @@ -1,10 +1,12 @@ -//! Reticulum mesh transport queue engine. +//! The gateway daemon transport's queue engine. //! -//! Long-range, low-bandwidth, resilient mesh networking via the Reticulum -//! network stack (LoRa, TCP, UDP, serial, I2P, and other mediums). No -//! Reticulum link is opened here: the platform side bridges to a running -//! Reticulum daemon (sidecar, TCP gateway, or embedded Python); the Rust -//! side manages queues, metrics, and the confirmation loop. +//! Named after Reticulum, its reference backbone: long-range, low-bandwidth, +//! resilient mesh networking over LoRa, TCP, UDP, serial, I2P and other +//! mediums. The name is the slot's, not a requirement on what stands +//! behind the daemon. No backbone link is opened here: the platform side +//! bridges to a gateway daemon over local IP, the daemon owns whatever +//! backbone it has, and the Rust side manages queues, metrics, and the +//! confirmation loop. //! //! The bridge contract: the platform reports daemon connectivity via //! [`ReticulumTransport::on_status_changed`], drains outbound wire bytes @@ -23,11 +25,12 @@ use std::time::{Duration, Instant}; use crate::common::recalculate_delivery_ratios; -/// Reticulum mesh transport implementation. +/// The gateway daemon transport, named after its reference backbone. /// -/// Provides connectivity via the Reticulum network for long-range, -/// low-bandwidth, resilient mesh networking. The platform bridges to a -/// gateway daemon speaking the contract in `docs/spec/gateway-contract.md`. +/// The platform bridges to a gateway daemon speaking the contract in +/// `docs/spec/gateway-contract.md`. The backbone behind that daemon, +/// Reticulum or another, is the gateway's property and reaches this crate +/// as nothing more than a capability token (ADR 0026). /// /// Reconnection, the attach handshake and the verdict loop are the platform /// bridge's, not this type's: it never opens a socket, so a timeout or a diff --git a/crates/offline-protocol-uniffi/src/lib.rs b/crates/offline-protocol-uniffi/src/lib.rs index e0b5d276a..955de45cf 100644 --- a/crates/offline-protocol-uniffi/src/lib.rs +++ b/crates/offline-protocol-uniffi/src/lib.rs @@ -4875,8 +4875,16 @@ impl OfflineProtocol { // ======================================================================== // RETICULUM TRANSPORT // ======================================================================== - - /// Called by the platform when the Reticulum daemon connection status changes. + // + // The `reticulum_*` names are the transport slot's, after its reference + // backbone, and they are the generated API of three bindings. The slot + // is the gateway daemon carrier: a bridge speaks the daemon contract + // over local IP, and what stands behind the daemon is the gateway's + // property, learned here only as a `backbone__v1` token that + // nothing reads (ADR 0026). + + /// Called by the platform when the gateway daemon connection status + /// changes. pub fn reticulum_status_changed(&self, is_connected: bool) -> Result<(), ProtocolError> { // Atomically read previous state and update in a single lock scope let was_connected = { @@ -5118,7 +5126,10 @@ impl OfflineProtocol { } /// Reticulum: capability tokens from the gateway's `Capabilities` answer - /// (e.g. `gateway_v1`, `backbone_reticulum_v1`). + /// (e.g. `gateway_v1`, `backbone_reticulum_v1`). A `backbone__v1` + /// token says what stands behind the daemon; the SDK stores it and + /// reads nothing from it, and a kind it does not know is kept like any + /// other token. /// /// The bridge MUST call this before `reticulum_status_changed(true)` on /// each attach — the contract's own ordering, so a device never drains @@ -5139,8 +5150,8 @@ impl OfflineProtocol { /// Reticulum: gateway-sourced presence for a peer (`PresenceStatus`). /// - /// `online=true` records the fact against Reticulum and re-drives that - /// peer's parked messages **over Reticulum**, because that is the carrier + /// `online=true` records the fact against this carrier and re-drives that + /// peer's parked messages **over this carrier**, because it is the one /// that just proved it can reach them; `online=false` parks pending /// welcomes without burning retry budget. Emits `presence_updated` with /// `source: reticulum` — except for self, blocked, or empty peer ids, diff --git a/crates/offline-protocol-uniffi/src/offline_protocol.udl b/crates/offline-protocol-uniffi/src/offline_protocol.udl index eaa701a3e..ee26ff2b1 100644 --- a/crates/offline-protocol-uniffi/src/offline_protocol.udl +++ b/crates/offline-protocol-uniffi/src/offline_protocol.udl @@ -1383,10 +1383,13 @@ interface OfflineProtocol { void reticulum_address_declaration_refused(string reason); // Reticulum: capability tokens from the gateway's Capabilities answer - // (e.g. "gateway_v1", "backbone_reticulum_v1"). MUST be called BEFORE - // reticulum_status_changed(true) on each attach — the contract's own - // ordering, so a device never drains into a gateway whose features it has - // not been told. Wholesale replace; the SDK clears the set itself when + // (e.g. "gateway_v1", "backbone_reticulum_v1"). A backbone__v1 + // token says what stands behind the daemon; the SDK stores it and reads + // nothing from it, and a kind it does not know is kept like any other + // token. MUST be called BEFORE reticulum_status_changed(true) on each + // attach: the contract's own ordering, so a device never drains into a + // gateway whose features it has not been told. Wholesale replace; the + // SDK clears the set itself when // the transport drops. Bounded like the relay's (64 tokens of 128 bytes) // and kept in its own set, so a token from whatever box is on the venue // Wi-Fi cannot open the relay's group-broadcast gate. @@ -1394,8 +1397,8 @@ interface OfflineProtocol { void reticulum_gateway_capabilities(sequence capabilities); // Reticulum: gateway-sourced presence for a peer (PresenceStatus). - // online=true records the fact against Reticulum and re-drives that - // peer's parked messages OVER RETICULUM, because that is the carrier that + // online=true records the fact against this carrier and re-drives that + // peer's parked messages OVER THIS CARRIER, because it is the one that // just proved it can reach them; online=false parks pending welcomes // without burning retry budget. Emits presence_updated with // source: reticulum, except for self, blocked, or empty peer ids. diff --git a/crates/offline-protocol/src/protocol/mod.rs b/crates/offline-protocol/src/protocol/mod.rs index c9cf6dc99..e31cf2e09 100644 --- a/crates/offline-protocol/src/protocol/mod.rs +++ b/crates/offline-protocol/src/protocol/mod.rs @@ -172,12 +172,18 @@ pub struct OfflineProtocol { /// seams. In-memory only, and absent facts mean today's behaviour. pub(crate) reachability: reachability::ReachabilityFacts, - /// Capability tokens the attached Reticulum gateway advertised. + /// Capability tokens the attached gateway daemon advertised. /// /// Delivered at attach, before the bridge reports the carrier available, /// and cleared when the carrier drops: a stale advertisement outlives the /// gateway that made it, and a reconnect may land on a different one. /// + /// Stored and never read on a decision. The backbone behind the daemon + /// is the gateway's own property and reaches this device only as a + /// `backbone__v1` token in this set; the set is for an application + /// to show, and the first production reader would turn a string set by + /// whoever holds the socket into a routing input (ADR 0026). + /// /// Kept apart from `group_mesh.relay_capabilities` deliberately, though /// both are capability sets from a gateway. That one gates the relay /// group-broadcast path, and a token advertised by whatever box is @@ -2579,7 +2585,7 @@ impl OfflineProtocol { } } - /// Records what the attached Reticulum gateway says it can do. + /// Records what the attached gateway daemon says it can do. /// /// Wholesale replace, like the relay's: each attach describes the gateway /// actually connected now. The bridge calls this **before** it reports the diff --git a/crates/offline-protocol/src/protocol/send.rs b/crates/offline-protocol/src/protocol/send.rs index 02fb7e0dd..cda141464 100644 --- a/crates/offline-protocol/src/protocol/send.rs +++ b/crates/offline-protocol/src/protocol/send.rs @@ -5700,7 +5700,8 @@ impl OfflineProtocol { } } - // Reticulum excluded: LoRa bandwidth (~0.7 KB/s typical) is unsuitable for media transfer. + // The daemon carrier is left out of the fallback order: its reference + // backbone is scarce-class (gateway-contract.md, rate class). for preferred in [ TransportType::Internet, TransportType::WiFiDirect, diff --git a/crates/offline-protocol/src/protocol/types.rs b/crates/offline-protocol/src/protocol/types.rs index eed4c2e33..b245b3b3e 100644 --- a/crates/offline-protocol/src/protocol/types.rs +++ b/crates/offline-protocol/src/protocol/types.rs @@ -40,8 +40,11 @@ pub enum GatewayCarrier { /// The internet relay, which implemented every verb before the contract /// named them. Internet, - /// A gateway daemon reached over the Reticulum transport's local IP - /// contract. + /// A gateway daemon reached over local IP, through the transport slot + /// named `Reticulum` after its reference backbone. The variant names + /// the carrier, not the backbone: what stands behind the daemon is the + /// gateway's property, and this device learns of it only as a + /// capability token it never reads (ADR 0026). Reticulum, } diff --git a/docs/adr/0026-the-backbone-is-a-gateway-property.md b/docs/adr/0026-the-backbone-is-a-gateway-property.md new file mode 100644 index 000000000..863a58028 --- /dev/null +++ b/docs/adr/0026-the-backbone-is-a-gateway-property.md @@ -0,0 +1,129 @@ +# 0026. The backbone is a gateway property + +**Status:** Accepted + +## Context + +The [gateway contract](../spec/gateway-contract.md) was written when one +backbone existed. Its backbone section opened with "between gateways, the +wide-area carrier is Reticulum", the Verdict rule was stated for "a Reticulum +gateway", and the one capability token a gateway advertised for its backbone +was `backbone_reticulum_v1`, with no rule for a second. The SDK's own names +followed: the transport slot, the `reticulum_*` FFI entry points and the two +mobile managers are all named after that backbone, and their doc comments +said "the attached Reticulum gateway" for what is, on the wire, a daemon +reached over local IP. + +None of that is what the device does. Four facts, verified in the tree: + +**The device never touches a backbone.** The transport opens no backbone +link, holds no backbone identity and sees no backbone frame. The bundled +managers speak newline-delimited JSON to a daemon at a configurable local +address, and everything past the daemon is the daemon's. + +**The engine stores the gateway's tokens and reads none of them.** The +capability set a gateway advertises at attach is kept, bounded and cleared on +drop, and the only readers of the set are tests. No send path, no parking +decision and no transport choice consults a token. The `mailbox_v1` +behaviour that looks like a token-driven feature is driven by the `stored` +and `pushed` flags on each verdict, and the token is the gateway's promise to +honour them, not the switch. + +**Every gateway answer is recorded against a transport type.** The +reachability facts a verdict or a presence answer produce are keyed by +recipient and by `TransportType`, and the newest answer for a carrier +replaces the last. The engine holds one transport per type, and the FFI +holds one daemon connection state. Two daemons attached through the one +slot would overwrite each other's facts recipient by recipient, and neither +would be wrong on its own terms. + +**Forty-odd guards pin the names.** Source guards in the FFI crate read the +mobile managers by path and assert their shape, and the `reticulum_*` entry +points are the generated API of three bindings. Renaming the slot is a +three-language break for a property the device does not have. + +## Decision + +1. **The backbone is the gateway's.** The contract states what a backbone + owes a gateway (gateway-to-gateway presence, arbitrary-size framing, a + provisioned peer list, a declared rate class) as obligations on the + gateway, with Reticulum as the reference backbone in a subsection of its + own. Nothing a device does depends on the section. +2. **The device learns the backbone only as a token.** A gateway advertises + each backbone it reaches as one token of the family `backbone__v1`. + A client ignores a kind it does not know and keeps the session. A + gateway may advertise no backbone token, and is a conforming gateway. + `backbone_reticulum_v1` is the first member, unchanged in spelling. +3. **The token is advisory and never a routing input.** The engine keeps + storing the set and keeps reading no token from it. A device does not + choose a carrier, park a message or size a frame because of a backbone + token. A token is set by whoever holds the socket, so a routing choice + that trusts it is a routing choice the gateway makes for the device, + and invariant 3 of the contract (no facts, no change) forbids that. +4. **One attached gateway daemon at a time.** The daemon link is one carrier + however many backbones stand behind it, and the facts it produces are + keyed by that carrier. A second daemon on the same device needs a second + transport type, which is a change to the transport set (a sixth variant, + every exhaustive match, the selector's ranking, the bridges' managers) + and is deferred until a deployment needs two daemons on one device. A + device that wants a second gateway's reach gets it through the first: the + gateways are peers over the backbone. +5. **The names stay.** The `reticulum_*` FFI entry points, the transport + variant and the manager file names keep their spelling. They name the + transport slot, after its reference backbone. Their doc comments say + what the slot is: the gateway daemon carrier, whose backbone is the + gateway's property. + +## Consequences + +- A gateway on another backbone, or on none, attaches the same devices with + the same managers and no SDK change. What it advertises differs by one + token, which nothing reads. +- The threat model's abuse-budget argument for gateways is per rate class: + the budgets a gateway applies are sized to what its backbone can carry, + and a device cannot verify either the class or the budgets. That was + already true for one backbone and is now stated for any. +- The `reticulum_*` names remain a small standing confusion for a reader + who meets them first. The doc comments carry the correction, and a rename + is future debt that would cost a three-binding API break to pay. +- Two daemons on one device is not supported, and a bridge that opens a + second connection to a second daemon while the first is attached produces + facts that overwrite each other. The contract says so. Nothing enforces + it in the engine, because the engine sees one carrier. + +## Alternatives considered + +**A transport variant per backbone kind.** Honest about what a second daemon +would need, and wrong about what a device does: the device speaks one +protocol to a daemon whatever stands behind it, so a variant per kind would +put a property the device cannot observe into an enum the device branches +on, and every exhaustive match would grow for a distinction with no +behaviour behind it. + +**Rename the slot to something neutral now.** The right name, at the cost of +a three-binding API break, forty guard edits and two manager renames, for a +change with no behaviour in it. The doc comments carry the correction +instead, and the rename waits for an API break that is happening anyway. + +**Carry the backbone in a wire field rather than a token.** A field is a +promise that something reads it. Nothing should, for the reason in decision +3, and a token in a set that is already bounded, cleared on drop and +documented as never driving a security decision is the shape that makes that +hard to forget. + +## What would undo this + +A send path, a parking decision or a transport ranking that reads a +`backbone_*` token. The getter exists for tests and for an application that +wants to show the token; the first production reader turns an advisory +string set by the socket's holder into a routing input, and does so without +anything failing. + +A second daemon attached through the one slot. It works, in the sense that +frames flow, and the two gateways' verdicts overwrite each other from then +on. + +A backbone obligation restated in device terms, such as a device-side frame +ceiling sized to a backbone's MDU. The device does not know what the +backbone is, and the day it is told, every device is coupled to one +deployment's link. diff --git a/docs/adr/README.md b/docs/adr/README.md index 456fa5b5e..38b658e58 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -36,6 +36,7 @@ silently undo it. Decisions that follow from the obvious default do not need one | [0023](0023-a-control-frame-states-when-it-was-made.md) | A control frame states when it was made | Accepted | | [0024](0024-the-sdk-ships-its-own-tls-stack.md) | The SDK ships its own TLS stack for telemetry | Accepted | | [0025](0025-native-packages-are-assembled-in-place.md) | Native packages are assembled from the bridge sources in place | Accepted | +| [0026](0026-the-backbone-is-a-gateway-property.md) | The backbone is a gateway property | Accepted | ## Format diff --git a/docs/reticulum.md b/docs/reticulum.md index cbcfef25b..b66ed0391 100644 --- a/docs/reticulum.md +++ b/docs/reticulum.md @@ -6,7 +6,7 @@ The Reticulum transport provides long-range, resilient mesh networking via the [ Reticulum is one of five transports in the Offline Protocol SDK, alongside BLE, Wi-Fi Direct, Internet and Nostr. It is disabled by default because it requires external infrastructure (a running Reticulum instance, an RNode radio, or a gateway). -> **This repository ships the device half.** The Rust transport opens no Reticulum link of its own: it manages queues, metrics and the confirmation loop, and expects the platform to bridge to a real Reticulum stack. Both mobile managers now speak [the gateway daemon contract](spec/gateway-contract.md) to a configurable address: they attach with a signed address declaration, settle each send on the gateway's verdict, and watch presence. What answers on the other end is a gateway daemon built to that contract, which is a deployment rather than something this SDK ships. With nothing listening at `daemonAddress`, enabling Reticulum gives you a transport that never becomes available. +> **This repository ships the device half.** The Rust transport opens no Reticulum link of its own: it manages queues, metrics and the confirmation loop, and expects the platform to bridge to a real Reticulum stack. Both mobile managers now speak [the gateway daemon contract](spec/gateway-contract.md) to a configurable address: they attach with a signed address declaration, settle each send on the gateway's verdict, and watch presence. What answers on the other end is a gateway daemon built to that contract, which is a deployment rather than something this SDK ships. With nothing listening at `daemonAddress`, enabling Reticulum gives you a transport that never becomes available. The transport is named after its reference backbone, and that is all the name means: the daemon contract does not require Reticulum behind the daemon, the backbone is [the gateway's property](spec/gateway-contract.md#the-backbone), and this device learns of it only as a `backbone__v1` capability token that nothing in the SDK reads ([ADR 0026](adr/0026-the-backbone-is-a-gateway-property.md)). ## When to Use Reticulum diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index a75cfd1a5..acc53d06d 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -459,10 +459,18 @@ enumeration above and is unchanged. **Mitigations specified but not enforceable from here**: the per-device and per-peer token-bucket budgets against backbone exhaustion are the gateway's own -to apply, so a device cannot verify that its gateway applies them. This is -listed apart from the others on purpose: a threat model that reads as -protection when the protection is somebody else's to implement is the failure -this document exists to prevent. +to apply, so a device cannot verify that its gateway applies them. They are +sized per [rate class](../spec/gateway-contract.md#what-a-backbone-owes-a-gateway): +a scarce-class backbone (a few bps up to LoRa-class kbps) carries direct +messages, acknowledgements and control frames and excludes media, and a +broad one carries what the daemon link accepts under the same budgets. The +class follows from the kind the gateway advertises, a token the device +stores and never acts on, so a gateway that applies the wrong class delays +every device behind it (the latency-and-battery cost above) and changes no +decision on any of them. +This is listed apart from the others on purpose: a threat model that reads +as protection when the protection is somebody else's to implement is the +failure this document exists to prevent. **What would close it:** nothing closes the lying-gateway case, because the lie is about someone else's state. Provisioning is the real control: gateways are diff --git a/docs/spec/gateway-contract.md b/docs/spec/gateway-contract.md index 5755e7a84..9159bb868 100644 --- a/docs/spec/gateway-contract.md +++ b/docs/spec/gateway-contract.md @@ -1,9 +1,11 @@ # The gateway contract A **gateway** bridges a zone (a governed BLE / Wi-Fi Direct flood) to somewhere -a zone cannot reach: the internet, or a wide-area Reticulum backbone. This -document specifies what a gateway must do to be one, and the wire protocol a -device speaks to a gateway daemon over local IP. +a zone cannot reach: the internet, or a wide-area backbone. This document +specifies what a gateway must do to be one, the wire protocol a device speaks +to a gateway daemon over local IP, and what a backbone owes a gateway. The +backbone is the gateway's property: a device never touches one, and learns +that one exists only as a token at attach. The contract is deliberately a *reframing* rather than an invention. The internet relay already implements every verb below; naming them is what lets a @@ -69,8 +71,10 @@ It is the reason the contract exists. A device's transport policy is otherwise blind to where the recipient is: a carrier being up counts as reachable for every recipient. The verdict is the only thing that contradicts that. -- A Reticulum gateway MUST implement Verdict, or it is not a gateway. This is - what closes the Reticulum half of the mixed-neighbourhood residual. +- A gateway daemon MUST implement Verdict, whatever backbone stands behind it, + or it is not a gateway. This is what closes the daemon half of the + mixed-neighbourhood residual, recorded against Reticulum when that was the + only backbone. - **Nostr structurally cannot implement Verdict.** A broadcast relay reports no per-recipient delivery. Nostr therefore remains a carrier and never becomes a gateway, and its half of the residual is permanent. This is recorded so the @@ -345,6 +349,12 @@ outlives the gateway that made it. Capability tokens MUST NOT drive a security decision. They gate features, and an attacker who can set them is already the gateway. +`backbone_reticulum_v1` is one member of the family +[`backbone__v1`](#the-backbone-token), which is how a gateway says what +stands behind it. The family's rules, including that a client ignores a kind +it does not know and that a gateway may advertise no backbone at all, are in +the backbone section. + ### Status ``` @@ -364,42 +374,123 @@ A gateway that wants to speak to the zone speaks ordinary frames. ## The backbone -Between gateways, the wide-area carrier is Reticulum. Wide-area routing over -intermittent, slow, heterogeneous links is the problem Reticulum spent a decade -solving, and this protocol does not reinvent it. - -### Gateway-owned destinations +Between gateways runs a wide-area carrier this document calls the backbone. +It is the gateway's property, never the device's: a device opens no backbone +link, holds no backbone identity, and learns that a backbone exists only as a +capability token at attach. Everything in this section binds the gateway. The +device half of this chapter is complete without it, which is what lets a +gateway change its backbone, or run with none, and have no device notice more +than a token. + +### What a backbone owes a gateway + +A carrier is a backbone for a gateway when the gateway can get four things +from it. Each is stated with what its absence costs, because a backbone that +provides three of the four fails in a way the device cannot see. + +1. **Gateway-to-gateway presence.** A gateway MUST be able to ask its peer + gateways whether a recipient is attached to them, and MUST answer the + same question when asked. This is the Presence verb reused between + gateways, and it is what turns "not attached here" into a verdict rather + than a guess: without it every recipient outside the local zone is + unreachable and the backbone carries nothing. +2. **Arbitrary-size framing.** A frame accepted from a device MUST reach the + peer gateway whole, up to the largest line the daemon link accepts (the + daemon's own line bound; the reference clients' is 1 MiB, per + [Framing](#framing)). How the backbone segments, sequences and + checks it is the backbone's business. A gateway MUST NOT ask a device to + size its frames to the backbone, because the device does not know what + the backbone is. +3. **A provisioned peer list.** The gateways a gateway asks and forwards to + are configuration, installed with it + ([ADR 0016](../adr/0016-gateways-are-provisioned-not-emergent.md)). A + backbone MAY offer discovery of its own, and a gateway MAY layer it on + later without changing the query shape; nothing here depends on it. +4. **A declared rate class.** Every backbone kind has one, and the gateway + applies it at its own egress. Two classes exist: + + | Class | Links | What egresses | + |-------|-------|---------------| + | `scarce` | A few bps up to LoRa-class kbps | Direct messages, acknowledgements and control frames only. Media MUST be excluded. | + | `broad` | Broadband | Whatever the daemon link accepts, under the gateway's budgets. | + + The class is a property of the kind, given in the [table of + kinds](#the-backbone-token); a kind not in that table is `scarce`. The + exclusion is the gateway's to enforce. The device side only prefers + against it: media pins to whichever carrier is current and the daemon + carrier is absent from the fallback order, so a device whose current + carrier is the daemon link does hand it media. A scarce gateway then + refuses each chunk with a plain-failure verdict (`budget_exceeded` or + `frame_too_large`, never `recipient_unreachable`) and the pinned + transfer fails on its retry ladder; the device does not know the class + (invariant 3), and that cost is the price of not knowing. + +### The backbone token + +A gateway advertises each backbone it can reach as one capability token of +the family -Reticulum announces are signed by the destination's own identity key. -Per-device backbone destinations would therefore require either phones running a -Reticulum stack (they do not) or gateways holding device private keys -(unacceptable). So: +``` +backbone__v1 +``` -- Each gateway announces **one** destination under its own Reticulum identity. -- An egress frame is wrapped: the outer layer is a link to the destination - gateway, the inner layer is the ordinary Offline Protocol wire message naming - the `off1` recipient. The gateway sees ciphertext and routing metadata only. -- A gateway locates a recipient's gateway from its own attach sessions and zone - presence, and by asking its peer gateways (the Presence verb, reused - gateway-to-gateway). The peer list is provisioned configuration; announce-based - discovery MAY layer on later without changing the query shape. +where `` is a lowercase name; the kinds this revision defines are in +the table below. + +- A gateway MAY advertise zero backbone tokens. A gateway with no backbone is + a conforming gateway: it attaches devices, answers verdicts for the + recipients it can see, and reports the rest unreachable. The zone gets mesh + fallback for everyone beyond it, which is invariant 3 doing its job. +- A client MUST ignore a kind it does not recognise and MUST keep the session. + An unknown kind is a gateway newer than the client, not a broken one. +- The token is **advisory, never a routing input.** A device MUST NOT choose + a carrier, park a message or size a frame on the strength of a backbone + token. What a device may do with one is show it, log it and hand it to the + application. The reference implementation stores the set and reads no + token from it, and + [ADR 0026](../adr/0026-the-backbone-is-a-gateway-property.md) records why + that is a decision and not an omission: a token is set by whoever holds + the socket, so a routing choice that trusts it is a routing choice the + gateway makes for the device. +- One kind per token. A gateway with two backbones of different kinds + advertises two tokens. + +| Kind | Rate class | Mechanism | +|------|------------|-----------| +| `reticulum` | `scarce` | [The Reticulum backbone](#the-reticulum-backbone) | + +`backbone_reticulum_v1` is the first member of the family and the only one +this revision defines. It was the token before the family was named, so a +gateway or client built to the earlier text agrees with this one. + +### One attached daemon at a time + +A device attaches to one gateway daemon at a time. Every gateway answer is +recorded against the carrier that gave it, and the daemon link is one carrier +however many backbones stand behind it. A second daemon on the same device +would therefore overwrite the first one's verdicts and presence answers with +its own, recipient by recipient, with neither wrong on its own terms. Two +daemons need two carriers, which is a change to the transport set and is +deferred, with that reason, in ADR 0026. A device that wants a second +gateway's reach gets it the way the zone does: the two gateways are peers +over the backbone, and the one daemon answers for both. -Recorded alternative, rejected for v1: device-signed announce blobs, where a -phone pre-signs its own announce and gateways republish it. It buys true device -mobility across zones at the cost of a new device-side crypto surface, lifetime -coupling between the `off1` identity and a Reticulum identity, and pressure on -the announce bandwidth budget. - -### Framing and the scarce path +### Gateway-owned destinations -The Reticulum MDU is 465 bytes and this protocol's frames routinely exceed it. -Gateway-to-gateway transfer therefore uses links with resources, which handle -sequencing, compression and integrity for arbitrary sizes, and gateways keep -long-lived links to provisioned peers. +The invariant, independent of the backbone kind: **a gateway is reachable on +the backbone under its own identity, and only under its own identity.** A +device's key never leaves the device, so no gateway can sign for a device, +and no gateway is handed a device key so that it could. It follows that a +device is reachable across the backbone only through the gateway it is +attached to, and that a device moving between zones moves nothing on the +backbone: the new gateway starts answering presence for it and the old one +stops. -Backbone links range from LoRa-class kbps down to a few bps, so **backbone -egress is a scarce path by default**: direct messages, acknowledgements and -control frames only. Media MUST be excluded unless a capability says otherwise. +Recorded alternative, rejected for v1: device-signed announce blobs, where a +device pre-signs its own backbone announce and gateways republish it. It buys +true device mobility across zones at the cost of a new device-side crypto +surface, lifetime coupling between the `off1` identity and a backbone +identity, and pressure on the backbone's announce budget. ### Re-origination into the zone @@ -411,6 +502,38 @@ and transport pinned ([ADR 0015](../adr/0015-relay-hint-frames-unacked-and-pinne a re-originated frame is the opposite disposition in both respects and needs no amendment to that decision. +### The Reticulum backbone + +The reference backbone, and the one the bundled managers were written against, +is [Reticulum](https://reticulum.network/). Wide-area routing over +intermittent, slow, heterogeneous links is the problem Reticulum spent a decade +solving, and this protocol does not reinvent it. This subsection is how +Reticulum meets the four obligations; nothing in it binds a gateway on another +backbone. + +- **Presence** is the Presence verb carried gateway-to-gateway over a + Reticulum link. +- **Framing.** The Reticulum MDU is 465 bytes and this protocol's frames + routinely exceed it, so gateway-to-gateway transfer uses links with + resources, which handle sequencing, compression and integrity for + arbitrary sizes, and gateways keep long-lived links to their provisioned + peers. +- **Peers** are provisioned destination hashes. Reticulum's own announces + MAY inform the list later without changing the query shape. +- **Rate class** `scarce`: Reticulum links range from LoRa-class kbps down + to a few bps. + +Destinations follow from the invariant above and from how Reticulum signs +them. A Reticulum announce is signed by the destination's own identity key, so +a per-device destination would need either a Reticulum stack on every device +(there is none) or gateways holding device private keys (which the invariant +forbids). Each gateway therefore announces **one** destination under its own +Reticulum identity. An egress frame is wrapped: the outer layer is a link to +the destination gateway, the inner layer is the ordinary Offline Protocol wire +message naming the `off1` recipient, and the gateway sees ciphertext and +routing metadata only. A gateway locates a recipient's gateway from its own +attach sessions and zone presence, and by asking its peer gateways. + ## Provisioning A gateway is a **deployment**, never a runtime state a phone reaches: a powered, @@ -430,8 +553,9 @@ a chokepoint, and nothing in this contract prevents a second. A gateway MUST bound what one attached device can consume: token-bucket budgets per attached device and per peer gateway, the pattern the zone's own forwarding -governor already applies per peer. Combined with the scarce-path rule, this caps -what a single device can do to a multi-bps backbone link. +governor already applies per peer. The budgets are sized to the backbone's +declared rate class, and combined with the class's exclusions they cap what a +single device can do to a multi-bps backbone link. The threats a gateway introduces (a fake gateway, verdict abuse, zone metadata at the operator, backbone exhaustion) are enumerated in the From 284a706cb2c91d29792001d0c12cabb8ac0ba580 Mon Sep 17 00:00:00 2001 From: bahdotsh Date: Thu, 1 Oct 2026 01:51:53 +0530 Subject: [PATCH 2/2] fix(bindings): regenerate the local API table for the definition's comments The table pins the definition's SHA-256, and this branch rewords two comments in it. --- bindings/python/offline_protocol_sdk/local_api/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/python/offline_protocol_sdk/local_api/table.py b/bindings/python/offline_protocol_sdk/local_api/table.py index 8a137537f..3d024bc3b 100644 --- a/bindings/python/offline_protocol_sdk/local_api/table.py +++ b/bindings/python/offline_protocol_sdk/local_api/table.py @@ -9,7 +9,7 @@ from __future__ import annotations -UDL_SHA256 = "d2b3a4e23be560b35388bfe45c5164ce4b7c002f5e11022700618c7e102823cb" +UDL_SHA256 = "cff2c11d29e6149e3d9c04919c03992fd2c1f760ecf20af1a6054e796851661e" TABLE = {'callbacks': ('MlsStorageProvider', 'ProtocolStateStorageProvider',