# Architecture: Reticulum-compatible Rust mesh stack with extensions

> **Status:** v0 draft. Authored as the design baseline for a new fork. Intended to be read by Claude Code, by human collaborators, and by reviewers from the broader Reticulum / RetiNet / Beechat communities.
>
> **Scope:** This document specifies the architecture for a Rust implementation that is **wire-compatible with the Reticulum Network Stack (RNS) reference protocol** at the gateway boundary, while permitting cleanly-isolated protocol extensions above that boundary. It is not a port of the Python reference. It builds on the layout established by `BeechatNetworkSystemsLtd/Reticulum-rs` (MIT) and the daemon work in `codeberg.org/lgh/Reticulum-rs` branch `daemon-mode`.
>
> **Audience:** Implementers. The reader is assumed to know Rust, async/await, basic cryptographic engineering (X25519, Ed25519, AEAD), and the existence of the Reticulum protocol family. Knowledge of the Python reference is not assumed; this document cites it where behavior is defined only there.

---

## 0. Why this exists

### 0.1 Context

The Reticulum reference implementation (`markqvist/Reticulum`) is a Python codebase of ~55,000 lines that defines, by behavior, the Reticulum protocol. The maintainer withdrew from public engagement in December 2025 ("Carrier Switch"). The reference license, changed in April 2025, contains anti-AI-training and no-harm clauses that block packaging in Debian, F-Droid, Alpine main, and most standard distribution channels. The protocol itself was dedicated to the public domain in 2016.

The community has organized:

- **RetiNet** (`codeberg.org/skyguy/retinet`) — AGPL-3.0 Python fork pre-license-change.
- **Reticulum-rs** (`crates.io/reticulum`, Beechat, MIT) — most mature non-Python implementation; ~3,600 lines covering Packet/Identity/Destination/Link/Transport with tokio async runtime.
- **rnsd-rs** (`codeberg.org/lgh/Reticulum-rs` branch `daemon-mode`) — community daemon on Beechat, running as a real transport node, reportedly <4% CPU vs Python rnsd's 20–100% spikes.
- **Formal specification effort** led by Jo Kroese, presented at FOSDEM 26 (Jan/Feb 2026), with stated outputs including "an initial conformance/test-vector plan that enables independent implementations."

### 0.2 Goals

In priority order:

1. **Wire compatibility** with Reticulum at the gateway boundary, sufficient to function as a transport node and as a terminal node carrying real application traffic (LXMF, file transfers via `rncp`, etc.).
2. **Architectural clarity**: a `no_std`-clean protocol core, with `std`/`tokio` confined to host adapters, suitable for both server deployments and (eventually) embedded targets.
3. **Extensibility**: a clean seam between Reticulum-compatible operation and protocol extensions (PQ link handshakes, hardware-backed identity, modern observability, MeshCore/Meshtastic adapters) that does not require Reticulum-network coordination to land.
4. **Operational quality**: structured logging via `tracing`, real metrics, clean configuration in valid TOML, proper error types, no global mutable state.
5. **Distribution-friendly licensing**: dual `MIT OR Apache-2.0` across all crates this fork originates; no contamination from post-April-2025 reference code or from AGPL forks.

### 0.3 Non-goals

- Bug-compatibility with the Python reference. We are compatible at the **wire** level; internal state machines are reimplemented from the spec and from observed protocol behavior, not transliterated from Python.
- Performance parity with the reference. We expect to be substantially faster on every dimension. This is not a goal because it's a side effect.
- Hosting the canonical protocol specification. That work belongs to Kroese et al.; this fork consumes their spec and contributes test vectors back.
- Replacing the Reticulum network. Existing nodes continue to function. This fork joins the network.

### 0.4 Naming

The crate family is unnamed in this document. Pick a name before first commit. Working placeholder: **`crate-name-tbd`**. The placeholder is used throughout; replace it before publication.

Constraints on the name: not "reticulum" (Beechat owns the crates.io slot), not a play on Mark's identifiers, not derivative of any existing implementation name. Suggestions are out of scope for this doc.

---

## 1. Architectural seam: gateway vs. extension

This is the single most important architectural decision in the document. Read it carefully.

### 1.1 The two layers

```
┌──────────────────────────────────────────────────────────────────┐
│                     APPLICATION LAYER                            │
│         (LXMF, your protocol, MeshCore bridge, etc.)             │
├──────────────────────────────────────────────────────────────────┤
│                     EXTENSION LAYER                              │
│   PQ link mode | hw-backed identity | structured RPC | metrics  │
│                                                                  │
│   Speaks "extended" framing to peers that negotiate it.         │
│   Falls back to gateway-native framing otherwise.               │
├──────────────────────────────────────────────────────────────────┤
│                     GATEWAY LAYER (reticulum-core)               │
│   Reticulum wire format. Announce / Path / Link / Resource.     │
│   Wire-compatible with the Reticulum network.                   │
├──────────────────────────────────────────────────────────────────┤
│                     INTERFACE LAYER                              │
│   TCP | UDP | RNode | Serial | KISS | Local | Auto-discovery    │
└──────────────────────────────────────────────────────────────────┘
```

The **gateway layer** is the part that has to be byte-exact with Reticulum. The **extension layer** is where we have design freedom. The seam is a Rust API, not a wire format — extensions never appear in Reticulum-compatible packets unless explicitly negotiated.

### 1.2 What this buys us

- **No coordination required to land extensions.** Reticulum-compatible peers see our nodes as ordinary Reticulum nodes. Extension-aware peers negotiate the better behavior.
- **Clean licensing story.** The gateway layer is implemented from the public-domain protocol spec and observed wire behavior. The extension layer is original work. No Reticulum-License-derived code, no AGPL contamination.
- **Embedded-friendly core.** Gateway is `no_std + alloc`. Extensions can be `std`-only. Interface adapters are `tokio`-bound. The split is enforced by the crate boundaries (§3).
- **Future-proof.** When (not if) the Reticulum protocol evolves under community stewardship, the gateway layer absorbs those changes; the extension layer is unaffected.

### 1.3 What this costs us

- **More crates.** ~8 crates instead of one. This is the right cost.
- **Discipline at the seam.** Extensions must never leak into Reticulum-compatible wire output. The crate boundaries enforce this; the test suite verifies it.
- **A negotiation mechanism.** Extension-aware peers need to discover each other. §6 specifies how.

---

## 2. Repository layout

Single Cargo workspace, multiple crates:

