archy/.claude/plans/luminous-snacking-snowflake.md
2026-03-17 00:03:08 +00:00

11 KiB

Phase 2: Mesh as Federation Transport

Context

Phase 1 built three independent communication silos: Tor-HTTP for federation, Meshcore LoRa for offline mesh, Nostr relays for discovery. They share no common abstraction — federation hardcodes Tor SOCKS5, mesh has its own serial listener, peer messaging uses a separate HTTP POST. Adding a new transport (LAN/mDNS) or changing routing logic means touching every caller.

Phase 2 unifies these under a NodeTransport trait with automatic transport selection per peer (Mesh > LAN > Tor), chunked LoRa messaging with Reed-Solomon FEC for payloads >160 bytes, CBOR delta compression for state sync, mDNS LAN discovery, and a "mesh only" off-grid mode.

The v2.0 roadmap (docs/roadmap-v2.0.md) also calls for a visual topology map and CRDT-based state sync — those are deferred to Phase 5 (Mesh Intelligence), but the transport abstraction here enables them.

Dependencies (Verified)

ciborium = "0.2.2"            # CBOR serde (134M downloads, active)
reed-solomon-erasure = "6.0"  # FEC (4M downloads, MIT)
mdns-sd = "0.18.2"            # LAN peer discovery (1.86M, active)

Architecture

core/archipelago/src/transport/
├── mod.rs              — NodeTransport trait, TransportKind, TransportRouter, PeerRegistry
├── tor.rs              — TorTransport (wraps node_message.rs SOCKS5 logic)
├── mesh_transport.rs   — MeshTransport (bridges MeshService cmd channel)
├── lan.rs              — LanTransport (mdns-sd + direct HTTP)
├── chunking.rs         — Split/reassemble with Reed-Solomon FEC
└── delta.rs            — CBOR delta encoding for NodeStateSnapshot

Unified Peer Identity

DID is the canonical identifier. Transport-specific addresses are attributes:

pub struct PeerRecord {
    pub did: String,
    pub pubkey_hex: String,
    pub name: Option<String>,
    pub trust_level: TrustLevel,
    // Transport capabilities
    pub mesh_contact_id: Option<u32>,
    pub lan_address: Option<SocketAddr>,
    pub onion_address: Option<String>,
    // Freshness
    pub last_mesh: Option<String>,
    pub last_lan: Option<String>,
    pub last_tor: Option<String>,
}

Transport Routing

TransportRouter::send_to_peer(did, payload)
  1. Look up PeerRecord by DID
  2. Filter to transports where peer has address AND transport is available AND not stale
  3. Sort by priority: Mesh(1) > LAN(2) > Tor(3)
  4. If mesh_only_mode: only allow Mesh
  5. Try each in order, fall back on failure

Chunking Protocol (for LoRa >160B)

Per-chunk structure (8 byte header + 124 byte payload = 132B, fits in 160B after encryption):

[0x01] [msg_id: u32 LE] [chunk_idx: u8] [total: u8] [is_parity: u8] [payload: 124B]

Reed-Solomon: 25% parity shards. 4 data chunks = 1 parity chunk. Any N-of-(N+parity) reconstructs. Max practical: ~10 chunks (~1.2KB) before LoRa airtime becomes unreasonable.

CBOR Delta Sync

Full JSON state: ~500-2000 bytes. CBOR delta when only CPU/mem changed: ~30-50 bytes. Fits single LoRa chunk.

pub struct StateDelta {
    pub ts: String,
    pub v: u8,
    pub apps: Option<Vec<AppStatus>>,      // Only changed apps
    pub apps_rm: Option<Vec<String>>,       // Removed app IDs
    pub cpu: Option<f64>,
    pub mem_u: Option<u64>,
    // ... only changed fields, short names for wire size
}

Implementation Steps

Step 1: Add deps to Cargo.toml

File: core/archipelago/Cargo.toml Add ciborium, reed-solomon-erasure, mdns-sd.

Step 2: Create transport/mod.rs — Core trait + PeerRegistry + TransportRouter

New file: core/archipelago/src/transport/mod.rs

  • NodeTransport trait: kind(), is_available(), send(address, payload)
  • TransportKind enum: Mesh = 1, Lan = 2, Tor = 3
  • TransportMessage: {from_did, payload: Vec<u8>, message_type}
  • PeerRegistry: unified peer store (DID -> PeerRecord), JSON persistence to transport-peers.json
  • TransportRouter: holds Vec<Box>, routes by priority with fallback
  • Register mod transport; in core/archipelago/src/lib.rs or main.rs

Step 3: Create transport/chunking.rs — Reed-Solomon FEC

New file: core/archipelago/src/transport/chunking.rs

  • encode_chunked(data: &[u8]) -> Vec<Vec<u8>> — split + FEC parity shards
  • ChunkReassembler — state machine tracking pending messages, attempts reconstruction when enough chunks arrive
  • decode_chunked(chunks: &[Option<Vec<u8>>]) -> Result<Vec<u8>> — reconstruct from data+parity
  • Unit tests: roundtrip, missing chunks, corrupted chunks

Step 4: Create transport/delta.rs — CBOR delta sync

New file: core/archipelago/src/transport/delta.rs

  • compute_delta(prev, curr: &NodeStateSnapshot) -> StateDelta
  • apply_delta(base: &NodeStateSnapshot, delta: &StateDelta) -> NodeStateSnapshot
  • encode_cbor(delta) -> Vec<u8> via ciborium
  • decode_cbor(bytes) -> Result<StateDelta>
  • Unit tests: delta correctness, CBOR roundtrip, size comparison vs JSON

Step 5: Create transport/tor.rs — TorTransport

New file: core/archipelago/src/transport/tor.rs

  • Wraps existing node_message::send_to_peer() pattern (reqwest + SOCKS5)
  • Reuses: node_message.rs:66 send logic, node_message.rs:115 health check
  • is_available() checks SOCKS5 proxy reachable at 127.0.0.1:9050
  • Receives handled by existing HTTP handler in api/handler.rs

Step 6: Create transport/mesh_transport.rs — MeshTransport

New file: core/archipelago/src/transport/mesh_transport.rs

  • Holds reference to Arc<RwLock<Option<MeshService>>> (same as RpcHandler)
  • send() routes through MeshService::send_message() for small payloads
  • For payloads >124B: chunk with chunking::encode_chunked(), send each chunk via MeshCommand::SendText
  • 200ms inter-chunk delay for LoRa airtime fairness
  • is_available() checks mesh device connected via MeshService::status()

Step 7: Create transport/lan.rs — LanTransport

New file: core/archipelago/src/transport/lan.rs

  • Uses mdns-sd::ServiceDaemon for discovery
  • Advertises: _archipelago._tcp.local. with TXT records: did=..., pubkey=..., version=...
  • Browses for peers, updates PeerRegistry when found
  • send(): direct HTTP POST to http://{ip}:5678/archipelago/node-message (same endpoint as Tor, no proxy)
  • Non-blocking init — if Avahi not available, fail gracefully

Step 8: Add mesh_only_mode to MeshConfig