```
crate-name-tbd/
├── Cargo.toml                  # [workspace], shared deps, lints
├── Cargo.lock
├── LICENSE-MIT
├── LICENSE-APACHE
├── README.md
├── ARCHITECTURE.md             # this document
├── CONTRIBUTING.md
├── SECURITY.md
├── docs/
│   ├── wire-format.md          # extracted from manual + Python + Beechat
│   ├── extensions.md           # spec for our extension layer
│   ├── threat-model.md
│   ├── conformance.md          # test vector strategy
│   └── adr/                    # Architecture Decision Records
│       ├── 0001-bridge-and-extend.md
│       ├── 0002-no-std-core.md
│       ├── 0003-licensing.md
│       └── ...
├── crates/
│   ├── reticulum-core/         # no_std + alloc; wire types, no I/O
│   ├── reticulum-crypto/       # no_std + alloc; primitives wrapper
│   ├── reticulum-codec/        # no_std + alloc; framing, encoding
│   ├── reticulum-gateway/      # std; the gateway state machine
│   ├── reticulum-iface/        # std + tokio; interface adapters
│   ├── reticulum-ext/          # std; extension layer
│   ├── reticulum-rnsd/         # binary crate; the daemon
│   ├── reticulum-cli/          # binary crate; user-facing utilities
│   └── reticulum-vectors/      # test fixtures and harness
├── xtask/                      # build automation (cargo xtask ...)
└── .github/
    └── workflows/              # CI: build matrix, clippy, fmt, test, audit
```

### 2.1 Crate roles

| Crate | `std`? | `tokio`? | Role |
|---|---|---|---|
| `reticulum-core` | no | no | Wire-format types, address hashes, packet enums, packed/unpacked representations, error enum. **No I/O. No async.** Pure data. |
| `reticulum-crypto` | no | no | Curve25519/Ed25519/AES/SHA/HMAC/HKDF wrappers presenting a stable internal API. Backend selectable via features (`rustcrypto`, `ring`, future PQ). |
| `reticulum-codec` | no | no | Encode/decode of packets, announces, link handshake messages. Zero-copy where possible. The byte-level conformance target. |
| `reticulum-gateway` | yes | no* | The Reticulum-compatible state machine: identity, destinations, paths, links, resources, ratchets. **Channel-based; runtime-agnostic.** Embedders provide an executor. |
| `reticulum-iface` | yes | yes | TCP/UDP/RNode/Serial/KISS/Local/Auto interface adapters. Tokio-based. Speaks `mpsc<TxMessage>`/`mpsc<RxMessage>` to the gateway. |
| `reticulum-ext` | yes | yes | Extension layer: PQ link negotiation, hardware identity, structured RPC over LocalInterface, observability hooks. Depends on `reticulum-gateway`. |
| `reticulum-rnsd` | yes | yes | The daemon. Wires gateway + iface + ext + config. Drop-in replacement target for `rnsd`. |
| `reticulum-cli` | yes | yes | `rnstatus`/`rnpath`/`rnprobe`/`rncp`/`rnid`/`rnx` workalikes. |
| `reticulum-vectors` | no | no | Conformance test fixtures (byte-level packet captures) plus the harness that consumes them. Used as a `[dev-dependencies]` by other crates. |

*Note on `reticulum-gateway` and async: the state machine is expressed as `poll`-style methods and channel handles. It does **not** depend on `tokio`. The daemon binds it to `tokio`. An embedded target could bind it to `embassy`. This is the same pattern as `quinn-proto` vs `quinn`, or `rustls` vs `tokio-rustls`. See §4.2.

### 2.2 Dependency graph

```
reticulum-core ────┬──── reticulum-codec ────┐
                   │                          │
                   ├──── reticulum-crypto ────┤
                   │                          │
                   └──────────────────────────┴──── reticulum-gateway ──┬── reticulum-iface ──┐
                                                                        │                      │
                                                                        └── reticulum-ext ────┤
                                                                                              │
                                                                                              ├── reticulum-rnsd
                                                                                              │
                                                                                              └── reticulum-cli
```

No cycles. `reticulum-vectors` is depended on by all upper crates as `[dev-dependencies]` only.

### 2.3 Workspace `Cargo.toml`

```toml
[workspace]
resolver = "2"
members = ["crates/*", "xtask"]

[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.75"
license = "MIT OR Apache-2.0"
authors = ["..."]
repository = "..."

[workspace.lints.rust]
unsafe_code = "forbid"      # except where explicitly noted per-crate
missing_docs = "warn"

[workspace.lints.clippy]
all = "warn"
pedantic = "warn"
cargo = "warn"

[workspace.dependencies]
# Pin every shared dep here; per-crate just references workspace = true.

# crypto
x25519-dalek      = { version = "2", default-features = false, features = ["static_secrets", "zeroize"] }
ed25519-dalek     = { version = "2", default-features = false, features = ["rand_core", "zeroize"] }
sha2              = { version = "0.10", default-features = false }
hmac              = { version = "0.12", default-features = false }
hkdf              = { version = "0.12", default-features = false }
aes               = { version = "0.8", default-features = false }
cbc               = { version = "0.1", default-features = false, features = ["alloc"] }
subtle            = { version = "2",   default-features = false }
zeroize           = { version = "1",   default-features = false, features = ["derive"] }
rand_core         = { version = "0.6", default-features = false, features = ["getrandom"] }

# encoding
serde             = { version = "1",   default-features = false, features = ["derive"] }
rmp-serde         = "1"
toml              = "0.8"

# error + log
thiserror         = "1"
tracing           = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }

# async (only in std crates)
tokio             = { version = "1",   features = ["rt-multi-thread", "net", "io-util", "sync", "time", "macros", "signal"] }
tokio-util        = "0.7"
futures           = "0.3"
bytes             = "1"

# config
figment           = { version = "0.10", features = ["toml", "env"] }

# CLI
clap              = { version = "4", features = ["derive"] }

# testing
proptest          = "1"
hex-literal       = "0.4"
```

### 2.4 Forbidden dependencies

- **`openssl`, `openssl-sys`** — we are pure-Rust crypto by default. `ring` is acceptable behind a feature flag for performance-critical deployments.
- **`async-std`** — tokio only.
- **`mio` directly** — go through tokio.
- **Anything GPL or AGPL** — including transitive. Audited via `cargo-deny`.
- **`unsafe`** without per-block justification — enforced by the `forbid(unsafe_code)` lint, opt-out documented per crate.

---

## 3. The gateway state machine (`reticulum-gateway`)

This is the heart of the project. It implements the Reticulum protocol.

### 3.1 Wire format primitives

All primitives belong to `reticulum-core` and `reticulum-codec`. They are `no_std + alloc`.

The Reticulum packet header is two bytes plus a 16-byte destination hash, optionally preceded by a 16-byte transport-id (in `HEADER_2`), plus one context byte. Byte 0 is packed flags:

```
 bit 7    bit 6    bit 5    bit 4    bit 3-2    bit 1-0
[ifac]   [hdr_typ][ctx_flg][trans_typ][dst_typ ][pkt_typ]
```

(`IFAC` is the high bit and is used by interfaces with authenticated framing; the gateway exposes it but most paths don't set it.)

```rust
// reticulum-core/src/wire.rs

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PacketType { Data = 0, Announce = 1, LinkRequest = 2, Proof = 3 }

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum DestinationType { Single = 0, Group = 1, Plain = 2, Link = 3 }

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TransportType { Broadcast = 0, Transport = 1, Relay = 2, Tunnel = 3 }

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum HeaderType { H1 = 0, H2 = 1 }

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContextByte(pub u8);
// Constants for NONE, RESOURCE, RESOURCE_ADV, RESOURCE_REQ, RESOURCE_HMU,
// RESOURCE_PRF, RESOURCE_ICL, RESOURCE_RCL, CACHE_REQUEST, REQUEST,
// RESPONSE, PATH_RESPONSE, COMMAND, COMMAND_STATUS, CHANNEL,
// KEEPALIVE (0xFA), LINKIDENTIFY (0xFB), LINKCLOSE (0xFC),
// LINKPROOF (0xFD), LRRTT (0xFE), LRPROOF (0xFF).
// Authoritative values: see Reticulum manual §"Wire Format" + Python RNS/Packet.py.

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AddressHash([u8; 16]);  // truncated 128-bit destination hash

#[derive(Debug, Clone)]
pub struct PacketHeader {
    pub ifac: bool,
    pub header_type: HeaderType,
    pub context_flag: bool,
    pub transport_type: TransportType,
    pub destination_type: DestinationType,
    pub packet_type: PacketType,
    pub hops: u8,
    pub transport_id: Option<AddressHash>,    // present iff HeaderType::H2
    pub destination: AddressHash,
    pub context: ContextByte,
}

pub const HEADER_SIZE_H1: usize = 1 + 1 + 16 + 1;            // 19 bytes
pub const HEADER_SIZE_H2: usize = 1 + 1 + 16 + 16 + 1;       // 35 bytes
pub const MTU: usize = 500;
```

`AddressHash` is exactly 16 bytes — the truncated-hash length of the reference. Do not parameterize over hash length; it's a protocol constant.

### 3.2 Identity

```rust
// reticulum-gateway/src/identity.rs

pub struct Identity {
    encryption_key: x25519_dalek::StaticSecret,
    signing_key:    ed25519_dalek::SigningKey,
}

pub struct PublicIdentity {
    encryption_pub: x25519_dalek::PublicKey,
    signing_pub:    ed25519_dalek::VerifyingKey,
}

impl Identity {
    pub fn generate<R: CryptoRngCore>(rng: &mut R) -> Self { ... }
    pub fn from_bytes(prv: &[u8; 64]) -> Result<Self, Error> { ... }
    pub fn to_bytes(&self) -> [u8; 64] { ... }

    pub fn public(&self) -> PublicIdentity { ... }

    /// Reticulum identity hash: SHA-256 over (enc_pub || sig_pub), truncated to 16 bytes.
    pub fn hash(&self) -> AddressHash { ... }

    pub fn sign(&self, data: &[u8]) -> ed25519_dalek::Signature { ... }
}

impl PublicIdentity {
    pub fn verify(&self, data: &[u8], sig: &ed25519_dalek::Signature) -> Result<(), Error> { ... }
    pub fn hash(&self) -> AddressHash { ... }
}
```

`StaticSecret` and `SigningKey` are zeroize-on-drop via their respective crates. `Identity` does not implement `Clone`.

### 3.3 Token (Reticulum's modified Fernet)

```rust
// reticulum-crypto/src/token.rs

/// Reticulum's modified Fernet: IV (16) || AES-256-CBC(PKCS7(plaintext)) || HMAC-SHA-256(IV||CT)
///
/// No version byte, no timestamp (deliberately, to avoid metadata leakage).
/// HMAC verification is constant-time via `subtle`.
pub struct Token {
    encryption_key: [u8; 32],
    signing_key:    [u8; 32],
}

impl Token {
    pub const OVERHEAD: usize = 16 + 32;  // IV + HMAC

    pub fn new(key: &DerivedKey) -> Self { ... }
    pub fn encrypt<R: CryptoRngCore>(&self, rng: &mut R, plaintext: &[u8], out: &mut [u8]) -> Result<usize, Error>;
    pub fn decrypt<'a>(&self, ciphertext: &[u8], out: &'a mut [u8]) -> Result<&'a [u8], Error>;
}
```

**Constant-time MAC verification is non-negotiable.** Use `subtle::ConstantTimeEq`. The Python reference uses `==` on `bytes` which is not constant-time; we fix this without breaking wire compatibility.

The 64-byte `DerivedKey` is what HKDF-SHA-256 produces from an ECDH shared secret. Split: first 32 bytes are signing key, last 32 are encryption key. (For the `fernet-aes128` legacy mode, key is 32 bytes total; not implemented by default — gated behind a feature for archaeology only.)

### 3.4 Destinations

```rust
// reticulum-gateway/src/destination.rs

#[derive(Debug, Clone)]
pub struct DestinationName {
    pub app_name: String,        // e.g. "lxmf"
    pub aspects:  Vec<String>,   // e.g. ["delivery"]
}

impl DestinationName {
    /// Reticulum name hash: SHA-256 of "appname.aspect1.aspect2", truncated to 10 bytes.
    pub fn name_hash(&self) -> [u8; 10] { ... }
}

pub enum DestinationDirection { In, Out }

pub enum DestinationKind {
    Single { identity: Arc<Identity> },         // we hold the private key
    SingleRemote { identity: PublicIdentity },  // we know only the public key
    Group { shared_key: [u8; 32] },             // PSK
    Plain,                                       // unencrypted
}

pub struct Destination {
    pub direction: DestinationDirection,
    pub kind: DestinationKind,
    pub name: DestinationName,
    // computed
    pub address: AddressHash,  // SHA-256(name_hash || identity_hash), trunc 16
}
```

The destination address derivation is:

```
address = truncate_16(SHA256( name_hash(10) || identity_hash(16) ))
```

This is invariant across implementations and must be exact.

### 3.5 Announces

The announce is the protocol's nervous system. An announce packet carries:

```
[ public_key(32) ] [ signing_key(32) ] [ name_hash(10) ] [ random_hash(10) ]
[ ratchet(32, optional) ] [ signature(64) ] [ app_data(...) ]
```

Signature covers: `destination_hash || public_key || signing_key || name_hash || random_hash || ratchet || app_data`.

```rust
// reticulum-gateway/src/announce.rs

pub struct AnnouncePayload {
    pub encryption_pub: [u8; 32],
    pub signing_pub:    [u8; 32],
    pub name_hash:      [u8; 10],
    pub random_hash:    [u8; 10],
    pub ratchet:        Option<[u8; 32]>,
    pub signature:      [u8; 64],
    pub app_data:       Vec<u8>,
}

impl AnnouncePayload {
    pub fn verify(&self, destination_hash: &AddressHash) -> Result<(), Error> {
        // 1. Recompute destination hash from name_hash + identity_hash; check match.
        // 2. Verify Ed25519 signature with self.signing_pub.
        Ok(())
    }

    pub fn encode(&self, buf: &mut Vec<u8>);
    pub fn decode(bytes: &[u8]) -> Result<Self, Error>;
}
```

### 3.6 Path table

A path entry says: "for destination D, send via next-hop H over interface I; learned at time T; distance N hops."

```rust
// reticulum-gateway/src/path.rs

pub struct PathEntry {
    pub destination: AddressHash,
    pub next_hop:    AddressHash,       // identity of the relaying transport node
    pub interface:   InterfaceId,
    pub hops:        u8,
    pub learned_at:  Instant,
    pub expires_at:  Instant,
}

pub struct PathTable {
    entries: HashMap<AddressHash, PathEntry>,
    // shard for hot lookup; consider dashmap in std builds
}

impl PathTable {
    pub fn lookup(&self, dst: &AddressHash) -> Option<&PathEntry>;
    pub fn insert(&mut self, entry: PathEntry);
    pub fn evict_expired(&mut self, now: Instant) -> usize;
    pub fn len(&self) -> usize;
}
```

Expiration follows reference defaults: 1 week for normal paths, 1 day for AP paths, 6 hours for roaming paths. These are policy, configurable via the gateway config struct.

### 3.7 Link state machine

Links are the protocol's encrypted bidirectional channels. Three-packet handshake:

1. **LinkRequest** from initiator: ephemeral X25519 pubkey + signing pubkey + truncated link-id.
2. **LRRTT** from destination: ephemeral X25519 pubkey + signing pubkey + signed proof + RTT measurement.
3. **LRPROOF** acknowledgment from initiator.

After handshake, both sides have a shared `DerivedKey` from HKDF over the X25519 shared secret. All link traffic is `Token`-encrypted with that key. Forward secrecy is achieved by the ephemerality of the handshake keys. Ratchets (§3.9) refresh the key over the link's lifetime.

```rust
// reticulum-gateway/src/link/mod.rs

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkState {
    Pending,        // request sent, awaiting LRRTT
    Handshaking,    // LRRTT received, awaiting LRPROOF
    Active,
    Stale,          // no traffic in KEEPALIVE * STALE_FACTOR
    Closed,
}

pub struct Link {
    pub id: AddressHash,                    // truncated SHA-256 of public link material
    state: LinkState,
    role:  LinkRole,                        // Initiator | Responder
    token: Option<Token>,                   // populated after handshake
    rtt:   Option<Duration>,
    established_at: Option<Instant>,
    last_inbound:   Instant,
    last_outbound:  Instant,
    keepalive_interval: Duration,
    teardown_at: Option<Instant>,
    // ratchet machinery in §3.9
}

pub enum LinkEvent {
    Established { id: AddressHash, rtt: Duration },
    DataReceived { id: AddressHash, plaintext: Vec<u8>, context: ContextByte },
    Closed { id: AddressHash, reason: LinkCloseReason },
}

pub enum LinkCloseReason { Timeout, InitiatorClosed, DestinationClosed, LocalError }
```

The state transitions are exhaustively enumerated in `docs/wire-format.md` (to be authored from the manual + the Python `Link.py`). All transitions are tested via the conformance vectors in §7.

### 3.8 Resource state machine

This is the largest single piece of missing functionality in Beechat (it does not implement Resource at all, only individual Packets). It's what `rncp`, file transfers, and large LXMF messages rely on.

A Resource:

1. **Advertises** the transfer: SHA-256 of the whole payload, total size, segmented part count, expected MTU. (`RESOURCE_ADV`)
2. **Receives** requests for specific parts (`RESOURCE_REQ`) and sends them as packets with context `RESOURCE`.
3. **Sends a hashmap update** (`RESOURCE_HMU`) periodically if the receiver needs to acknowledge a windowed batch.
4. **Closes** with a proof (`RESOURCE_PRF`) — full-payload hash signed.
5. Either side can cancel (`RESOURCE_ICL` / `RESOURCE_RCL`).

```rust
// reticulum-gateway/src/resource/mod.rs

pub struct Resource {
    pub id:         [u8; 32],
    pub link_id:    AddressHash,
    pub total_size: u64,
    pub part_size:  u32,
    pub part_count: u32,
    pub direction:  ResourceDirection,
    pub state:      ResourceState,
    // received: bitmap of parts we have; for receivers
    received: BitVec,
    // pending: parts not yet sent; for senders
    pending: BitVec,
    started_at: Instant,
    progress_callback: Option<Box<dyn Fn(f32) + Send>>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceState {
    Advertised,
    Transferring,
    Assembling,    // receiver-side after all parts in
    Complete,
    Failed { reason: ResourceFailReason },
    Cancelled,
}

pub enum ResourceEvent {
    Started   { id: [u8; 32], total_size: u64 },
    Progress  { id: [u8; 32], received: u32, total: u32 },
    Complete  { id: [u8; 32], data: Vec<u8> },
    Failed    { id: [u8; 32], reason: ResourceFailReason },
}
```

A separate ADR documents the chunking and retransmit details, because they have several edge cases (compression, sequencing, part-size negotiation under MTU pressure) that need spec-level attention. **First Claude Code task should not be Resource; build the announce subsystem and a real interop test against Python first.**

### 3.9 Ratchets

Ratchets are short-lived X25519 keys announced by destinations to provide forward secrecy at announce-time. A sender encrypting to a destination uses the latest known ratchet pubkey instead of the static encryption pubkey. Receivers maintain a window of recent ratchet privkeys to allow decryption of in-flight packets after rotation.

```rust
// reticulum-gateway/src/ratchet.rs

pub struct Ratchet {
    pub id:         [u8; 32],
    pub created_at: Instant,
}

pub struct RatchetStore {
    /// For destinations we own: our own active and recently-active private ratchets.
    owned:  HashMap<AddressHash, Vec<RatchetPriv>>,
    /// For destinations we know: the latest-known public ratchet announced by them.
    known:  HashMap<AddressHash, RatchetPub>,
}

impl RatchetStore {
    /// Reference policy: keep ratchets for 30 days (RATCHET_EXPIRY).
    pub fn evict_expired(&mut self, now: Instant) -> usize;

    pub fn latest_for(&self, dst: &AddressHash) -> Option<&RatchetPub>;
    pub fn rotate_owned(&mut self, dst: &AddressHash, rng: &mut impl CryptoRngCore);
    pub fn remember_announced(&mut self, dst: AddressHash, ratchet: RatchetPub);
}
```

### 3.10 The gateway facade

Everything above is wired together by a runtime-agnostic facade:

```rust
// reticulum-gateway/src/lib.rs

pub struct Gateway {
    identity:       Identity,
    destinations:   DestinationTable,
    paths:          PathTable,
    links:          LinkTable,
    resources:      ResourceTable,
    ratchets:       RatchetStore,
    announces:      AnnounceTable,        // outbound queue + dedup
    packet_dedup:   PacketDedupSet,
    config:         GatewayConfig,
}

/// All gateway interaction is via this enum. No locks held across `step`.
pub enum GatewayCmd {
    SubmitInbound  { from_iface: InterfaceId, bytes: Bytes },
    SendPacket     { destination: AddressHash, payload: Bytes, context: ContextByte },
    AnnounceLocal  { destination: AddressHash, app_data: Bytes },
    OpenLink       { destination: AddressHash },
    CloseLink      { link_id: AddressHash },
    StartResource  { link_id: AddressHash, payload: Bytes },
    Tick           { now: Instant },
    Shutdown,
}

pub enum GatewayEvent {
    PacketOut       { interface: InterfaceId, bytes: Bytes },
    AnnounceReceived{ destination: AddressHash, identity: PublicIdentity, app_data: Bytes, hops: u8 },
    LinkEvent       (LinkEvent),
    ResourceEvent   (ResourceEvent),
    PathLearned     { destination: AddressHash, via: AddressHash, hops: u8 },
    PathLost        { destination: AddressHash },
}

impl Gateway {
    pub fn new(identity: Identity, config: GatewayConfig) -> Self;

    /// Feed in one command, get a (possibly empty) vector of events.
    /// All gateway logic lives here. No async, no I/O.
    pub fn step(&mut self, cmd: GatewayCmd) -> Vec<GatewayEvent>;

    /// Returns the next instant the gateway wants `Tick`'d at.
    pub fn next_deadline(&self) -> Option<Instant>;
}
```

This shape — `cmd in, events out, deadline hint` — is the same as `quinn-proto::Connection`, `rustls::ConnectionCommon`, `boring`'s SSL state machine. It is **directly testable** without any runtime, it is portable to embedded, and it composes cleanly with any async executor.

The daemon in §5 binds it to `tokio` via a single task that owns the `Gateway`, reads from a `mpsc::Receiver<GatewayCmd>`, and writes events to outbound channels.

---

## 4. Interfaces (`reticulum-iface`)

### 4.1 The interface trait

```rust
// reticulum-iface/src/lib.rs

#[async_trait::async_trait]
pub trait Interface: Send + 'static {
    fn id(&self) -> InterfaceId;
    fn kind(&self) -> InterfaceKind;
    fn mtu(&self) -> usize;

    /// Spawn the interface's I/O task(s). Returns sender/receiver pair for
    /// packet exchange with the gateway, plus a cancellation token.
    async fn start(self: Box<Self>)
        -> Result<InterfaceHandle, Error>;
}

pub struct InterfaceHandle {
    pub id:        InterfaceId,
    pub tx:        mpsc::Sender<TxFrame>,       // gateway → iface
    pub rx:        mpsc::Receiver<RxFrame>,     // iface → gateway
    pub cancel:    CancellationToken,
}

pub struct TxFrame { pub bytes: Bytes, pub priority: Priority }
pub struct RxFrame { pub bytes: Bytes, pub rssi: Option<i16>, pub snr: Option<f32> }
```

### 4.2 Concrete interfaces — priority order

| # | Interface | Why this order |
|---|---|---|
| 1 | **TCP client/server** | Already in Beechat. Lowest-friction interop test. Bootstrap and Internet transit. |
| 2 | **UDP** | Already in Beechat. Required for `AutoInterface` (which uses UDP multicast for discovery). |
| 3 | **LocalInterface** | Without this, existing Sideband/MeshChat/NomadNet clients can't share our daemon. **Blocks adoption.** |
| 4 | **RNodeInterface** | Required to participate in the LoRa half of the network. Serial + KISS framing + RNode command set. |
| 5 | **AutoInterface** | RNS 1.1+ feature; participates in interface discovery. UDP multicast. |
| 6 | **SerialInterface** | Generic serial; subset of RNode logic. |
| 7 | **KISSInterface** | TNC compatibility; mostly a framing module reused by RNode + Serial. |
| 8 | **AX25KISSInterface** | Packet radio with AX.25 framing. Niche but well-defined. |
| 9 | **I2PInterface** | Tor/I2P transit. Lower priority. |
| 10 | **BackboneInterface** | High-throughput inter-transport-node link. Lower priority. |

### 4.3 RNodeInterface notes

The RNode is a microcontroller running custom firmware that exposes a KISS-like serial protocol with extended command bytes for radio parameters (frequency, bandwidth, spreading factor, coding rate, TX power). The protocol:

- KISS frame escaping: `FEND=0xC0`, `FESC=0xDB`, `TFEND=0xDC`, `TFESC=0xDD`.
- Command byte 0x00 carries data frames.
- Commands 0x01–0x0F set radio parameters.
- Special commands for firmware identification, watchdog, error reporting.

Reference: `markqvist/Reticulum/RNS/Interfaces/RNodeInterface.py` (pre-April-2025 only, for understanding intent; do not copy code). Authoritative source is the RNode firmware: `markqvist/RNode_Firmware` and `RNode_Firmware_CE`.

### 4.4 LocalInterface notes

Used by separate processes on the same host to share a single `rnsd` daemon. Reference uses TCP on loopback with its own framing. **We diverge**: use a Unix domain socket with length-prefixed `bincode`-encoded messages, with capability tokens for client authentication. Wrap a Reticulum-compatible shim around the existing TCP-on-loopback for backward compatibility with unmodified clients. This is the first place the extension layer shows up.

---

## 5. The daemon (`reticulum-rnsd`)

### 5.1 Responsibilities

- Load and validate TOML config (§5.2).
- Construct the `Gateway` and required interfaces.
- Spawn one tokio task that owns the `Gateway` and drives it.
- Spawn one tokio task per interface.
- Bridge them with `mpsc` channels.
- Run the `Tick` deadline driver.
- Expose a control socket (`LocalInterface`) for utilities and client apps.
- Expose a metrics endpoint (Prometheus) and a `tracing` subscriber.
- Handle SIGINT/SIGTERM cleanly; persist gateway state on shutdown.

### 5.2 Config format

Valid TOML, deliberately incompatible with the reference's `configobj` format. A converter (§5.3) handles migration.

```toml
[reticulum]
enable_transport      = false
share_instance        = true
shared_instance_port  = 37428
identity_path         = "~/.config/reticulum/identity"

[reticulum.logging]
level   = "info"           # trace|debug|info|warn|error
format  = "compact"        # compact|json
target  = "stderr"         # stderr|file
file    = "~/.config/reticulum/rnsd.log"

[reticulum.metrics]
enabled = true
bind    = "127.0.0.1:9119"

[[interfaces]]
name              = "Default TCP Server"
type              = "TCPServerInterface"
interface_enabled = true
bind_host         = "[::]"
bind_port         = 4242

[[interfaces]]
name              = "RNode #1"
type              = "RNodeInterface"
interface_enabled = true
port              = "/dev/ttyUSB0"
frequency         = 867500000
bandwidth         = 125000
txpower           = 5
spreading_factor  = 10
coding_rate       = 6

# Extension layer config — namespaced under [extensions.*]
[extensions.pq_links]
enabled        = true
kem            = "ml-kem-768"
sig            = "ml-dsa-65"
require_pq_for = []         # list of destination hashes that REQUIRE PQ; rest fall back

[extensions.observability]
otlp_endpoint = "http://127.0.0.1:4317"
```

### 5.3 Config converter

A binary `convert-config` that reads a Python-RNS `configobj` file and emits valid TOML. Round-trip tested against the reference's example configs. Reuse / adapt the implementation from `lgh/Reticulum-rs` daemon-mode branch (MIT, attribution preserved).

### 5.4 Daemon main loop sketch

```rust
// reticulum-rnsd/src/main.rs

#[tokio::main]
async fn main() -> Result<()> {
    let args = Args::parse();
    let config = Config::load(&args.config_path)?;
    init_tracing(&config);
    init_metrics(&config)?;

    let identity = Identity::load_or_create(&config.reticulum.identity_path)?;
    let gateway  = Gateway::new(identity, config.gateway());

    let (cmd_tx, cmd_rx)   = mpsc::channel::<GatewayCmd>(1024);
    let (evt_tx, mut evt_rx) = broadcast::channel::<GatewayEvent>(1024);

    // Gateway-driver task: owns the gateway, single owner.
    let gw_task = tokio::spawn(gateway_driver(gateway, cmd_rx, evt_tx.clone()));

    // Spawn each interface; wire it to cmd_tx (for inbound) and a subscribed evt rx (for outbound).
    let mut iface_handles = Vec::new();
    for iface_cfg in &config.interfaces {
        let handle = build_interface(iface_cfg).start().await?;
        spawn_iface_pump(handle, cmd_tx.clone(), evt_tx.subscribe());
        iface_handles.push(/* metadata */);
    }

    // Local control socket
    let ctl = LocalControlServer::bind(&config).await?;
    tokio::spawn(ctl.run(cmd_tx.clone(), evt_tx.subscribe()));

    // Signal handling
    let mut sigint  = tokio::signal::unix::signal(SignalKind::interrupt())?;
    let mut sigterm = tokio::signal::unix::signal(SignalKind::terminate())?;
    tokio::select! {
        _ = sigint.recv()  => tracing::info!("SIGINT received"),
        _ = sigterm.recv() => tracing::info!("SIGTERM received"),
        _ = gw_task        => tracing::error!("gateway task exited unexpectedly"),
    }

    cmd_tx.send(GatewayCmd::Shutdown).await.ok();
    // Wait for tasks, persist state, exit.
    Ok(())
}
```

The single-owner gateway task is the architectural keystone. **No shared mutable state across tasks.** Everything is messages.

---

## 6. Extension layer (`reticulum-ext`)

### 6.1 Negotiation

Extensions are advertised in the **announce app_data** field, which the reference protocol treats as opaque. We define a structured TLV:

```
app_data := [ user_app_data ] || [ 0xEE 0xEE ] || [ ext_tlv_seq ]

ext_tlv_seq := one or more of:
   [type:u8] [length:u16 BE] [value:length bytes]
```

The `0xEE 0xEE` sentinel marks the boundary; it is chosen to be vanishingly unlikely to occur in existing app_data. Type values:

| Type | Meaning | Value |
|---|---|---|
| 0x01 | PQ link support | KEM ID (1 byte) + sig ID (1 byte) + static PQ pubkey hash (32 bytes) |
| 0x02 | Hardware identity attestation | attestation blob (opaque, signed by HW root) |
| 0x03 | Extended capability set | bitfield (variable) |
| 0x04..0x7F | Reserved for future standard extensions |
| 0x80..0xFF | Reserved for vendor-private extensions |

Reticulum-only peers see opaque trailing bytes after the standard app_data and ignore them. Extension-aware peers parse the TLV and act accordingly.

### 6.2 PQ link mode

When two extension-aware peers establish a link, after the standard Reticulum 3-packet handshake, the initiator sends an extension control packet (context `EXT_LINK_UPGRADE`) containing an ML-KEM-768 encapsulation. The responder decapsulates and replies with ML-DSA-65-signed confirmation. From that point, the link's `Token` is rekeyed via HKDF over `classical_shared || pq_shared`, providing **defense in depth** — an attacker must break both ECDH and ML-KEM to recover traffic.

Use `pqcrypto-mlkem` and `pqcrypto-mldsa` (or `ml-kem`/`ml-dsa` from RustCrypto when stable).

This is gated by a feature flag. Default off. Required for some destination hashes if listed in config (`require_pq_for`).

### 6.3 Hardware-backed identity

The TLV type 0x02 carries an attestation: a signature by a hardware root key over the announce's classical signing pubkey. Verifiers consult a configurable trust store of hardware-root pubkeys. Useful for high-assurance deployments (your sigil-id work fits here).

### 6.4 Observability

`tracing` spans on every gateway event, Prometheus metrics on counts/latencies/queue depths, OTLP export optional. **None of this is on the wire** — it's local instrumentation. Listed in the extension layer because it lives in `reticulum-ext` and is opt-in.

### 6.5 MeshCore / Meshtastic adapters

Out of scope for the v0.1 spec but the seam exists: an adapter is an `Interface` impl that translates the foreign protocol's frames into Reticulum packets (and vice versa) at the gateway boundary. Two design notes for whoever picks this up:

- **Identity translation** is the hard part. Reticulum addresses are SHA-256-derived; Meshtastic uses 32-bit IDs; MeshCore uses 6-byte public-key prefixes. The adapter maintains a mapping table.
- **Encryption translation is not possible** for end-to-end encrypted traffic on either side. The adapter exposes plaintext only for traffic destined for/originating from the adapter node itself.

ADR-required before implementation: `docs/adr/00xx-foreign-protocol-adapters.md`.

---

## 7. Conformance and testing

### 7.1 The vector format

Test vectors live in `reticulum-vectors/fixtures/`. Each fixture is a directory:

```
fixtures/announce_basic/
  ├── meta.toml          # describes scenario, source, capture date
  ├── inputs.toml        # identity seeds, parameters, RNG seeds
  ├── expected/
  │     ├── 001-announce-out.bin
  │     ├── 002-announce-decoded.toml
  │     └── ...
  └── README.md
```

`inputs.toml` provides deterministic seeds so the implementation can be re-run reproducibly. `expected/*.bin` are byte-exact captures from a running Python reference. `expected/*.toml` are human-readable decodings for diffing.

### 7.2 Capture harness

`reticulum-vectors/capture/` is a small Python script that drives `rns` reference (or RetiNet) with deterministic identity seeds and captures packets via a mock interface that writes every byte to disk. Output is committed to the repo. Re-run only when the spec changes; otherwise treat fixtures as immutable.

### 7.3 Categories

| Category | Description | Priority |
|---|---|---|
| `wire/header` | Pack/unpack every header flag combination | P0 |
| `wire/announce` | Announce signing and verification, app_data variants | P0 |
| `wire/path_request` | Path request and response | P0 |
| `wire/link_handshake` | All three packets, full state machine | P0 |
| `wire/link_keepalive` | Link KEEPALIVE / STALE / TIMEOUT | P1 |
| `wire/proof` | Packet proofs and resource proofs | P1 |
| `wire/resource` | Resource adv / req / part / HMU / proof / cancel | P1 |
| `wire/channel` | Channel framing inside links | P2 |
| `crypto/token` | Encrypt/decrypt round-trips, MAC failure modes | P0 |
| `crypto/identity` | Identity hash derivation | P0 |
| `crypto/ratchet` | Ratchet announcement and use | P1 |
| `interop/python` | Live testing against running Python rnsd | P0 (continuous) |
| `interop/retinet` | Live testing against RetiNet | P0 |
| `interop/beechat` | Live testing against `reticulum-rs` baseline | P1 |

### 7.4 Interop CI

A GitHub Actions (or Forgejo) workflow that spins up a Python `rnsd` in one container and our `rnsd` in another, joined by TCP, runs an announce / link / resource scenario, and asserts both sides agree. Runs on every PR.

---

## 8. Build, CI, and release

### 8.1 `cargo xtask`

Replace ad-hoc shell scripts. Tasks:

- `cargo xtask test-all` — fmt, clippy, build all crates with all feature combinations, unit tests, conformance vectors.
- `cargo xtask interop` — spin up the interop CI scenario locally.
- `cargo xtask coverage` — `cargo-llvm-cov`.
- `cargo xtask audit` — `cargo-deny` + `cargo-audit`.
- `cargo xtask release` — version bumps, changelogs, dry-run publish.

### 8.2 CI matrix

- Rust toolchains: stable, beta, MSRV (1.75).
- OS: linux-x86_64, linux-aarch64, macos-aarch64, windows-x86_64.
- Feature combinations: default, `--no-default-features`, `pq-links`, `ring-backend`.
- Steps: fmt → clippy → build → test → conformance → interop.

### 8.3 Release artifacts

- `crates.io` for libraries.
- Pre-built binaries for `reticulum-rnsd` and `reticulum-cli` on releases page: Linux x86_64/aarch64, macOS aarch64, Windows x86_64.
- `.deb` and `.rpm` packages (built via `cargo-deb` / `cargo-generate-rpm`).
- Alpine APKBUILD as a separate contribution.
- Docker image (multi-arch).

---

## 9. Security

### 9.1 Threat model summary

Detailed: `docs/threat-model.md`. Headline assumptions:

- Adversary may control radio medium, IP transit, and a subset of transport nodes.
- Adversary cannot break X25519, Ed25519, AES-256, SHA-256 within the classical model. PQ extension addresses post-quantum risk for opt-in deployments.
- Adversary may attempt timing side channels on MAC verification and key comparison.
- Initiator anonymity is a design goal of Reticulum and must be preserved (no source addresses on packets).
- Compromise of a transport node reveals path information, not link contents.

### 9.2 Concrete commitments

- All MAC and key comparisons use `subtle::ConstantTimeEq`. No exceptions.
- All private key material wraps `zeroize::Zeroizing` or comes from a crate that does so internally (`x25519-dalek` static_secrets, `ed25519-dalek` SigningKey with the `zeroize` feature).
- `forbid(unsafe_code)` at workspace level. Exceptions require an ADR and a justification comment per unsafe block.
- Dependencies audited via `cargo-deny` on every CI run. License allowlist: MIT, Apache-2.0, BSD-3, BSD-2, ISC, Unicode-DFS-2016, MPL-2.0 (only for non-source-modifying use). **GPL, AGPL, LGPL, SSPL, BSL are blocked.**
- No `openssl` dependency by default; `ring` only behind explicit feature.
- All randomness from `OsRng` (via `rand_core::OsRng`).
- Replay protection on announces and links per the reference's packet-hashlist mechanism.

### 9.3 Disclosure

`SECURITY.md` documents the responsible disclosure address and PGP key. Coordinated disclosure window: 90 days from acknowledgment.

---

## 10. Licensing chain

This is important enough to repeat from §0.

### 10.1 What we use

- **The Reticulum protocol specification** — public domain (dedicated 2016). Used freely.
- **The Reticulum manual** — referenced for protocol semantics. Documentation, not code.
- **Beechat `Reticulum-rs`** — MIT-licensed. Code may be adapted with attribution. Listed in `NOTICE`.
- **`lgh/Reticulum-rs` daemon-mode** — MIT-licensed (same lineage). Config-converter code adapted with attribution.
- **Kroese's formal spec draft** — license per Kroese; cited as authoritative for protocol semantics where it converges with reference behavior.

### 10.2 What we do not use

- **Code from `markqvist/Reticulum` dated after April 2025.** Reticulum License with anti-AI / no-harm clauses; not compatible with our `MIT OR Apache-2.0` licensing.
- **RetiNet code.** AGPL-3.0. Incompatible with our licensing posture.
- **Pre-April-2025 `markqvist/Reticulum` code is technically MIT** but our policy is to implement from the protocol spec rather than from the reference code, to keep the derivation chain clean.

### 10.3 Our license

All crates this fork originates: `MIT OR Apache-2.0`. Standard Rust dual license. Compatible with crates.io, Debian, F-Droid, Alpine main.

### 10.4 Attribution

`NOTICE` file at workspace root lists:

- Beechat Network Systems Ltd for `Reticulum-rs` architectural baseline.
- Liam (`lgh`) for daemon and config-converter prior art.
- Mark Qvist for the original Reticulum protocol design.
- Jo Kroese et al. for the formal specification effort.
- Reticulum community (RetiNet, microReticulum, go-reticulum) for interop testing.

---

## 11. Roadmap

### 11.1 Phase 1 — Bootstrap (target: 2–3 weeks of focused work)

- [ ] Workspace scaffolding per §2 (Claude Code task #1).
- [ ] Port Beechat's `Packet`, `Identity`, `Token` into `reticulum-core` + `reticulum-crypto` + `reticulum-codec`, with constant-time MAC verification.
- [ ] Conformance vector capture harness in `reticulum-vectors/capture/`; capture P0 wire/header and wire/announce vectors from running Python rnsd.
- [ ] `Gateway` facade per §3.10 with stub state — accepts commands, returns empty event vectors. Compiles.
- [ ] First passing test: encode an announce, verify it byte-for-byte against a captured Python announce with the same identity seed.
- [ ] CI: fmt, clippy, build, test, cargo-deny.

**Exit criterion:** workspace compiles, conformance test for wire/header and wire/announce passes, fork is publishable as `v0.1.0-alpha.1`.

### 11.2 Phase 2 — Wire compatibility (4–6 weeks)

- [ ] Full announce subsystem: emit, receive, verify, path table population.
- [ ] Path request/response.
- [ ] Link establishment state machine. All three handshake packets; KEEPALIVE; STALE; close.
- [ ] Ratchets.
- [ ] TCP and UDP interfaces wired through; `reticulum-iface` minimum.
- [ ] Live interop test against Python rnsd: announce visible, link establishes, data flows.
- [ ] LocalInterface with Reticulum-compatible TCP-on-loopback fallback.

**Exit criterion:** a Sideband client running unmodified on the same machine can use our `rnsd` as its shared instance, exchange messages with a Python-hosted destination, and tear the link down cleanly.

### 11.3 Phase 3 — Application traffic (4–8 weeks)

- [ ] Resource state machine, full bidirectional.
- [ ] Channel + Buffer framing.
- [ ] Transport-mode forwarding: paths, reverse tables, packet dedup.
- [ ] AutoInterface (UDP multicast discovery).
- [ ] RNodeInterface.
- [ ] `rnstatus`, `rnpath`, `rncp`, `rnid` utilities.

**Exit criterion:** `rncp` transfers files in both directions between our node and a Python node over both TCP and RNode-LoRa.

### 11.4 Phase 4 — Extensions and embedded (open-ended)

- [ ] Extension TLV framing in announce app_data.
- [ ] PQ link mode (ML-KEM-768 + ML-DSA-65).
- [ ] Hardware-attested identities.
- [ ] OpenTelemetry / Prometheus.
- [ ] `no_std` validation of `reticulum-core` + `reticulum-crypto` + `reticulum-codec`.
- [ ] First embedded reference build (target TBD: RP2040, ESP32-S3, nRF52?).
- [ ] MeshCore / Meshtastic adapter ADR + prototype.

---

## 12. Open questions

These need resolution before or during Phase 1. Track in `docs/open-questions.md`.

1. **Crate name.** §0.4. Required before first commit.
2. **Repository host.** GitHub (most contributors), Codeberg (where RetiNet and rnsd-rs live), or self-hosted Forgejo? Recommendation: Codeberg for the community center of gravity, GitHub mirror for discoverability.
3. **Relationship to Beechat.** Hard fork, soft fork (PRs upstream where they fit), or separate crate that depends on `reticulum`? Recommendation: separate crate family with our own naming; PRs to Beechat for fixes that fit their scope.
4. **Communication channel.** Join FOSDEM Zulip; consider Matrix bridge.
5. **PQ crate selection.** `pqcrypto-mlkem`/`pqcrypto-mldsa` (C bindings, mature) vs `ml-kem`/`ml-dsa` (pure Rust, less mature)? Decision deferred to Phase 4.
6. **Configuration migration.** Hard-cut TOML or runtime-load both formats? Recommendation: TOML-only with offline `convert-config` tool.
7. **Resource compression.** Reference supports it; do we? Yes if interop demands it (very likely).

---

## 13. References

- Reticulum manual: `https://reticulum.network/manual/`
- Reference implementation: `https://github.com/markqvist/Reticulum` (read-only reference; do not copy code post-April-2025)
- Beechat Reticulum-rs: `https://github.com/BeechatNetworkSystemsLtd/Reticulum-rs`
- rnsd-rs daemon branch: `https://codeberg.org/lgh/Reticulum-rs/src/branch/daemon-mode`
- RetiNet: `https://codeberg.org/skyguy/retinet`
- microReticulum: `https://github.com/attermann/microReticulum`
- FOSDEM 26 community meetup slides: `https://fosdem.org/2026/events/attachments/9NCWUR-reticulum_community_meetup_implementations_migration_and_future/slides/267005/reticulum_dimz1j8.pdf`
- FOSDEM 26 formal spec BoF: `https://fosdem.org/2026/schedule/event/NEN89E-future-of-reticulum/`
- FOSDEM 26 Beechat talk: `https://fosdem.org/2026/schedule/event/KF7STF-reticulum-rs_porting_the_trustless_mesh_from_python_to_rust/`

---

## 14. First Claude Code task

Concrete starting point for the implementer:

> Read `ARCHITECTURE.md`. Create the workspace per §2, with empty crate skeletons (`lib.rs` containing only the module declarations specified in §3 and §4 stubbed with `todo!()` or empty types). Set up `Cargo.toml` workspace dependencies per §2.3. Create `LICENSE-MIT`, `LICENSE-APACHE`, `README.md` (brief — point to ARCHITECTURE.md), `CONTRIBUTING.md`, `SECURITY.md`, `NOTICE` with the attributions per §10.4, `.gitignore`, GitHub Actions CI per §8.2 covering fmt/clippy/build/test/cargo-deny. Write `docs/adr/0001-bridge-and-extend.md` summarizing §1. Do **not** implement any protocol logic yet. Stop at a green CI on an empty skeleton.

Subsequent tasks pick up at Phase 1 §11.1.

---

*End of v0 draft. Review and amend before first commit.*