File: core/archipelago/src/mesh/mod.rs

  • Add pub mesh_only_mode: Option<bool> to MeshConfig (with #[serde(default)])
  • TransportRouter reads this flag to restrict routing

Step 9: Wire TransportRouter into server.rs

File: core/archipelago/src/server.rs (~line 136, after mesh service init)

  • Create PeerRegistry, load from disk
  • Create TorTransport, MeshTransport, LanTransport
  • Start LAN advertisement + discovery
  • Create TransportRouter with all three + mesh_only flag
  • Spawn background task bridging MeshEvent::IdentityReceived -> PeerRegistry
  • Store router on RpcHandler

Step 10: Add transport RPC endpoints

New file: core/archipelago/src/api/rpc/transport.rs Modify: core/archipelago/src/api/rpc/mod.rs (add dispatch routes)

Endpoints:

  • transport.status — available transports, mesh_only flag, peer count
  • transport.peers — unified peer list with per-peer transport capabilities + preferred transport
  • transport.send — send message via best transport (by DID)
  • transport.set-mode — toggle mesh_only mode

Step 11: Add opt-in federation sync via transport

File: core/archipelago/src/federation.rs

  • Add sync_with_peer_via_transport() alongside existing sync_with_peer()
  • Uses CBOR delta encoding when transport is mesh (saves bandwidth)
  • Falls back to full JSON over Tor for backward compat
  • Background sync task checks for TransportRouter, uses it if available

Step 12: Frontend — transport store + UI indicators

New file: neode-ui/src/stores/transport.ts — Pinia store for transport status Modify: neode-ui/src/views/Mesh.vue — "OFF-GRID" indicator when mesh_only mode Modify: neode-ui/mock-backend.js — add transport.* mock RPC responses

Step 13: Mock backend transport data

File: neode-ui/mock-backend.js Add transport.status, transport.peers, transport.send, transport.set-mode mock responses with realistic data showing mixed transport capabilities.

Files Summary

New (8)

  1. core/archipelago/src/transport/mod.rs
  2. core/archipelago/src/transport/tor.rs
  3. core/archipelago/src/transport/mesh_transport.rs
  4. core/archipelago/src/transport/lan.rs
  5. core/archipelago/src/transport/chunking.rs
  6. core/archipelago/src/transport/delta.rs
  7. core/archipelago/src/api/rpc/transport.rs
  8. neode-ui/src/stores/transport.ts

Modified (8)

  1. core/archipelago/Cargo.toml — add 3 deps
  2. core/archipelago/src/lib.rs or main — mod transport;
  3. core/archipelago/src/server.rs — init router, bridge events
  4. core/archipelago/src/api/rpc/mod.rs — dispatch + store router
  5. core/archipelago/src/mesh/mod.rsmesh_only_mode config field
  6. core/archipelago/src/federation.rssync_with_peer_via_transport()
  7. neode-ui/src/views/Mesh.vue — off-grid indicator
  8. neode-ui/mock-backend.js — transport.* mock data

Reused Existing Code

  • node_message.rs:66-112 — Tor SOCKS5 send logic -> wrapped by TorTransport
  • node_message.rs:115-135 — Tor health check -> TorTransport.is_available()
  • mesh/mod.rs:209-282 — MeshService::send_message() -> called by MeshTransport
  • mesh/listener.rs:35-37 — MeshCommand channel -> used for chunked sends
  • mesh/crypto.rs — X25519 ECDH + ChaCha20-Poly1305 -> reused as-is
  • federation.rs:36-49 — FederatedNode struct -> PeerRecord wraps this
  • federation.rs:179-191 — update_node_state() -> adapted for CBOR deltas

Verification

  1. Unit tests (run on dev server):

    cargo test --all-features -- transport
    cargo test --all-features -- chunking
    cargo test --all-features -- delta
    
  2. Frontend type check:

    cd neode-ui && npm run type-check
    
  3. Dev mode test:

    cd neode-ui && npm start
    # Navigate to /dashboard/mesh — verify OFF-GRID toggle
    
  4. Deploy + integration test:

    ./scripts/deploy-to-target.sh --live
    # On .228: curl transport.status RPC
    # Verify mDNS advertises _archipelago._tcp.local.
    # If mesh device connected: send chunked message >160B to .198
    # Verify federation sync uses CBOR delta when available
    

Implementation Order

Week 1: Steps 1-4 (deps, trait, chunking, delta — pure library code with unit tests) Week 2: Steps 5-7 (three transport implementations) Week 3: Steps 8-11 (integration: server wiring, RPC, federation migration) Week 4: Steps 12-13 (frontend UI, mock data, deploy + test)