2026-05-17 18:07:40 -04:00
//! Mesh networking: LoRa radio integration for offline peer discovery
2026-03-17 00:03:08 +00:00
//! and encrypted messaging between Archipelago nodes.
//!
2026-05-17 18:07:40 -04:00
//! Supports Meshcore firmware via Companion USB and Meshtastic firmware via
//! the Meshtastic serial API on Heltec, T-Beam, RAK WisBlock, Station G2,
//! and other ESP32/nRF52-based LoRa boards.
2026-03-17 00:03:08 +00:00
2026-03-17 02:26:07 +00:00
pub mod alerts ;
pub mod bitcoin_relay ;
2026-03-17 00:03:08 +00:00
pub mod crypto ;
pub mod listener ;
2026-05-17 18:07:40 -04:00
pub mod meshtastic ;
feat: Phase 3 Week 3 — typed messages + store-and-forward outbox
- Create mesh/message_types.rs: typed message envelope system
- MeshMessageType enum: Text, Alert, Invoice, PsbtHash, Coordinate,
PrekeyBundle, SessionInit, BlockHeader, TxRelay, LightningRelay
- TypedEnvelope: CBOR wire format with 0x02 prefix, optional Ed25519 sig
- Payload types: AlertPayload (with AlertType enum), InvoicePayload
(sats as u64), Coordinate (integer microdegrees, no float),
PsbtHashPayload, BlockHeaderPayload, TxRelayPayload, LightningRelayPayload
- Signed envelope creation + verification for alerts/block headers
- 8 unit tests
- Create mesh/outbox.rs: store-and-forward message queue
- PendingMessage with TTL (24h default), retry count, relay hops (max 3)
- MeshOutbox: persistent VecDeque, max 200 messages, expiry, relay support
- Disk persistence to mesh-outbox.json
- 6 unit tests: enqueue, deliver, expire, persistence, max size, relay hops
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:08:58 +00:00
pub mod message_types ;
pub mod outbox ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
pub mod protocol ;
2026-03-17 01:50:22 +00:00
pub mod ratchet ;
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
pub mod reticulum ;
2026-06-17 19:19:32 -04:00
pub mod scheduler ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
pub mod serial ;
2026-03-17 01:54:26 +00:00
pub mod session ;
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
pub mod steganography ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
pub mod types ;
2026-03-17 01:28:35 +00:00
pub mod x3dh ;
2026-03-17 00:03:08 +00:00
pub use types ::* ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
use alerts ::DeadManSwitch ;
2026-03-17 00:03:08 +00:00
use anyhow ::{ Context , Result } ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
use bitcoin_relay ::{ BlockHeaderCache , RelayTracker } ;
2026-03-17 00:03:08 +00:00
use ed25519_dalek ::SigningKey ;
use listener ::MeshState ;
use serde ::{ Deserialize , Serialize } ;
use std ::path ::{ Path , PathBuf } ;
use std ::sync ::Arc ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
use std ::time ::Duration ;
2026-03-17 00:03:08 +00:00
use tokio ::fs ;
2026-03-22 03:30:21 +00:00
use tokio ::sync ::watch ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
use tracing ::{ error , info , warn } ;
2026-03-17 00:03:08 +00:00
const MESH_CONFIG_FILE : & str = " mesh-config.json " ;
2026-04-18 14:02:34 -04:00
const MESH_IGNORED_RADIO_FILE : & str = " mesh-ignored-radio-contacts.json " ;
feat(storage): encrypt chat history + mesh contacts at rest, atomic writes, persist contacts (#12)
User: chat history (messages + mesh/Tor contacts) must persist and be
secure/encrypted per best practice. Root cause of the .198 loss was the B17
mount race writing empty stores over real data (B17 already fixes the trigger);
this hardens storage so it can never silently lose or expose data:
- storage_crypto: shared at-rest envelope mirroring credentials::store — key =
SHA-256(domain ‖ node identity key) (seed-derived, per-store domain
separation), ChaCha20-Poly1305 AEAD with a random 96-bit nonce, tamper-evident.
Transparent migration of legacy plaintext files. Unit-tested (round-trip,
wrong-key/tamper rejection, plaintext detection).
- messages.json: encrypted at rest + ATOMIC write (temp+rename) so a crash/
reboot mid-write cannot corrupt history; decrypt-with-migration on load; a
failed decrypt never overwrites the on-disk data.
- mesh contacts (alias/notes/pinned/blocked): were ONLY in memory and lost on
every restart — now persisted to mesh-contacts.json (encrypted, atomic),
loaded on MeshState startup, saved after contacts-save/contacts-block.
Explicit clear (mesh.clear-all) still wipes everything, as intended.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:54:37 -04:00
const MESH_CONTACTS_FILE : & str = " mesh-contacts.json " ;
2026-03-17 00:03:08 +00:00
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Derive a stable synthetic `contact_id` for a federation peer from its
/// archipelago ed25519 pubkey. Mesh LoRa contacts use meshcore firmware's
/// own ID space (small ints 0..N), so federation peers are mapped into the
/// high half of u32 space to avoid collision. Both the receive path
/// (`inject_typed_from_federation`) and the startup pre-seed use this
/// formula so they always produce the same id for the same peer.
2026-06-19 05:03:18 -04:00
/// Mesh contacts at or above this id are synthetic federation peers (the high
/// half of the u32 space). Meshcore radio contacts use the firmware's low-int id
/// space, so this bit cleanly distinguishes "arrived over the authenticated
/// federation transport" from "heard over the radio".
pub ( crate ) const FEDERATION_CONTACT_ID_BASE : u32 = 0x8000_0000 ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
pub ( crate ) fn federation_peer_contact_id ( archipelago_pubkey_hex : & str ) -> u32 {
let bytes = hex ::decode ( archipelago_pubkey_hex ) . unwrap_or_default ( ) ;
if bytes . len ( ) < 4 {
return 0x8000_0001 ;
}
let low = u32 ::from_le_bytes ( [ bytes [ 0 ] , bytes [ 1 ] , bytes [ 2 ] , bytes [ 3 ] ] ) ;
0x8000_0000 | ( low & 0x7FFF_FFFF )
}
2026-06-19 13:25:24 -04:00
/// Bind radio (LoRa) contacts to their federation twin's archipelago identity.
///
/// The same physical node commonly appears twice in the peer table: a radio
/// contact (low `contact_id`, firmware routing key only, `arch_pubkey_hex ==
/// None`) and a federation peer (high `contact_id`, `arch_pubkey_hex` set). The
/// radio half carries no archipelago identity because identity adverts are no
/// longer broadcast on the public channel (anti-spam), so the `!ai` trust gate
/// and envelope signature verification have no key to check a radio asker
/// against — a `!ai` from a trusted node over LoRa is therefore denied, and the
/// node shows up as two separate contacts.
///
/// We correlate the two halves by exact, case-insensitive `advert_name` and copy
/// the federation peer's `arch_pubkey_hex`/`did`/`x25519` onto the radio peer.
/// This only supplies a CANDIDATE identity key; it does NOT bypass
/// authentication. A radio envelope must still carry an Ed25519 signature that
/// verifies against this bound key (see `handle_typed_envelope_direct`), so a
/// meshcore node merely *named* like a trusted node cannot impersonate it — it
/// cannot produce the signature. The candidate key comes from the authenticated
/// federation handshake (`nodes.json`), never from anything the radio packet
/// claims. Names held by more than one federation peer are treated as ambiguous
/// and skipped so a duplicate name can't bind the wrong identity.
pub ( crate ) fn bind_federation_twins ( peers : & mut std ::collections ::HashMap < u32 , MeshPeer > ) {
// name (lowercased) -> federation identity; `None` marks an ambiguous name
// (seen on more than one federation peer) which we must not bind.
type FedIdentity = ( String , Option < String > , Option < [ u8 ; 32 ] > ) ;
let mut fed_by_name : std ::collections ::HashMap < String , Option < FedIdentity > > =
std ::collections ::HashMap ::new ( ) ;
for p in peers . values ( ) {
if p . contact_id < FEDERATION_CONTACT_ID_BASE {
continue ;
}
let Some ( arch ) = p . arch_pubkey_hex . clone ( ) else {
continue ;
} ;
let name = p . advert_name . trim ( ) . to_ascii_lowercase ( ) ;
if name . is_empty ( ) {
continue ;
}
fed_by_name
. entry ( name )
. and_modify ( | e | * e = None ) // a second federation peer with this name → ambiguous
. or_insert ( Some ( ( arch , p . did . clone ( ) , p . x25519_pubkey ) ) ) ;
}
if fed_by_name . is_empty ( ) {
return ;
}
for p in peers . values_mut ( ) {
if p . contact_id > = FEDERATION_CONTACT_ID_BASE | | p . arch_pubkey_hex . is_some ( ) {
continue ;
}
let name = p . advert_name . trim ( ) . to_ascii_lowercase ( ) ;
if name . is_empty ( ) {
continue ;
}
if let Some ( Some ( ( arch , did , x25519 ) ) ) = fed_by_name . get ( & name ) {
p . arch_pubkey_hex = Some ( arch . clone ( ) ) ;
if p . did . is_none ( ) {
p . did = did . clone ( ) ;
}
if p . x25519_pubkey . is_none ( ) {
p . x25519_pubkey = * x25519 ;
}
}
}
}
2026-06-20 08:01:14 -04:00
/// One logical contact after collapsing cross-transport twins (see
/// [`group_peer_twins`]). A node reachable both over LoRa and over federation
/// has two `MeshPeer` rows (different `contact_id`s) but is one conversation.
pub ( crate ) struct PeerGroup {
/// The peer row the UI should address. The radio twin when one exists (so
/// `send_typed_wire` stays mesh-first — LoRa if reachable, else federation
/// via the bound arch key), otherwise the federation row. Gap-healed so its
/// name / `arch_pubkey_hex` / `did` are populated from whichever twin had
/// them, and `reachable` is the OR across the group.
pub canonical : MeshPeer ,
/// Every `contact_id` in the group. The conversation's messages are the
/// union of those keyed by any of these ids — federation-injected messages
/// land on the federation twin's id, radio messages on the radio twin's.
pub contact_ids : Vec < u32 > ,
}
/// Collapse cross-transport twin peers into one conversation per identity.
///
/// The same node commonly appears twice in the peer table: a radio twin (low
/// `contact_id`, firmware routing key) and a federation twin (high
/// `contact_id`, archipelago key), correlated by [`bind_federation_twins`]
/// which copies `arch_pubkey_hex` onto the radio twin but leaves both rows.
/// Messages are keyed by `peer_contact_id`, so they split across the two ids:
/// a federation-injected message sits on the federation row while the user may
/// open the radio row and see an empty thread (the `.120`→`.89` symptom).
///
/// Group peers by `arch_pubkey_hex` when set, else treat each peer as its own
/// singleton group keyed by `contact_id`. Grouping is done ONLY here at surface
/// time — never re-keyed at bind time — so outbound routing keeps the distinct
/// per-twin `contact_id`s and stays mesh-first. First-seen order is preserved
/// for stable downstream sorting.
pub ( crate ) fn group_peer_twins ( peers : & [ MeshPeer ] ) -> Vec < PeerGroup > {
let mut order : Vec < String > = Vec ::new ( ) ;
let mut groups : std ::collections ::HashMap < String , Vec < MeshPeer > > =
std ::collections ::HashMap ::new ( ) ;
for p in peers {
let key = match p . arch_pubkey_hex . as_deref ( ) {
Some ( arch ) if ! arch . is_empty ( ) = > format! ( " arch: {} " , arch . to_ascii_lowercase ( ) ) ,
_ = > format! ( " cid: {} " , p . contact_id ) ,
} ;
if ! groups . contains_key ( & key ) {
order . push ( key . clone ( ) ) ;
}
groups . entry ( key ) . or_default ( ) . push ( p . clone ( ) ) ;
}
let mut out = Vec ::with_capacity ( order . len ( ) ) ;
for key in order {
let members = match groups . remove ( & key ) {
Some ( m ) if ! m . is_empty ( ) = > m ,
_ = > continue ,
} ;
let contact_ids : Vec < u32 > = members . iter ( ) . map ( | m | m . contact_id ) . collect ( ) ;
// Canonical = the radio twin (lowest id below the federation base) when
// one exists, else the lowest id overall (a federation-only peer).
let canonical_src = members
. iter ( )
. filter ( | m | m . contact_id < FEDERATION_CONTACT_ID_BASE )
. min_by_key ( | m | m . contact_id )
. or_else ( | | members . iter ( ) . min_by_key ( | m | m . contact_id ) )
. expect ( " non-empty members " ) ;
let mut canonical = canonical_src . clone ( ) ;
// Heal gaps from the twin: a radio row may lack the advert name, arch
// identity, or did that only the federation row carries.
if canonical . advert_name . trim ( ) . is_empty ( ) {
if let Some ( named ) = members . iter ( ) . find ( | m | ! m . advert_name . trim ( ) . is_empty ( ) ) {
canonical . advert_name = named . advert_name . clone ( ) ;
}
}
if canonical . arch_pubkey_hex . is_none ( ) {
canonical . arch_pubkey_hex = members . iter ( ) . find_map ( | m | m . arch_pubkey_hex . clone ( ) ) ;
}
if canonical . did . is_none ( ) {
canonical . did = members . iter ( ) . find_map ( | m | m . did . clone ( ) ) ;
}
// Reachable if ANY twin is reachable (radio path or off-radio federation).
canonical . reachable = members . iter ( ) . any ( | m | m . reachable ) ;
out . push ( PeerGroup {
canonical ,
contact_ids ,
} ) ;
}
out
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Upsert a mesh peer record representing a federation node so the UI can
/// address it as a chat and `mesh.send-content` can route ContentRef to it.
/// Existing entries (same contact_id) are updated in place, preserving any
/// previously observed radio state (rssi/snr/hops).
pub ( crate ) async fn upsert_federation_peer (
state : & Arc < listener ::MeshState > ,
archipelago_pubkey_hex : & str ,
did : & str ,
name : Option < & str > ,
) -> u32 {
let contact_id = federation_peer_contact_id ( archipelago_pubkey_hex ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let display_name = name . map ( | s | s . to_string ( ) ) . unwrap_or_else ( | | {
let short = & archipelago_pubkey_hex [ .. archipelago_pubkey_hex . len ( ) . min ( 8 ) ] ;
format! ( " Archipelago {} " , short )
} ) ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
let mut peers = state . peers . write ( ) . await ;
let existing = peers . get ( & contact_id ) . cloned ( ) ;
let peer = MeshPeer {
contact_id ,
advert_name : display_name ,
did : Some ( did . to_string ( ) ) ,
pubkey_hex : Some ( archipelago_pubkey_hex . to_string ( ) ) ,
2026-06-19 05:03:18 -04:00
// Federation peers are authenticated by the Tor relay upstream; their
// archipelago key is known, so bind it as the identity key too.
arch_pubkey_hex : Some ( archipelago_pubkey_hex . to_string ( ) ) ,
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
x25519_pubkey : existing . as_ref ( ) . and_then ( | p | p . x25519_pubkey ) ,
rssi : existing . as_ref ( ) . and_then ( | p | p . rssi ) ,
snr : existing . as_ref ( ) . and_then ( | p | p . snr ) ,
last_heard : chrono ::Utc ::now ( ) . to_rfc3339 ( ) ,
hops : existing . as_ref ( ) . map ( | p | p . hops ) . unwrap_or ( 0 ) ,
2026-06-18 08:08:52 -04:00
last_advert : existing . as_ref ( ) . map ( | p | p . last_advert ) . unwrap_or ( 0 ) ,
// Federation peers are reachable off-radio (Tor/FIPS), so always true.
reachable : true ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
// Off-radio E2E (federation) is handled by the archy-peer path; preserve
// any radio PKI capability learned for a twinned contact.
pkc_capable : existing . as_ref ( ) . map ( | p | p . pkc_capable ) . unwrap_or ( false ) ,
feat(mesh): Meshtastic RSSI/SNR + peer-location map wiring (backlog #14/#15, part 1)
Backend: parse_mesh_packet now decodes MeshPacket.rx_snr (field 8, float) and
rx_rssi (field 12, int32), and a new POSITION_APP branch decodes Position.
latitude_i/longitude_i (fields 1/2, sfixed32) -- all field numbers confirmed
against the canonical meshtastic/protobufs mesh.proto, not guessed. Threaded
through ParsedContact -> refresh_contacts -> MeshPeer (mirroring how
pkc_capable was wired for #17), so mesh.peers now surfaces real rssi/snr/lat/
lon instead of always-null. Fixed a real bug found along the way:
update_node_info's unconditional contact replace would have silently wiped
any already-tracked signal/position data on the next NodeInfo packet -- now
preserves it.
Frontend: mesh.ts's updateNodePositionsFromPeers() feeds real position data
into the SAME nodePositions map MeshMap.vue already renders from (parallel to
the existing Coordinate/Alert-message path) -- MeshMap.vue itself needed zero
changes, it was already built for this.
105/105 mesh tests pass (4 new: rx_snr/rx_rssi decode, position decode +
incomplete-field handling, full packet_to_inbound_frame integration).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 22:52:42 -04:00
lat : existing . as_ref ( ) . and_then ( | p | p . lat ) ,
lon : existing . as_ref ( ) . and_then ( | p | p . lon ) ,
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
} ;
peers . insert ( contact_id , peer ) ;
2026-06-19 13:25:24 -04:00
// A radio twin of this node (same advert_name, no arch identity yet) can now
// inherit this federation peer's archipelago key — so a signed `!ai`/typed
// message arriving over LoRa from it authenticates and the duplicate radio
// contact resolves to the same identity.
bind_federation_twins ( & mut peers ) ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
drop ( peers ) ;
state . update_peer_count ( ) . await ;
contact_id
}
2026-06-17 06:12:56 -04:00
/// Purge a federation peer from all live mesh state and persisted contacts so
/// removing a node (federation.remove-node) also clears its chat contact,
/// thread, and any per-contact customisation — otherwise a stale/renamed node
/// (e.g. an old "Arch HP" entry) lingers in the chat list even after it's gone
/// from `nodes.json` (#2). Keyed by the synthetic `contact_id` for the peer
/// table/messages and by `pubkey_hex` for the pubkey-keyed contacts/presence
/// stores.
pub ( crate ) async fn purge_federation_peer (
state : & Arc < listener ::MeshState > ,
contact_id : u32 ,
pubkey_hex : & str ,
data_dir : & Path ,
) {
state . peers . write ( ) . await . remove ( & contact_id ) ;
state . shared_secrets . write ( ) . await . remove ( & contact_id ) ;
state
. messages
. write ( )
. await
. retain ( | m | m . peer_contact_id ! = contact_id ) ;
state . presence . write ( ) . await . remove ( pubkey_hex ) ;
let mut contacts = state . contacts . write ( ) . await ;
if contacts . remove ( pubkey_hex ) . is_some ( ) {
let snapshot = contacts . clone ( ) ;
drop ( contacts ) ;
if let Err ( e ) = save_mesh_contacts ( data_dir , & snapshot ) . await {
warn! ( " Failed to persist mesh contacts after purge: {} " , e ) ;
}
}
state . update_peer_count ( ) . await ;
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Load federation nodes from disk and upsert each as a synthetic mesh peer.
/// Called at MeshService startup so the chat list already contains every
/// known federation node — users can share files to them without first
/// receiving a message.
pub ( crate ) async fn seed_federation_peers_into_mesh (
state : & Arc < listener ::MeshState > ,
data_dir : & Path ,
) {
let nodes = match crate ::federation ::load_nodes ( data_dir ) . await {
Ok ( n ) = > n ,
Err ( _ ) = > return ,
} ;
2026-06-15 12:29:12 -04:00
// Skip nodes whose onion we've already seeded: the same physical node can
// linger in the federation list under two dids (see B1/B2). Seeding both
// would create two chat contacts for one node — one by name+logo and one
// by raw did. One onion → one mesh contact.
let mut seen_onions = std ::collections ::HashSet ::new ( ) ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
for node in nodes {
2026-06-15 12:29:12 -04:00
let onion_key = node . onion . trim_end_matches ( " .onion " ) . to_string ( ) ;
if ! onion_key . is_empty ( ) & & ! seen_onions . insert ( onion_key ) {
tracing ::debug! ( did = % node . did , onion = % node . onion , " skipping duplicate federation node (onion already seeded) " ) ;
continue ;
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
upsert_federation_peer ( state , & node . pubkey , & node . did , node . name . as_deref ( ) ) . await ;
}
}
2026-03-17 00:03:08 +00:00
/// Mesh configuration (persisted to disk).
#[ derive(Debug, Clone, Serialize, Deserialize) ]
pub struct MeshConfig {
pub enabled : bool ,
/// Specific device path, or None for auto-detection.
#[ serde(default) ]
pub device_path : Option < String > ,
/// Channel name for broadcasts.
#[ serde(default) ]
pub channel_name : Option < String > ,
2026-06-26 09:44:23 -04:00
/// Meshtastic LoRa region (e.g. "EU_868", "US", "ANZ"). Fresh-flashed
/// Meshtastic radios ship region-UNSET and are RF-silent until a region is
/// set, so archy provisions this region on connect to bring every node onto
/// the same band automatically (the parity equivalent of a meshcore radio
/// coming up on its configured band). Ignored for meshcore devices and when
/// unset/None.
#[ serde(default) ]
pub lora_region : Option < String > ,
2026-03-17 00:03:08 +00:00
/// Whether to periodically broadcast our identity.
#[ serde(default) ]
pub broadcast_identity : bool ,
/// Custom advertised name on the mesh network.
#[ serde(default) ]
pub advert_name : Option < String > ,
/// Off-grid mode: disable Tor/internet, route everything via mesh only.
#[ serde(default) ]
pub mesh_only_mode : Option < bool > ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
/// Announce new Bitcoin block headers over mesh (internet-connected nodes only).
#[ serde(default) ]
pub announce_block_headers : bool ,
feat(wallet,content,seed): Fedimint dual-ecash, paid content streaming, seed ceremony
- Fedimint ecash alongside Cashu: fedimint-clientd (fmcd) HTTP bridge,
fedimint_client, fedimint RPC, wallet wiring
- Paid peer content: content invoices + streaming content server + content RPCs
- Seed-phrase ceremony/reveal RPCs and CLI ceremony tool
- LND wallet, mesh status/messaging, app-stack (netbird HTTPS), and
decoupled-update wiring; Fedimint Client core app in catalog
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:21:07 -04:00
/// Accept Bitcoin block headers received over mesh from peers. On by default;
/// turn off to ignore inbound headers (the receive half of issue #28).
#[ serde(default = " default_true " ) ]
pub receive_block_headers : bool ,
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
/// Steganographic encoding mode for mesh messages (Normal = disabled).
#[ serde(default) ]
pub steganography_mode : steganography ::SteganographyMode ,
2026-03-20 11:56:20 +00:00
/// Encrypt directed relay messages (TX, Lightning, block headers) via ratchet or shared secret.
/// Set to false to disable encryption for debugging or rollback.
#[ serde(default = " default_true " ) ]
pub encrypt_relay_messages : bool ,
2026-06-17 17:41:15 -04:00
/// Answer AI queries (AssistQuery) from peers using this node's local LLM
/// (issue #50). Off by default — the node only becomes a mesh AI on opt-in.
#[ serde(default) ]
pub assistant_enabled : bool ,
/// Ollama model used to answer AI queries. None → the built-in default.
#[ serde(default) ]
pub assistant_model : Option < String > ,
/// When true (default), only federation-Trusted peers may ask; when false,
/// any peer on the mesh may ask (spends this node's compute + airtime).
#[ serde(default = " default_true " ) ]
pub assistant_trusted_only : bool ,
2026-06-17 19:19:32 -04:00
/// Which AI backend answers queries: "claude" (the shared Claude proxy
/// token at secrets/claude-api-key — default for now, works without a
/// local GPU) or "ollama" (a local model on this node).
#[ serde(default = " default_assistant_backend " ) ]
pub assistant_backend : String ,
2026-06-18 03:33:37 -04:00
/// Per-contact allowlist (ed25519 pubkey hex) permitted to use `!ai` even
/// when `assistant_trusted_only` is on and they aren't federation-Trusted.
#[ serde(default) ]
pub assistant_allowed_contacts : Vec < String > ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
/// Pin the expected firmware on `device_path`/auto-detected ports. A
/// reflashable board (e.g. Heltec V3) can run Meshcore, Meshtastic, or
/// RNode firmware, so probe order alone is best-effort — set this when an
/// operator knows which one is plugged in. When `Some`, only that
/// device's probe runs (no other firmware's init bytes are ever injected
/// into the port); `None` keeps today's Meshcore→Meshtastic→Reticulum
/// strict-probe auto-detect.
#[ serde(default) ]
pub device_kind : Option < types ::DeviceType > ,
2026-07-03 18:08:35 +00:00
/// Optional plain-TCP Reticulum interface — radio-less dev/verification
/// alternative to the serial-RNode path (see `types::ReticulumTcpConfig`
/// doc comment). Not exposed via `mesh.configure`/frontend; hand-edit
/// this file to use it. `None` (default) preserves today's
/// serial-only/auto-detect behavior unchanged.
#[ serde(default) ]
pub reticulum_tcp : Option < types ::ReticulumTcpConfig > ,
2026-06-17 19:19:32 -04:00
}
fn default_assistant_backend ( ) -> String {
" claude " . to_string ( )
2026-03-20 11:56:20 +00:00
}
fn default_true ( ) -> bool {
true
2026-03-17 00:03:08 +00:00
}
impl Default for MeshConfig {
fn default ( ) -> Self {
Self {
enabled : false ,
device_path : None ,
channel_name : Some ( " archipelago " . to_string ( ) ) ,
2026-06-26 09:44:23 -04:00
lora_region : None ,
2026-03-17 00:03:08 +00:00
broadcast_identity : true ,
advert_name : None ,
mesh_only_mode : None ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
announce_block_headers : false ,
feat(wallet,content,seed): Fedimint dual-ecash, paid content streaming, seed ceremony
- Fedimint ecash alongside Cashu: fedimint-clientd (fmcd) HTTP bridge,
fedimint_client, fedimint RPC, wallet wiring
- Paid peer content: content invoices + streaming content server + content RPCs
- Seed-phrase ceremony/reveal RPCs and CLI ceremony tool
- LND wallet, mesh status/messaging, app-stack (netbird HTTPS), and
decoupled-update wiring; Fedimint Client core app in catalog
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:21:07 -04:00
receive_block_headers : true ,
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
steganography_mode : steganography ::SteganographyMode ::Normal ,
2026-03-20 11:56:20 +00:00
encrypt_relay_messages : true ,
2026-06-17 17:41:15 -04:00
assistant_enabled : false ,
assistant_model : None ,
assistant_trusted_only : true ,
2026-06-17 19:19:32 -04:00
assistant_backend : default_assistant_backend ( ) ,
2026-06-18 03:33:37 -04:00
assistant_allowed_contacts : Vec ::new ( ) ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
device_kind : None ,
2026-07-03 18:08:35 +00:00
reticulum_tcp : None ,
2026-03-17 00:03:08 +00:00
}
}
}
pub async fn load_config ( data_dir : & Path ) -> Result < MeshConfig > {
let path = data_dir . join ( MESH_CONFIG_FILE ) ;
if ! path . exists ( ) {
return Ok ( MeshConfig ::default ( ) ) ;
}
let content = fs ::read_to_string ( & path )
. await
. context ( " Failed to read mesh config " ) ? ;
let config : MeshConfig = serde_json ::from_str ( & content ) . unwrap_or_default ( ) ;
Ok ( config )
}
pub async fn save_config ( data_dir : & Path , config : & MeshConfig ) -> Result < ( ) > {
fs ::create_dir_all ( data_dir )
. await
. context ( " Failed to create data dir " ) ? ;
let content =
serde_json ::to_string_pretty ( config ) . context ( " Failed to serialize mesh config " ) ? ;
fs ::write ( data_dir . join ( MESH_CONFIG_FILE ) , content )
. await
. context ( " Failed to write mesh config " ) ? ;
Ok ( ( ) )
}
2026-04-18 14:02:34 -04:00
pub async fn load_ignored_radio_contacts ( data_dir : & Path ) -> Vec < String > {
let path = data_dir . join ( MESH_IGNORED_RADIO_FILE ) ;
if ! path . exists ( ) {
return Vec ::new ( ) ;
}
match fs ::read_to_string ( & path ) . await {
Ok ( s ) = > serde_json ::from_str ::< Vec < String > > ( & s ) . unwrap_or_default ( ) ,
Err ( _ ) = > Vec ::new ( ) ,
}
}
pub async fn save_ignored_radio_contacts ( data_dir : & Path , pubkeys : & [ String ] ) -> Result < ( ) > {
fs ::create_dir_all ( data_dir ) . await . ok ( ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let content =
serde_json ::to_string_pretty ( pubkeys ) . context ( " Failed to serialize ignored-radio list " ) ? ;
2026-04-18 14:02:34 -04:00
fs ::write ( data_dir . join ( MESH_IGNORED_RADIO_FILE ) , content )
. await
. context ( " Failed to write ignored-radio list " ) ? ;
Ok ( ( ) )
}
feat(storage): encrypt chat history + mesh contacts at rest, atomic writes, persist contacts (#12)
User: chat history (messages + mesh/Tor contacts) must persist and be
secure/encrypted per best practice. Root cause of the .198 loss was the B17
mount race writing empty stores over real data (B17 already fixes the trigger);
this hardens storage so it can never silently lose or expose data:
- storage_crypto: shared at-rest envelope mirroring credentials::store — key =
SHA-256(domain ‖ node identity key) (seed-derived, per-store domain
separation), ChaCha20-Poly1305 AEAD with a random 96-bit nonce, tamper-evident.
Transparent migration of legacy plaintext files. Unit-tested (round-trip,
wrong-key/tamper rejection, plaintext detection).
- messages.json: encrypted at rest + ATOMIC write (temp+rename) so a crash/
reboot mid-write cannot corrupt history; decrypt-with-migration on load; a
failed decrypt never overwrites the on-disk data.
- mesh contacts (alias/notes/pinned/blocked): were ONLY in memory and lost on
every restart — now persisted to mesh-contacts.json (encrypted, atomic),
loaded on MeshState startup, saved after contacts-save/contacts-block.
Explicit clear (mesh.clear-all) still wipes everything, as intended.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:54:37 -04:00
/// Load persisted mesh contact customisations (alias / notes / pinned / blocked),
/// decrypting at rest with the node key and migrating any legacy plaintext file.
/// Returns an empty map on any error so a read failure never loses live state.
pub async fn load_mesh_contacts (
data_dir : & Path ,
) -> std ::collections ::HashMap < String , listener ::ContactEntry > {
let path = data_dir . join ( MESH_CONTACTS_FILE ) ;
let Ok ( raw ) = fs ::read ( & path ) . await else {
return std ::collections ::HashMap ::new ( ) ;
} ;
let bytes = if crate ::storage_crypto ::is_plaintext_json ( & raw ) {
raw
} else {
match crate ::storage_crypto ::derive_key (
data_dir ,
crate ::storage_crypto ::DOMAIN_MESH_CONTACTS ,
)
. await
{
Ok ( k ) = > match crate ::storage_crypto ::open ( & raw , & k ) {
Ok ( p ) = > p ,
Err ( e ) = > {
warn! ( " mesh contacts: decrypt failed ({e}); keeping in-memory state " ) ;
return std ::collections ::HashMap ::new ( ) ;
}
} ,
Err ( _ ) = > return std ::collections ::HashMap ::new ( ) ,
}
} ;
serde_json ::from_slice ( & bytes ) . unwrap_or_default ( )
}
/// Persist mesh contact customisations, encrypted at rest with the node key and
/// written atomically (temp + rename) so a crash mid-write can't corrupt them.
pub async fn save_mesh_contacts (
data_dir : & Path ,
contacts : & std ::collections ::HashMap < String , listener ::ContactEntry > ,
) -> Result < ( ) > {
fs ::create_dir_all ( data_dir ) . await . ok ( ) ;
let content = serde_json ::to_vec ( contacts ) . context ( " Failed to serialize mesh contacts " ) ? ;
let bytes = match crate ::storage_crypto ::derive_key (
data_dir ,
crate ::storage_crypto ::DOMAIN_MESH_CONTACTS ,
)
. await
{
Ok ( k ) = > crate ::storage_crypto ::seal ( & content , & k ) . unwrap_or ( content ) ,
Err ( _ ) = > content , // no key yet (pre-onboarding) → plaintext rather than no-write
} ;
let path = data_dir . join ( MESH_CONTACTS_FILE ) ;
let tmp = path . with_extension ( " json.tmp " ) ;
fs ::write ( & tmp , & bytes )
. await
. context ( " Failed to write mesh contacts tmp " ) ? ;
fs ::rename ( & tmp , & path )
. await
. context ( " Failed to rename mesh contacts " ) ? ;
Ok ( ( ) )
}
2026-03-17 00:03:08 +00:00
/// Detect serial devices that could be mesh radios.
/// Checks both Meshcore (via probe) and legacy Meshtastic paths.
pub async fn detect_devices ( ) -> Vec < String > {
serial ::detect_serial_devices ( ) . await
}
// ─── MeshService ────────────────────────────────────────────────────────
/// Top-level mesh networking service.
/// Manages the background listener, exposes APIs for RPC handlers.
pub struct MeshService {
state : Arc < MeshState > ,
config : MeshConfig ,
data_dir : PathBuf ,
shutdown_tx : Option < watch ::Sender < bool > > ,
listener_handle : Option < tokio ::task ::JoinHandle < ( ) > > ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
deadman_handle : Option < tokio ::task ::JoinHandle < ( ) > > ,
block_announcer_handle : Option < tokio ::task ::JoinHandle < ( ) > > ,
2026-04-14 10:24:27 -04:00
presence_handle : Option < tokio ::task ::JoinHandle < ( ) > > ,
2026-06-17 19:19:32 -04:00
scheduler_handle : Option < tokio ::task ::JoinHandle < ( ) > > ,
2026-03-17 00:03:08 +00:00
cmd_rx : Option < tokio ::sync ::mpsc ::Receiver < listener ::MeshCommand > > ,
// Crypto identity for this node
our_did : String ,
our_ed_pubkey_hex : String ,
our_x25519_secret : [ u8 ; 32 ] ,
our_x25519_pubkey_hex : String ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
signing_key : SigningKey ,
2026-04-18 11:53:06 -04:00
/// Human-readable server name (e.g. "Arch Dev", "ThinkPad") for mesh adverts.
server_name : Option < String > ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
// Phase 4: off-grid Bitcoin operations
pub block_header_cache : Arc < BlockHeaderCache > ,
pub relay_tracker : Arc < RelayTracker > ,
pub dead_man_switch : Arc < DeadManSwitch > ,
2026-06-17 19:19:32 -04:00
/// Scheduled / queued outbound mesh messages (issue #50, phase 1.7).
pub scheduler : Arc < scheduler ::MeshScheduler > ,
2026-03-17 00:03:08 +00:00
}
impl MeshService {
/// Create a new MeshService. Does not start the listener yet.
pub async fn new (
data_dir : & Path ,
signing_key : & SigningKey ,
did : & str ,
ed_pubkey_hex : & str ,
) -> Result < Self > {
let config = load_config ( data_dir ) . await ? ;
let channel_name = config
. channel_name
. clone ( )
. unwrap_or_else ( | | " archipelago " . to_string ( ) ) ;
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
let block_header_cache = Arc ::new ( BlockHeaderCache ::new ( ) ) ;
let relay_tracker = Arc ::new ( RelayTracker ::new ( ) ) ;
2026-03-20 11:56:20 +00:00
let session_manager = Arc ::new ( session ::SessionManager ::new ( data_dir ) ) ;
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
let ( state , _rx , cmd_rx ) = MeshState ::new (
& channel_name ,
Arc ::clone ( & block_header_cache ) ,
Some ( Arc ::clone ( & relay_tracker ) ) ,
config . steganography_mode ,
2026-03-20 11:56:20 +00:00
config . encrypt_relay_messages ,
feat(wallet,content,seed): Fedimint dual-ecash, paid content streaming, seed ceremony
- Fedimint ecash alongside Cashu: fedimint-clientd (fmcd) HTTP bridge,
fedimint_client, fedimint RPC, wallet wiring
- Paid peer content: content invoices + streaming content server + content RPCs
- Seed-phrase ceremony/reveal RPCs and CLI ceremony tool
- LND wallet, mesh status/messaging, app-stack (netbird HTTPS), and
decoupled-update wiring; Fedimint Client core app in catalog
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:21:07 -04:00
config . receive_block_headers ,
2026-03-20 11:56:20 +00:00
Arc ::clone ( & session_manager ) ,
feat(mesh): Telegram primitives pass + attachment transport router
Bundles the Phase 2b/3/4/5 work that accumulated across prior sessions
and the new attachment chunking router from this session. Everything
ships in one shot so the full mesh surface stays coherent on-wire.
Telegram primitives (variants 13–18, 20–22):
- Reply / Reaction / ReadReceipt / Forward / Edit / Delete
- Presence heartbeat + last-seen tracking
- ChannelInvite + ContactCard payload types
- MessageKey (sender_pubkey, sender_seq) as cross-transport identity
- Action menu, reply banner, edit banner, tombstones, (edited) marker
- Debounced auto-read-receipts on scroll + message arrival
Activated prototypes (Phase 4):
- PsbtHash send RPC
- Contacts CRUD (in-memory alias/notes/pinned/blocked)
- Outbox 📤 badge, rotate-prekeys button
- Chunked send fallback (MCIIXXTT framing) as auto-failover inside
send_typed_wire when a typed wire exceeds the LoRa per-frame budget
Unified inbox (Phase 1):
- conversations.list + conversations.messages RPCs (UI collapse deferred)
Attachment transport router (new this session):
- ContentInline variant 23 + ContentInlinePayload carrying file bytes
directly in the envelope for small files with no Tor path
- mesh.send-content-inline RPC — mirrors to local BlobStore, rides
send_typed_wire which auto-chunks over MCIIXXTT framing (~2.3 KB cap)
- mesh.transport-advice RPC as single source of truth for tier
decisions: auto-mesh / choose / tor-only / impossible
- Receive arm writes inline bytes to local BlobStore so the existing
content_ref card renderer handles both transports uniformly
- MeshState.blob_store field + order-independent propagation from
RpcHandler::set_blob_store / set_mesh_service
- Frontend handleAttachFile calls advice first, branches into silent
auto-send, transport-chooser modal, Tor-only path, or red error
- Transport modal with 📡 mesh / 🧅 Tor options + ETA + disabled
state when peer has no Tor reachability
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:40:19 -04:00
ed_pubkey_hex . to_string ( ) ,
2026-06-17 17:41:15 -04:00
listener ::AssistantConfig {
enabled : config . assistant_enabled ,
model : config . assistant_model . clone ( ) ,
trusted_only : config . assistant_trusted_only ,
2026-06-17 19:19:32 -04:00
backend : config . assistant_backend . clone ( ) ,
2026-06-18 03:33:37 -04:00
allowed_contacts : config . assistant_allowed_contacts . clone ( ) ,
2026-06-17 17:41:15 -04:00
} ,
data_dir . to_path_buf ( ) ,
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
) ;
2026-03-17 00:03:08 +00:00
// Derive X25519 keys from Ed25519 identity
let x25519_secret = crypto ::ed25519_secret_to_x25519 ( signing_key ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let x25519_pubkey =
crypto ::ed25519_pubkey_to_x25519 ( & signing_key . verifying_key ( ) . to_bytes ( ) ) ? ;
2026-03-17 00:03:08 +00:00
let x25519_pubkey_hex = hex ::encode ( x25519_pubkey ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let dead_man_switch = Arc ::new ( DeadManSwitch ::new ( data_dir ) . await . unwrap_or_else ( | e | {
warn! ( " Failed to load dead man config (using defaults): {} " , e ) ;
// Fallback: create with defaults (won't persist until configured)
tokio ::runtime ::Handle ::current ( )
. block_on ( DeadManSwitch ::new ( data_dir ) )
. expect ( " DeadManSwitch fallback should succeed " )
} ) ) ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
// Pre-seed mesh state with a synthetic peer record for every known
// federation node. Mesh LoRa discovery and federation have disjoint
// identity namespaces, so without this step a user can't address a
// federated peer from the mesh UI until one side receives over the
// radio — which never happens for nodes that only share Tor.
seed_federation_peers_into_mesh ( & state , data_dir ) . await ;
2026-04-18 14:02:34 -04:00
// Load the radio-contact blocklist so previously-wiped firmware
// contacts stay hidden after restart. Without this, meshcore's
// persistent on-device contact table regenerates the rows on the
// next refresh_contacts cycle and the user sees stale entries
// they already cleared.
{
let ignored = load_ignored_radio_contacts ( data_dir ) . await ;
let mut set = state . radio_contact_blocklist . write ( ) . await ;
for pk in ignored {
set . insert ( pk ) ;
}
}
feat(storage): encrypt chat history + mesh contacts at rest, atomic writes, persist contacts (#12)
User: chat history (messages + mesh/Tor contacts) must persist and be
secure/encrypted per best practice. Root cause of the .198 loss was the B17
mount race writing empty stores over real data (B17 already fixes the trigger);
this hardens storage so it can never silently lose or expose data:
- storage_crypto: shared at-rest envelope mirroring credentials::store — key =
SHA-256(domain ‖ node identity key) (seed-derived, per-store domain
separation), ChaCha20-Poly1305 AEAD with a random 96-bit nonce, tamper-evident.
Transparent migration of legacy plaintext files. Unit-tested (round-trip,
wrong-key/tamper rejection, plaintext detection).
- messages.json: encrypted at rest + ATOMIC write (temp+rename) so a crash/
reboot mid-write cannot corrupt history; decrypt-with-migration on load; a
failed decrypt never overwrites the on-disk data.
- mesh contacts (alias/notes/pinned/blocked): were ONLY in memory and lost on
every restart — now persisted to mesh-contacts.json (encrypted, atomic),
loaded on MeshState startup, saved after contacts-save/contacts-block.
Explicit clear (mesh.clear-all) still wipes everything, as intended.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:54:37 -04:00
// Restore persisted contact customisations (alias/notes/pinned/blocked),
// decrypted with the node key, so they survive restarts.
{
let saved = load_mesh_contacts ( data_dir ) . await ;
if ! saved . is_empty ( ) {
let mut contacts = state . contacts . write ( ) . await ;
for ( pk , entry ) in saved {
contacts . insert ( pk , entry ) ;
}
}
}
2026-03-17 00:03:08 +00:00
Ok ( Self {
state ,
config ,
data_dir : data_dir . to_path_buf ( ) ,
shutdown_tx : None ,
listener_handle : None ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
deadman_handle : None ,
block_announcer_handle : None ,
2026-04-14 10:24:27 -04:00
presence_handle : None ,
2026-06-17 19:19:32 -04:00
scheduler_handle : None ,
2026-03-17 00:03:08 +00:00
cmd_rx : Some ( cmd_rx ) ,
our_did : did . to_string ( ) ,
our_ed_pubkey_hex : ed_pubkey_hex . to_string ( ) ,
our_x25519_secret : x25519_secret ,
our_x25519_pubkey_hex : x25519_pubkey_hex ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
signing_key : signing_key . clone ( ) ,
2026-04-18 11:53:06 -04:00
server_name : None ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
block_header_cache ,
relay_tracker ,
dead_man_switch ,
2026-06-17 19:19:32 -04:00
scheduler : Arc ::new ( scheduler ::MeshScheduler ::load ( data_dir ) . await ) ,
2026-03-17 00:03:08 +00:00
} )
}
2026-04-18 11:53:06 -04:00
/// Set the human-readable server name used in mesh adverts.
pub fn set_server_name ( & mut self , name : Option < String > ) {
self . server_name = name ;
}
2026-03-17 00:03:08 +00:00
/// Start the background mesh listener.
pub fn start ( & mut self ) -> Result < ( ) > {
if self . listener_handle . is_some ( ) {
anyhow ::bail! ( " Mesh listener already running " ) ;
}
let ( shutdown_tx , shutdown_rx ) = watch ::channel ( false ) ;
self . shutdown_tx = Some ( shutdown_tx ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let cmd_rx = self
. cmd_rx
. take ( )
2026-03-17 00:03:08 +00:00
. ok_or_else ( | | anyhow ::anyhow! ( " Command channel already consumed " ) ) ? ;
let handle = listener ::spawn_mesh_listener (
Arc ::clone ( & self . state ) ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
self . data_dir . clone ( ) ,
2026-03-17 00:03:08 +00:00
self . config . device_path . clone ( ) ,
self . our_did . clone ( ) ,
self . our_ed_pubkey_hex . clone ( ) ,
self . our_x25519_secret ,
self . our_x25519_pubkey_hex . clone ( ) ,
2026-04-18 11:53:06 -04:00
self . server_name . clone ( ) ,
2026-06-26 09:44:23 -04:00
self . config . lora_region . clone ( ) ,
self . config . channel_name . clone ( ) ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
self . config . device_kind ,
2026-07-03 18:08:35 +00:00
self . config . reticulum_tcp . clone ( ) ,
2026-03-17 00:03:08 +00:00
shutdown_rx ,
cmd_rx ,
) ;
self . listener_handle = Some ( handle ) ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
// Spawn dead man's switch background checker
let dms = Arc ::clone ( & self . dead_man_switch ) ;
let dms_state = Arc ::clone ( & self . state ) ;
let dms_key = self . signing_key . clone ( ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let dms_shutdown = self
. shutdown_tx
. as_ref ( )
. ok_or_else ( | | anyhow ::anyhow! ( " Shutdown channel not initialized " ) ) ?
. subscribe ( ) ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
let dms_handle = tokio ::spawn ( async move {
let mut shutdown = dms_shutdown ;
let mut interval = tokio ::time ::interval ( Duration ::from_secs ( 60 ) ) ;
interval . tick ( ) . await ; // skip first immediate tick
loop {
tokio ::select! {
_ = interval . tick ( ) = > {
if dms . is_triggered ( ) . await {
let was_triggered = * dms . triggered_flag ( ) . await ;
if ! was_triggered {
error! ( " Dead man's switch TRIGGERED — broadcasting alert " ) ;
if let Ok ( wire ) = dms . build_signed_alert ( & dms_key ) . await {
for ch in [ 0 u8 , 1 ] {
2026-04-14 10:24:27 -04:00
let _ = dms_state . send_cmd (
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
listener ::MeshCommand ::BroadcastChannel {
channel : ch ,
payload : wire . clone ( ) ,
} ,
) . await ;
}
}
dms . mark_triggered ( ) . await ;
}
}
}
_ = shutdown . changed ( ) = > {
if * shutdown . borrow ( ) { return ; }
}
}
}
} ) ;
self . deadman_handle = Some ( dms_handle ) ;
2026-06-17 19:19:32 -04:00
// Scheduled-message task (issue #50, phase 1.7): fires queued messages
// when due, retrying peer DMs until the peer is back in range.
let sched = Arc ::clone ( & self . scheduler ) ;
let sched_state = Arc ::clone ( & self . state ) ;
let sched_shutdown = self
. shutdown_tx
. as_ref ( )
. ok_or_else ( | | anyhow ::anyhow! ( " Shutdown channel not initialized " ) ) ?
. subscribe ( ) ;
let sched_handle = tokio ::spawn ( async move {
scheduler ::run_scheduler ( sched , sched_state , sched_shutdown ) . await ;
} ) ;
self . scheduler_handle = Some ( sched_handle ) ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
// Spawn block header announcer (internet-connected nodes only)
if self . config . announce_block_headers {
let bha_state = Arc ::clone ( & self . state ) ;
let bha_cache = Arc ::clone ( & self . block_header_cache ) ;
let bha_key = self . signing_key . clone ( ) ;
let bha_did = self . our_did . clone ( ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let bha_shutdown = self
. shutdown_tx
. as_ref ( )
. ok_or_else ( | | anyhow ::anyhow! ( " Shutdown channel not initialized " ) ) ?
. subscribe ( ) ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
let bha_handle = tokio ::spawn ( async move {
let mut shutdown = bha_shutdown ;
let mut interval = tokio ::time ::interval ( Duration ::from_secs ( 30 ) ) ;
interval . tick ( ) . await ; // skip first
let mut last_announced_height : u64 = 0 ;
2026-06-18 08:08:52 -04:00
let mut last_announce_at : Option < std ::time ::Instant > = None ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
let client = match reqwest ::Client ::builder ( )
. timeout ( Duration ::from_secs ( 10 ) )
. build ( )
{
Ok ( c ) = > c ,
Err ( e ) = > {
error! ( " Failed to create HTTP client for block announcer: {} " , e ) ;
return ;
}
} ;
loop {
tokio ::select! {
_ = interval . tick ( ) = > {
// Poll Bitcoin Core for latest block
match bitcoin_rpc_getblockcount ( & client ) . await {
Ok ( height ) if height > last_announced_height = > {
2026-06-18 08:08:52 -04:00
// Advance the tip baseline immediately so a fast Bitcoin
// catch-up (a new block every poll) doesn't re-fire each tick.
last_announced_height = height ;
// Throttle: at most one announcement per ~9 min. Real ~10 min
// blocks still propagate, but a rapid catch-up can no longer
// flood the shared LoRa channel.
if last_announce_at
. map ( | t | t . elapsed ( ) < Duration ::from_secs ( 540 ) )
. unwrap_or ( false )
{
continue ;
}
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
if let Ok ( header ) = bitcoin_rpc_getblockheader_by_height ( & client , height ) . await {
// Store in cache
let payload = message_types ::BlockHeaderPayload {
height ,
hash : header . hash . clone ( ) ,
prev_hash : header . prev_hash . clone ( ) ,
timestamp : header . timestamp ,
announced_by : bha_did . clone ( ) ,
} ;
2026-07-02 21:02:54 -04:00
if let Err ( e ) = bha_cache . store_header ( payload ) . await {
warn! ( " Failed to persist block-header cache: {e:#} " ) ;
}
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
// Build signed announcement and broadcast
match bitcoin_relay ::build_block_header_announcement (
height ,
& header . hash ,
& header . prev_hash ,
header . timestamp ,
& bha_did ,
& bha_key ,
) {
Ok ( wire ) = > {
// Send to peers — prefer Archy nodes, fall back to all (max 5)
let peers = bha_state . peers . read ( ) . await ;
let mut sent = 0 u32 ;
let max_peers = 5 u32 ;
// First pass: Archy nodes
for peer in peers . values ( ) {
if sent > = max_peers { break ; }
if ! peer . advert_name . starts_with ( " Archy- " ) { continue ; }
if let Some ( ref pk ) = peer . pubkey_hex {
if let Ok ( pk_bytes ) = hex ::decode ( pk ) {
if pk_bytes . len ( ) > = 6 {
let mut prefix = [ 0 u8 ; 6 ] ;
prefix . copy_from_slice ( & pk_bytes [ .. 6 ] ) ;
2026-04-14 10:24:27 -04:00
let _ = bha_state . send_cmd (
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
listener ::MeshCommand ::SendRaw {
dest_pubkey_prefix : prefix ,
payload : wire . clone ( ) ,
} ,
) . await ;
sent + = 1 ;
}
}
}
}
2026-06-18 08:08:52 -04:00
// NOTE: intentionally NO fallback to arbitrary
// peers. Block headers go ONLY to known Archy
// (federated) nodes — never to random meshcore
// devices on the shared public channel.
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
drop ( peers ) ;
2026-06-18 08:08:52 -04:00
if sent > 0 {
last_announce_at = Some ( std ::time ::Instant ::now ( ) ) ;
info! ( height , hash = % header . hash , peers = sent , " Announced block header to Archy peers " ) ;
}
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
}
Err ( e ) = > warn! ( " Failed to build block announcement: {} " , e ) ,
}
}
}
Ok ( _ ) = > { } // No new block
Err ( e ) = > {
// Bitcoin not running or not reachable — that's fine, skip
tracing ::debug! ( " Block poll: {} " , e ) ;
}
}
}
_ = shutdown . changed ( ) = > {
if * shutdown . borrow ( ) { return ; }
}
}
}
} ) ;
self . block_announcer_handle = Some ( bha_handle ) ;
info! ( " Block header announcer started " ) ;
}
2026-04-14 10:24:27 -04:00
// Presence heartbeat broadcaster is DISABLED. The CBOR-encoded
// PresencePayload was rendering as garbled bytes on peers that
// didn't understand the typed envelope (e.g. the FreeMadeira
// repeater echoed it back as plaintext on channel 0), spamming
// every visible node with "Archy-…: av<61> …fstatusfonline…" every
// 120s. Re-enable only after either (a) presence moves to a
// non-broadcast path or (b) we can guarantee no plain-text-only
// receivers on the shared channel.
self . presence_handle = None ;
2026-03-17 00:03:08 +00:00
info! ( " Mesh service started " ) ;
Ok ( ( ) )
}
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
/// Stop the background listener and dead man's switch.
2026-03-17 00:03:08 +00:00
pub async fn stop ( & mut self ) {
if let Some ( tx ) = self . shutdown_tx . take ( ) {
let _ = tx . send ( true ) ;
}
if let Some ( handle ) = self . listener_handle . take ( ) {
let _ = handle . await ;
}
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
if let Some ( handle ) = self . deadman_handle . take ( ) {
handle . abort ( ) ;
let _ = handle . await ;
}
2026-06-17 19:19:32 -04:00
if let Some ( handle ) = self . scheduler_handle . take ( ) {
handle . abort ( ) ;
let _ = handle . await ;
}
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
if let Some ( handle ) = self . block_announcer_handle . take ( ) {
handle . abort ( ) ;
let _ = handle . await ;
}
2026-04-14 10:24:27 -04:00
if let Some ( handle ) = self . presence_handle . take ( ) {
handle . abort ( ) ;
let _ = handle . await ;
}
// Recreate the cmd channel so a subsequent start() has a fresh
// receiver. The listener task took ownership of the old receiver
// on its previous run and dropped it when the task ended, so
// without this swap the next start() hits "Command channel
// already consumed". Swapping the sender inside MeshState means
// every Arc holder transparently picks up the new channel.
let ( new_tx , new_rx ) = tokio ::sync ::mpsc ::channel ( 32 ) ;
* self . state . cmd_tx . write ( ) . await = new_tx ;
self . cmd_rx = Some ( new_rx ) ;
2026-03-17 00:03:08 +00:00
info! ( " Mesh service stopped " ) ;
}
/// Get current mesh status.
pub async fn status ( & self ) -> MeshStatus {
2026-06-30 23:03:09 -04:00
let mut status = self . state . status . read ( ) . await . clone ( ) ;
// The operator-configured LoRa region isn't part of the live session
// state (it's config, read once at session start) — compose it in
// here rather than threading it through the session's shared status
// writes, for the Device tab (#8) to display.
status . region = self . config . lora_region . clone ( ) ;
status
2026-03-17 00:03:08 +00:00
}
2026-04-18 11:53:06 -04:00
/// Get a reference to the shared mesh state.
pub fn state ( & self ) -> & Arc < listener ::MeshState > {
& self . state
}
2026-03-17 00:03:08 +00:00
/// Get list of discovered peers.
pub async fn peers ( & self ) -> Vec < MeshPeer > {
self . state . peers . read ( ) . await . values ( ) . cloned ( ) . collect ( )
}
/// Get message history.
pub async fn messages ( & self , limit : Option < usize > ) -> Vec < MeshMessage > {
let messages = self . state . messages . read ( ) . await ;
let limit = limit . unwrap_or ( MAX_MESSAGES_DEFAULT ) ;
// Return in chronological order (oldest first) — take last N items
let len = messages . len ( ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let skip = len . saturating_sub ( limit ) ;
2026-03-17 00:03:08 +00:00
messages . iter ( ) . skip ( skip ) . cloned ( ) . collect ( )
}
feat(mesh): MessageKey foundation and debug-dump RPC
Adds sender_pubkey + sender_seq fields to MeshMessage so received
messages carry a stable cross-transport identity: (sender_pubkey,
sender_seq) pair. This is the foundation for the upcoming reply,
reaction, edit, and read-receipt variants — they need to target a
message by an ID that is meaningful on every node, not just locally.
Receive-side population lives in dispatch.rs::store_typed_message,
which now looks up the peer's pubkey_hex and copies envelope.seq from
the decoded TypedEnvelope. Sent-side population will land when we
plumb a per-node monotonic seq counter through the RPC layer.
Also adds mesh.debug-dump: a full in-memory state snapshot returning
peers, messages, status, shared-secret peer ids, encrypt_relay flag,
and stego mode — intended for smoke tests and bug investigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:18:01 -04:00
/// Full in-memory state dump for debugging. Returns peers, all messages,
/// status, shared-secret peer ids (not the secrets), encrypt_relay flag,
/// and stego mode. Intended for development/smoke-test use only — don't
/// call this on a hot path.
pub async fn debug_dump ( & self ) -> serde_json ::Value {
let status = self . state . status . read ( ) . await . clone ( ) ;
let peers : Vec < _ > = self . state . peers . read ( ) . await . values ( ) . cloned ( ) . collect ( ) ;
let messages : Vec < _ > = self . state . messages . read ( ) . await . iter ( ) . cloned ( ) . collect ( ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let secret_peer_ids : Vec < u32 > = self
. state
. shared_secrets
. read ( )
. await
. keys ( )
. copied ( )
. collect ( ) ;
feat(mesh): MessageKey foundation and debug-dump RPC
Adds sender_pubkey + sender_seq fields to MeshMessage so received
messages carry a stable cross-transport identity: (sender_pubkey,
sender_seq) pair. This is the foundation for the upcoming reply,
reaction, edit, and read-receipt variants — they need to target a
message by an ID that is meaningful on every node, not just locally.
Receive-side population lives in dispatch.rs::store_typed_message,
which now looks up the peer's pubkey_hex and copies envelope.seq from
the decoded TypedEnvelope. Sent-side population will land when we
plumb a per-node monotonic seq counter through the RPC layer.
Also adds mesh.debug-dump: a full in-memory state snapshot returning
peers, messages, status, shared-secret peer ids, encrypt_relay flag,
and stego mode — intended for smoke tests and bug investigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:18:01 -04:00
serde_json ::json! ( {
" status " : status ,
" peers " : peers ,
" peer_count " : peers . len ( ) ,
" messages " : messages ,
" message_count " : messages . len ( ) ,
" secret_peer_ids " : secret_peer_ids ,
" encrypt_relay " : self . state . encrypt_relay ,
" stego_mode " : format ! ( " {:?} " , self . state . stego_mode ) ,
} )
}
2026-04-13 08:01:10 -04:00
/// Resolve a peer's 6-byte public-key prefix for mesh addressing.
async fn peer_dest_prefix ( & self , contact_id : u32 ) -> Result < [ u8 ; 6 ] > {
2026-03-17 00:03:08 +00:00
let peers = self . state . peers . read ( ) . await ;
let peer = peers
. get ( & contact_id )
. ok_or_else ( | | anyhow ::anyhow! ( " Peer not found " ) ) ? ;
let pubkey_hex = peer
. pubkey_hex
. as_ref ( )
. ok_or_else ( | | anyhow ::anyhow! ( " Peer has no public key " ) ) ? ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let pubkey_bytes =
hex ::decode ( pubkey_hex ) . map_err ( | _ | anyhow ::anyhow! ( " Invalid peer public key " ) ) ? ;
2026-03-17 00:03:08 +00:00
if pubkey_bytes . len ( ) < 6 {
anyhow ::bail! ( " Peer public key too short " ) ;
}
let mut dest_prefix = [ 0 u8 ; 6 ] ;
dest_prefix . copy_from_slice ( & pubkey_bytes [ .. 6 ] ) ;
2026-04-13 08:01:10 -04:00
Ok ( dest_prefix )
}
2026-03-17 00:03:08 +00:00
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Split an oversized wire payload into MC-framed base64 chunks and send
/// each via the mesh device. Matches the receive-side reassembly in
/// `mesh/listener/decode.rs::handle_chunked_frame` (header `MCIIXXTT`,
/// 20-chunk cap, 152 base64 chars per chunk). The caller must ensure
/// the peer exists and the device is connected.
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
#[ allow(dead_code) ]
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
async fn send_chunked_payload ( & self , contact_id : u32 , payload : Vec < u8 > ) -> Result < ( ) > {
use base64 ::Engine ;
const HEADER_LEN : usize = 8 ; // MC + msg_id(2) + chunk_idx(2) + total(2)
const MAX_CHUNK_B64 : usize = protocol ::MAX_MESSAGE_LEN - HEADER_LEN ;
const MAX_CHUNKS : u8 = 20 ;
let b64 = base64 ::engine ::general_purpose ::STANDARD . encode ( & payload ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let total_chunks = b64 . len ( ) . div_ceil ( MAX_CHUNK_B64 ) as u8 ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
if total_chunks = = 0 | | total_chunks > MAX_CHUNKS {
anyhow ::bail! (
" Payload too large to chunk: {} bytes → {} chunks (max {}) " ,
payload . len ( ) ,
total_chunks ,
MAX_CHUNKS
) ;
}
// Pick a 1-byte msg_id. Use the low 8 bits of the unix nanos; not
// cryptographically unique but collisions within a 120s reassembly
// window are astronomically unlikely for normal send rates.
let msg_id : u8 =
( chrono ::Utc ::now ( ) . timestamp_nanos_opt ( ) . unwrap_or ( 0 ) as u64 & 0xFF ) as u8 ;
let dest_prefix = self . peer_dest_prefix ( contact_id ) . await ? ;
for chunk_idx in 0 .. total_chunks {
let start = chunk_idx as usize * MAX_CHUNK_B64 ;
let end = ( start + MAX_CHUNK_B64 ) . min ( b64 . len ( ) ) ;
let chunk = & b64 [ start .. end ] ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let frame = format! (
" MC{:02X}{:02X}{:02X}{} " ,
msg_id , chunk_idx , total_chunks , chunk
) ;
self . state
. send_cmd ( listener ::MeshCommand ::SendText {
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
dest_pubkey_prefix : dest_prefix ,
payload : frame . into_bytes ( ) ,
} )
. await
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
}
tracing ::info! (
contact_id ,
msg_id ,
chunks = total_chunks ,
bytes = payload . len ( ) ,
" Sent chunked payload over mesh "
) ;
Ok ( ( ) )
}
2026-04-13 08:01:10 -04:00
/// Send raw wire payload bytes to a peer (no Sent-record bookkeeping).
/// Callers are responsible for storing the MeshMessage record afterwards.
2026-04-18 11:07:08 -04:00
///
/// Oversized payloads (>LoRa per-frame budget) are handled by the lower
/// `send_dm_via_channel` layer, which base64-encodes + MC-frame-chunks
/// the bytes into 80-char pieces and reassembles on the receiver. We
/// must NOT chunk here as well — doing so double-chunks and produces
/// bytes the receiver can't decode.
2026-04-13 08:01:10 -04:00
async fn send_raw_payload ( & self , contact_id : u32 , payload : Vec < u8 > ) -> Result < ( ) > {
let status = self . state . status . read ( ) . await ;
if ! status . device_connected {
anyhow ::bail! ( " No mesh device connected " ) ;
}
drop ( status ) ;
2026-03-17 00:03:08 +00:00
2026-04-13 08:01:10 -04:00
let dest_prefix = self . peer_dest_prefix ( contact_id ) . await ? ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self . state
. send_cmd ( listener ::MeshCommand ::SendText {
2026-03-17 00:03:08 +00:00
dest_pubkey_prefix : dest_prefix ,
payload ,
} )
. await
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
2026-04-13 08:01:10 -04:00
Ok ( ( ) )
}
/// Send a typed envelope wire payload and record a rich Sent MeshMessage.
/// Used by RPC handlers that transmit invoice/coordinate/alert/etc. so the
/// UI sees a proper rich Sent card instead of garbage wire-byte plaintext.
2026-04-13 13:19:30 -04:00
///
/// `sender_seq` should match the seq encoded into the TypedEnvelope by the
/// caller — the RPC handler allocates it via `next_send_seq` before
/// building the envelope and threads it through here so the local Sent
/// record carries the same MessageKey the receiver will see.
2026-04-13 08:01:10 -04:00
pub async fn send_typed_wire (
& self ,
contact_id : u32 ,
wire : Vec < u8 > ,
type_label : & str ,
display_text : & str ,
typed_payload : Option < serde_json ::Value > ,
2026-04-13 13:19:30 -04:00
sender_seq : u64 ,
2026-04-13 08:01:10 -04:00
) -> Result < MeshMessage > {
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
// Federation-synthetic contacts (high bit set) don't exist in the
// meshcore firmware contact table, so LoRa send would fail at
// `peer_dest_prefix`. Any envelope larger than the LoRa frame budget
// also needs the federation path. In both cases we look up the
// peer's onion (by archipelago pubkey first, then by DID) and POST
// over Tor; otherwise the send falls through to LoRa.
let is_federation_synthetic = contact_id & 0x8000_0000 ! = 0 ;
let exceeds_lora = wire . len ( ) > protocol ::MAX_MESSAGE_LEN ;
2026-06-19 10:09:14 -04:00
// Mesh-preferred routing with a federation fallback. A normal radio
// contact is delivered over LoRa (preferred — free, local, no internet).
// But if that contact is the same node as a federated peer — we know its
// archipelago identity (`arch_pubkey_hex`) → onion — AND it is NOT
// currently reachable over the radio (out of LoRa range, e.g. a peer on
// another continent), route the message over the federation transport
// (FIPS→Tor) instead of handing it to a radio that physically cannot
// deliver it. Reachable radio peers stay on the mesh; oversized
// envelopes (file shares etc.) always take the federation path.
2026-06-30 05:08:17 -04:00
let radio_federated_unreachable = ! is_federation_synthetic & & ! exceeds_lora & & {
let peers = self . state . peers . read ( ) . await ;
peers
. get ( & contact_id )
. map ( | p | ! p . reachable & & p . arch_pubkey_hex . is_some ( ) )
. unwrap_or ( false )
} ;
2026-06-30 06:22:45 -04:00
let mesh_only_mode = load_config ( & self . data_dir )
. await
. ok ( )
. and_then ( | cfg | cfg . mesh_only_mode )
. unwrap_or ( false ) ;
if ! mesh_only_mode
& & ( is_federation_synthetic | | exceeds_lora | | radio_federated_unreachable )
{
2026-06-17 03:24:34 -04:00
// Resolve the peer's pubkey/did. Prefer the live mesh peer table,
// but fall back to federation storage for federation-synthetic ids
// that were never seeded into `state.peers` — e.g. a radio-less
// node where the mesh device table is empty. Without this fallback
// chatting a federation contact bails "Unknown federation peer"
// even though we know its onion from nodes.json.
let from_table = {
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
let peers = self . state . peers . read ( ) . await ;
2026-06-19 10:09:14 -04:00
peers . get ( & contact_id ) . map ( | p | {
// Resolve via the archipelago IDENTITY key (not the firmware
// routing key) — that's what matches the peer's onion entry
// in nodes.json for the federation lookup below.
(
p . arch_pubkey_hex . clone ( ) . or_else ( | | p . pubkey_hex . clone ( ) ) ,
p . did . clone ( ) ,
)
} )
2026-06-17 03:24:34 -04:00
} ;
let ( peer_pubkey , peer_did ) = match from_table {
Some ( v ) = > v ,
None if is_federation_synthetic = > {
let nodes = crate ::federation ::load_nodes ( & self . data_dir )
. await
. unwrap_or_default ( ) ;
match nodes
. iter ( )
. find ( | n | federation_peer_contact_id ( & n . pubkey ) = = contact_id )
{
Some ( n ) = > ( Some ( n . pubkey . clone ( ) ) , Some ( n . did . clone ( ) ) ) ,
None = > anyhow ::bail! ( " Unknown federation peer {} " , contact_id ) ,
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
}
}
2026-06-17 03:24:34 -04:00
None = > ( None , None ) ,
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
} ;
let nodes = crate ::federation ::load_nodes ( & self . data_dir )
. await
. unwrap_or_default ( ) ;
let onion = peer_pubkey
. as_ref ( )
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
. and_then ( | pk | {
nodes
. iter ( )
. find ( | n | & n . pubkey = = pk )
. map ( | n | n . onion . clone ( ) )
} )
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
. or_else ( | | {
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
peer_did
. as_ref ( )
. and_then ( | d | nodes . iter ( ) . find ( | n | & n . did = = d ) . map ( | n | n . onion . clone ( ) ) )
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
} ) ;
if let Some ( onion ) = onion {
return self
. send_typed_wire_via_federation (
contact_id ,
& onion ,
wire ,
type_label ,
display_text ,
typed_payload ,
sender_seq ,
)
. await ;
}
2026-04-18 11:07:08 -04:00
// No federation path — fall through to send_raw_payload, which
// hands the wire to the lower DM-via-channel layer. That layer
// (`send_dm_via_channel` in listener/session.rs) handles both
// single-frame and chunked transmission internally; we must NOT
// pre-chunk here as well or the receiver sees garbage.
2026-06-30 06:22:45 -04:00
} else if mesh_only_mode
& & ( is_federation_synthetic | | exceeds_lora | | radio_federated_unreachable )
{
tracing ::info! (
contact_id ,
bytes = wire . len ( ) ,
is_federation_synthetic ,
exceeds_lora ,
radio_federated_unreachable ,
" Off-grid mode active; forcing mesh message over LoRa only "
) ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
}
2026-04-13 08:01:10 -04:00
self . send_raw_payload ( contact_id , wire ) . await ? ;
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
let device_type = self . state . status . read ( ) . await . device_type ;
let radio_transport = radio_transport_label ( device_type ) ;
2026-04-13 08:01:10 -04:00
Ok ( self
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
. record_sent_typed (
contact_id ,
type_label ,
display_text ,
typed_payload ,
sender_seq ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
Some ( radio_transport . to_string ( ) ) ,
2026-06-29 04:29:25 -04:00
// Archy↔archy typed envelopes over LoRa are identity-signed; the
// radio E2E flag (meshtastic PKI / meshcore session) isn't
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
// threaded to the send side yet, so don't over-claim E2E here —
// except Reticulum/LXMF, which is unconditionally E2E on every
// send regardless of peer/session state (see send_message).
device_type = = DeviceType ::Reticulum ,
)
. await )
}
2026-06-30 22:07:45 -04:00
/// Send an image via native LXMF `FIELD_IMAGE` instead of our own typed
/// envelope — for a stock (non-archy) peer that can't decode our CBOR
/// wire format. Caller (the RPC layer) gates this on
/// `!is_archy_peer(contact_id)`; low-level "just send the bytes" shape
/// mirroring `send_raw_payload` — does NOT record a Sent MeshMessage
/// itself, callers use `record_sent_typed` same as the typed-envelope
/// paths so the Sent card renders identically regardless of which wire
/// format actually went out.
pub async fn send_native_image (
& self ,
contact_id : u32 ,
mime : & str ,
bytes : Vec < u8 > ,
caption : Option < String > ,
) -> Result < ( ) > {
let status = self . state . status . read ( ) . await ;
if ! status . device_connected {
anyhow ::bail! ( " No mesh device connected " ) ;
}
drop ( status ) ;
let dest_prefix = self . peer_dest_prefix ( contact_id ) . await ? ;
self . state
. send_cmd ( listener ::MeshCommand ::SendNativeImage {
dest_pubkey_prefix : dest_prefix ,
mime : mime . to_string ( ) ,
bytes ,
caption ,
} )
. await
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
Ok ( ( ) )
}
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
/// Send a typed envelope over a dedicated Reticulum RNS Resource transfer
/// (`MeshCommand::SendResource`) instead of the small inline-chunk path
/// `send_typed_wire`/`send_raw_payload` uses. Callers (the `mesh.send-content-inline`
/// RPC handler) are responsible for only reaching this when the active
/// device is actually Reticulum and the payload fits the
/// `RETICULUM_RESOURCE_MAX` budget — see `mesh.transport-advice`'s
/// `"resource-mesh"` tier, the single source of truth for that decision.
/// Mirrors `send_typed_wire`'s signature/return shape so RPC call sites
/// can switch between the two paths without restructuring.
pub async fn send_content_resource (
& self ,
contact_id : u32 ,
wire : Vec < u8 > ,
type_label : & str ,
display_text : & str ,
typed_payload : Option < serde_json ::Value > ,
sender_seq : u64 ,
) -> Result < MeshMessage > {
let status = self . state . status . read ( ) . await ;
if ! status . device_connected {
anyhow ::bail! ( " No mesh device connected " ) ;
}
drop ( status ) ;
let dest_prefix = self . peer_dest_prefix ( contact_id ) . await ? ;
self . state
. send_cmd ( listener ::MeshCommand ::SendResource {
dest_pubkey_prefix : dest_prefix ,
payload : wire ,
} )
. await
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
let device_type = self . state . status . read ( ) . await . device_type ;
let radio_transport = radio_transport_label ( device_type ) ;
Ok ( self
. record_sent_typed (
contact_id ,
type_label ,
display_text ,
typed_payload ,
sender_seq ,
Some ( radio_transport . to_string ( ) ) ,
// Reticulum/LXMF is unconditionally E2E on every send — same
// reasoning as send_message's native-text path. This method
// is Reticulum-only by construction (callers gate on
// device_type before reaching it), so this is never wrong.
true ,
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
)
2026-04-13 08:01:10 -04:00
. await )
}
2026-04-13 13:37:48 -04:00
/// Send a typed envelope to a peer via the federation (Tor) path rather
/// than LoRa. Used when the envelope exceeds LoRa's per-frame budget —
/// ContentRef is the canonical example, at 400+ bytes with thumb + cap.
/// The peer's onion must already be in federation storage. Records a
/// Sent MeshMessage locally on success so the UI gets the rich card.
///
/// This does NOT use chunking and does NOT go through the mesh radio —
/// it is a straight HTTP POST over Tor to the peer's
/// `/archipelago/mesh-typed` endpoint.
2026-04-18 15:57:11 -04:00
///
/// The POST itself is fire-and-forget: we record the Sent MeshMessage
/// synchronously (so the UI sees the bubble immediately) and spawn the
/// Tor HTTP in the background. Tor circuit setup is 1– 5s per envelope
/// and blocking the RPC on it made `mesh.send` feel laggy — especially
/// over a held Enter key. Delivery failures still surface via the
/// absent read-receipt path: `delivered` stays `false` on the Sent
/// record if the peer never echoes back a receipt.
2026-04-13 13:37:48 -04:00
pub async fn send_typed_wire_via_federation (
& self ,
contact_id : u32 ,
peer_onion : & str ,
wire : Vec < u8 > ,
type_label : & str ,
display_text : & str ,
typed_payload : Option < serde_json ::Value > ,
sender_seq : u64 ,
) -> Result < MeshMessage > {
use base64 ::{ engine ::general_purpose ::STANDARD as BASE64 , Engine as _ } ;
use ed25519_dalek ::Signer ;
// Sign the raw wire bytes so the receiver can attribute the envelope
2026-04-19 01:36:04 -04:00
// to our pubkey even when it arrives over federation/FIPS/Tor rather
// than the radio. Signature covers the wire only — the receiver
// re-hashes.
2026-04-13 13:37:48 -04:00
let signature = hex ::encode ( self . signing_key . sign ( & wire ) . to_bytes ( ) ) ;
let wire_b64 = BASE64 . encode ( & wire ) ;
let body = serde_json ::json! ( {
" from_pubkey " : self . our_ed_pubkey_hex ,
" from_name " : self . our_did ,
" typed_envelope_b64 " : wire_b64 ,
" signature " : signature ,
} ) ;
2026-04-18 15:57:11 -04:00
// Record Sent now so the UI bubble appears immediately.
let msg = self
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
. record_sent_typed (
contact_id ,
type_label ,
display_text ,
typed_payload ,
sender_seq ,
2026-06-29 04:29:25 -04:00
// Transport is finalized below once the background send resolves
// FIPS vs Tor; mark E2E now — a federation envelope is
// identity-signed and rides an encrypted transport.
None ,
true ,
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
)
2026-04-18 15:57:11 -04:00
. await ;
2026-04-19 01:36:04 -04:00
// Fire the send in the background. FIPS is preferred when the peer
// is federated and running fips; Tor is the fallback. Failures are
// logged but do not propagate — caller already has the Sent
2026-04-18 15:57:11 -04:00
// MeshMessage and the UI's delivery indicator tracks the receipt.
2026-04-19 01:36:04 -04:00
let peer_onion_owned = peer_onion . to_string ( ) ;
let data_dir_owned = self . data_dir . clone ( ) ;
2026-06-29 04:29:25 -04:00
// Finalize the Sent record's transport pill once we know which leg
// (FIPS/Tor) actually delivered it.
let state_for_transport = self . state . clone ( ) ;
let sent_msg_id = msg . id ;
2026-04-18 15:57:11 -04:00
tokio ::spawn ( async move {
2026-04-19 01:36:04 -04:00
let fips_npub =
crate ::federation ::fips_npub_for_onion ( & data_dir_owned , & peer_onion_owned ) . await ;
let req = crate ::fips ::dial ::PeerRequest ::new (
fips_npub . as_deref ( ) ,
& peer_onion_owned ,
" /archipelago/mesh-typed " ,
)
2026-04-19 01:44:41 -04:00
. service ( crate ::settings ::transport ::PeerService ::Messaging )
2026-06-19 10:09:14 -04:00
. timeout ( std ::time ::Duration ::from_secs ( 120 ) )
// Fast-fail a FIPS path the peer isn't reachable on (the common case
// for remote/Tailscale peers that share no FIPS overlay with us) so
// the Tor fallback delivers the message in ~3-5s instead of the send
// hanging on FIPS. FIPS-reachable peers connect in <1s and still use
// it; only an unreachable FIPS path is short-circuited. Matches the
// federation-sync fix. 8s ≈ the FIPS connect_timeout headroom.
. fips_timeout ( std ::time ::Duration ::from_secs ( 8 ) ) ;
2026-04-19 01:36:04 -04:00
match req . send_json ( & body ) . await {
Ok ( ( resp , transport ) ) if resp . status ( ) . is_success ( ) = > {
tracing ::debug! ( contact_id , transport = % transport , " Federation envelope delivered " ) ;
2026-06-29 04:29:25 -04:00
// Tag the Sent bubble with the leg that delivered it (the
// transport pill: "fips" / "tor").
let mut messages = state_for_transport . messages . write ( ) . await ;
if let Some ( m ) = messages . iter_mut ( ) . find ( | m | m . id = = sent_msg_id ) {
m . transport = Some ( transport . to_string ( ) ) ;
}
2026-04-18 15:57:11 -04:00
}
2026-04-19 01:36:04 -04:00
Ok ( ( resp , transport ) ) = > warn! (
2026-04-18 15:57:11 -04:00
contact_id ,
status = % resp . status ( ) ,
2026-04-19 01:36:04 -04:00
transport = % transport ,
2026-04-18 15:57:11 -04:00
" Peer rejected federation-routed envelope "
) ,
Err ( e ) = > warn! ( contact_id , " Federation POST failed: {} " , e ) ,
}
} ) ;
Ok ( msg )
2026-04-13 13:37:48 -04:00
}
/// Inject a typed envelope received over federation (Tor) into MeshState
/// as if it had arrived over the mesh radio. Looks up contact_id by
/// matching pubkey_hex against known mesh peers; falls back to a
/// synthetic id derived from the pubkey bytes so the UI can still
/// address the chat (will render as a new "peer" if not in the list).
pub async fn inject_typed_from_federation (
& self ,
from_pubkey_hex : & str ,
from_name : Option < & str > ,
wire : Vec < u8 > ,
) -> Result < ( ) > {
let envelope = crate ::mesh ::message_types ::TypedEnvelope ::from_wire ( & wire ) ? ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
// Federation and mesh have disjoint identity namespaces: a LoRa
// mesh contact carries meshcore's firmware-issued pubkey, not the
// archipelago ed25519 key. So we cannot rely on matching pubkeys
// across transports. Instead, every federation peer has a stable
// synthetic mesh contact_id derived from its archipelago pubkey,
// and is upserted into mesh state the first time we hear from them.
let ( federation_did , federation_name ) = {
2026-04-13 14:13:36 -04:00
let nodes = crate ::federation ::load_nodes ( & self . data_dir )
. await
. unwrap_or_default ( ) ;
nodes
. into_iter ( )
. find ( | n | n . pubkey = = from_pubkey_hex )
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
. map ( | n | ( Some ( n . did ) , n . name ) )
. unwrap_or ( ( None , None ) )
2026-04-13 13:37:48 -04:00
} ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
// If the federation list knows this sender, use its DID; otherwise
// fall back to `from_name` (the sender's self-reported DID from the
// envelope body, signature-verified upstream in handle_mesh_typed_relay).
let effective_did = federation_did
. or_else ( | | from_name . map ( | s | s . to_string ( ) ) )
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
. unwrap_or_else ( | | {
format! (
" did:unknown:{} " ,
& from_pubkey_hex [ .. from_pubkey_hex . len ( ) . min ( 16 ) ]
)
} ) ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
let display_name = federation_name
. clone ( )
. or_else ( | | from_name . map ( | s | s . to_string ( ) ) )
. unwrap_or_else ( | | " federation peer " . to_string ( ) ) ;
let contact_id = upsert_federation_peer (
& self . state ,
from_pubkey_hex ,
& effective_did ,
Some ( & display_name ) ,
)
. await ;
2026-06-29 04:29:25 -04:00
// The inbound HTTP gives no FIPS-vs-Tor signal, so label the message
// with the leg most recently used with this peer (federation storage's
// `last_transport`), defaulting to Tor. Federation envelopes are E2E
// (identity-signed over an encrypted transport).
let transport_label = {
let nodes = crate ::federation ::load_nodes ( & self . data_dir )
. await
. unwrap_or_default ( ) ;
nodes
. iter ( )
. find ( | n | n . pubkey = = from_pubkey_hex )
. and_then ( | n | n . last_transport . clone ( ) )
. filter ( | t | t = = " fips " | | t = = " tor " )
. unwrap_or_else ( | | " tor " . to_string ( ) )
} ;
let before = listener ::dispatch ::max_message_id ( & self . state ) . await ;
2026-04-13 13:37:48 -04:00
listener ::dispatch ::handle_typed_envelope_direct (
& self . state ,
contact_id ,
& display_name ,
envelope ,
)
. await ;
2026-06-29 04:29:25 -04:00
listener ::dispatch ::stamp_received_transport (
& self . state ,
contact_id ,
before ,
& transport_label ,
true ,
)
. await ;
2026-04-13 13:37:48 -04:00
Ok ( ( ) )
}
2026-04-13 13:19:30 -04:00
/// Allocate the next outbound seq for a target. Convenience passthrough
/// to MeshState::next_send_seq; used by RPC handlers before encoding a
/// TypedEnvelope so the seq on the wire matches the Sent record.
pub async fn next_send_seq ( & self , target : u32 ) -> u64 {
self . state . next_send_seq ( target ) . await
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Look up a stored MeshMessage by its local `id`. Used by the Forward
/// RPC to pull an existing record's typed payload for re-encoding.
pub async fn find_message_by_id ( & self , id : u64 ) -> Option < MeshMessage > {
let messages = self . state . messages . read ( ) . await ;
messages . iter ( ) . find ( | m | m . id = = id ) . cloned ( )
}
2026-04-14 10:24:27 -04:00
/// Drop a stored MeshMessage by local id. Used after sending control
/// envelopes (read receipts) so they don't surface as their own
/// bubbles in the chat history. The wire frame is already on its way;
/// this just prunes the local Sent record.
pub async fn drop_message_by_id ( & self , id : u64 ) {
let mut messages = self . state . messages . write ( ) . await ;
messages . retain ( | m | m . id ! = id ) ;
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Apply an Edit locally to any own-Sent message matching `sender_seq`
/// (sender_pubkey is implicit = self). Rewrites `plaintext` and appends
/// an `edited_at` marker on `typed_payload` so the UI can show "(edited)".
/// Best-effort: missing target is silently ignored.
pub async fn apply_local_edit ( & self , target_seq : u64 , new_text : & str , edited_at : u32 ) {
let mut messages = self . state . messages . write ( ) . await ;
for m in messages . iter_mut ( ) {
if m . sender_seq = = Some ( target_seq )
& & matches! ( m . direction , crate ::mesh ::types ::MessageDirection ::Sent )
{
m . plaintext = new_text . to_string ( ) ;
let mut obj = match m . typed_payload . take ( ) {
Some ( serde_json ::Value ::Object ( o ) ) = > o ,
_ = > serde_json ::Map ::new ( ) ,
} ;
obj . insert ( " edited_at " . to_string ( ) , serde_json ::json! ( edited_at ) ) ;
obj . insert ( " text " . to_string ( ) , serde_json ::json! ( new_text ) ) ;
m . typed_payload = Some ( serde_json ::Value ::Object ( obj ) ) ;
break ;
}
}
}
/// Apply a Delete tombstone locally to an own-Sent message.
pub async fn apply_local_delete ( & self , target_seq : u64 ) {
let mut messages = self . state . messages . write ( ) . await ;
for m in messages . iter_mut ( ) {
if m . sender_seq = = Some ( target_seq )
& & matches! ( m . direction , crate ::mesh ::types ::MessageDirection ::Sent )
{
m . plaintext = " 🗑 message deleted " . to_string ( ) ;
m . typed_payload = Some ( serde_json ::json! ( { " deleted " : true } ) ) ;
m . message_type = " delete " . to_string ( ) ;
break ;
}
}
}
2026-04-13 08:01:10 -04:00
/// Broadcast a typed envelope wire payload on a mesh channel and record a
/// rich Sent MeshMessage. Bytes are sent directly — do NOT utf8_lossy-encode
/// binary envelope bytes before handing them here.
pub async fn send_channel_typed_wire (
& self ,
channel : u8 ,
wire : Vec < u8 > ,
type_label : & str ,
display_text : & str ,
typed_payload : Option < serde_json ::Value > ,
2026-04-13 13:19:30 -04:00
sender_seq : u64 ,
2026-04-13 08:01:10 -04:00
) -> Result < MeshMessage > {
let status = self . state . status . read ( ) . await ;
if ! status . device_connected {
anyhow ::bail! ( " No mesh device connected " ) ;
}
drop ( status ) ;
if wire . len ( ) > protocol ::MAX_MESSAGE_LEN {
anyhow ::bail! (
" Message too large for LoRa: {} bytes (max {}) " ,
wire . len ( ) ,
protocol ::MAX_MESSAGE_LEN
) ;
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self . state
. send_cmd ( listener ::MeshCommand ::BroadcastChannel {
2026-04-13 08:01:10 -04:00
channel ,
payload : wire ,
} )
. await
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
let chan_contact_id = u32 ::MAX - ( channel as u32 ) ;
let chan_name = format! ( " Channel {} " , channel ) ;
let msg_id = self . state . next_id ( ) . await ;
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
let radio_transport = radio_transport_label ( self . state . status . read ( ) . await . device_type ) ;
2026-04-13 08:01:10 -04:00
let msg = MeshMessage {
id : msg_id ,
direction : MessageDirection ::Sent ,
peer_contact_id : chan_contact_id ,
peer_name : Some ( chan_name ) ,
plaintext : display_text . to_string ( ) ,
timestamp : chrono ::Utc ::now ( ) . to_rfc3339 ( ) ,
delivered : false ,
2026-06-29 04:29:25 -04:00
// Channel broadcasts use the shared channel PSK, not per-identity
// E2E — so not an E2E message, but it does travel over the radio.
2026-04-13 08:01:10 -04:00
encrypted : false ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
transport : Some ( radio_transport . to_string ( ) ) ,
2026-04-13 08:01:10 -04:00
message_type : type_label . to_string ( ) ,
typed_payload ,
2026-04-13 13:19:30 -04:00
sender_pubkey : Some ( self . our_ed_pubkey_hex . clone ( ) ) ,
sender_seq : Some ( sender_seq ) ,
2026-04-13 08:01:10 -04:00
} ;
self . state . store_message ( msg . clone ( ) ) . await ;
{
let mut status = self . state . status . write ( ) . await ;
status . messages_sent + = 1 ;
}
Ok ( msg )
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Send a text message to a peer. Wraps the text in a typed `Text`
/// envelope (variant 0) with an allocated `sender_seq`, so the resulting
/// MeshMessage carries a stable MessageKey — this is what makes replies
/// and reactions addressable against plain text bubbles.
2026-04-13 08:01:10 -04:00
pub async fn send_message ( & self , contact_id : u32 , text : & str ) -> Result < MeshMessage > {
2026-06-30 12:44:31 -04:00
use crate ::mesh ::message_types ::{ MeshMessageType , TypedEnvelope } ;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs
Adds every remaining wire variant and RPC needed to finish the Telegram-quality
mesh plan in a single pass:
* Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence,
21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was
enum-only, now wired through from_u8/label/from_label).
* MessageType::from_label() as the inverse of label() — used by the Forward
path to re-encode a stored typed body back through its original variant.
* RPCs: mesh.send-psbt (variant 3 was previously enum-only),
mesh.send-read-receipt, mesh.forward-message, mesh.edit-message,
mesh.delete-message, mesh.broadcast-presence, mesh.presence-list,
mesh.contacts-list, mesh.contacts-save, mesh.contacts-block,
mesh.send-channel-invite, conversations.list, conversations.messages.
* MeshState gains presence (pubkey → status+timestamps) and contacts
(pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores.
* MeshService gains find_message_by_id (Forward lookup), apply_local_edit /
apply_local_delete (optimistic local echo), and send_chunked_payload — an
MC-framed base64 splitter that fires as a fallback inside send_typed_wire
when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the
existing receive-side reassembly in listener/decode.rs.
* Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt
(rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward,
Edit, Delete. Edit/Delete guard against cross-peer tampering by matching
the target MessageKey pubkey against the sender's advertised pubkey_hex.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
let seq = self . state . next_send_seq ( contact_id ) . await ;
2026-06-30 12:44:31 -04:00
let device_type = self . state . status . read ( ) . await . device_type ;
let archy = self . is_archy_peer ( contact_id ) . await ;
// Transport choice is DEVICE-AWARE so we fix Meshtastic without regressing
// Meshcore:
// • Meshtastic (any peer) → plain text native DM on TEXT_MESSAGE_APP. The
// firmware end-to-end (PKC/Curve25519) encrypts a directed DM to any
// peer whose public key it knows (archy peers exchange them via
// NodeInfo), so it's delivered E2E and shows as chat on every client.
// Meshtastic firmware 2.7.x will NOT deliver our opaque binary typed
// envelope as a message (PRIVATE_APP is opaque app-data; a base64
// envelope overflows one LoRa frame and chunk-fails) — wrapping text
// is exactly what silently broke archy↔archy Meshtastic LoRa.
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
// • Meshcore/Reticulum archy peer → keep the rich signed typed envelope.
// Meshcore frames are binary-safe (no UTF-8 mangling) and Reticulum/LXMF
// is binary-safe and high-capacity too; both carry their own transport
// E2E plus our signature for `!ai` auth / seq reply addressing, so the
// envelope works there and we must not drop it.
2026-06-30 12:44:31 -04:00
// • Meshcore stock client → plain text (can't decode our envelope).
// Rich typed messages (invoice/coordinate/reaction/…) always use the
// typed-wire path via `send_typed_wire`; only plain Text is routed here.
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
let use_typed_envelope =
archy & & matches! ( device_type , DeviceType ::Meshcore | DeviceType ::Reticulum ) ;
2026-06-30 12:44:31 -04:00
if use_typed_envelope {
// Sign with our archipelago identity so the receiver can authenticate
// us over LoRa (verifies against our bound `arch_pubkey_hex`). `with_seq`
// is applied after signing — seq is not covered by the signature.
let envelope = TypedEnvelope ::new_signed (
MeshMessageType ::Text ,
text . as_bytes ( ) . to_vec ( ) ,
& self . signing_key ,
)
. with_seq ( seq ) ;
let wire = envelope . to_wire ( ) ? ;
return self
. send_typed_wire ( contact_id , wire , " text " , text , None , seq )
. await ;
}
2026-06-30 10:39:34 -04:00
let dest_prefix = self . peer_dest_prefix ( contact_id ) . await ? ;
self . state
. send_cmd ( listener ::MeshCommand ::SendNativeText {
dest_pubkey_prefix : dest_prefix ,
payload : text . as_bytes ( ) . to_vec ( ) ,
} )
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
. await
2026-06-30 10:39:34 -04:00
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
// The firmware PKI-encrypts a directed DM to any peer whose key it knows;
// archy peers always exchange keys, so mark those Sent rows E2E so the
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
// pill shows immediately. A non-archy stock peer (e.g. 3ccc) can also be
// PKC-capable once we've learned its NodeInfo public key — OR that in too
// so the pill isn't archy-only. (The receiver independently stamps E2E
// from the radio's `pki_encrypted` flag, so an inbound row is accurate
// regardless.)
//
// Reticulum/LXMF has no such conditional: every send is encrypted to the
// destination's identity key by the LXMF router itself, archy peer or
// not — so it's unconditionally E2E rather than gated on `archy`/`pkc_capable`
// (which is a Meshtastic-only concept; Reticulum contacts never set it).
let pkc_capable = self . peer_pkc_capable ( contact_id ) . await ;
let encrypted = device_type = = DeviceType ::Reticulum | | archy | | pkc_capable ;
2026-06-30 10:39:34 -04:00
Ok ( self
. record_sent_typed (
contact_id ,
" text " ,
text ,
None ,
seq ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
Some ( radio_transport_label ( device_type ) . to_string ( ) ) ,
encrypted ,
2026-06-30 10:39:34 -04:00
)
. await )
2026-03-17 00:03:08 +00:00
}
2026-06-18 08:08:52 -04:00
/// Whether `contact_id` is an archipelago peer (vs a stock meshcore client).
/// Federation-synthetic ids are always archy; radio contacts count as archy
/// only once we've learned their archipelago identity (DID or x25519 key,
/// from federation seeding or an identity exchange). Stock clients have
/// neither, so we send them plain text rather than typed envelopes.
2026-06-30 22:07:45 -04:00
pub ( crate ) async fn is_archy_peer ( & self , contact_id : u32 ) -> bool {
2026-06-18 08:08:52 -04:00
if contact_id & 0x8000_0000 ! = 0 {
return true ;
}
let peers = self . state . peers . read ( ) . await ;
peers
. get ( & contact_id )
. map ( | p | p . did . is_some ( ) | | p . x25519_pubkey . is_some ( ) )
. unwrap_or ( false )
}
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
/// Whether `contact_id`'s real radio PKI (Curve25519) key is known, so the
/// firmware delivers a directed DM to it end-to-end encrypted even though
/// it's not an archipelago peer (e.g. stock Meshtastic peer 3ccc). Stamped
/// onto `MeshPeer::pkc_capable` by `refresh_contacts` from the driver's
/// `get_contacts()`.
async fn peer_pkc_capable ( & self , contact_id : u32 ) -> bool {
self . state
. peers
. read ( )
. await
. get ( & contact_id )
. map ( | p | p . pkc_capable )
. unwrap_or ( false )
}
2026-04-13 08:01:10 -04:00
/// Record a Sent MeshMessage for a typed envelope that has already been
/// transmitted by the caller. Used by the RPC layer after sending
/// invoice/coordinate/alert/etc. so the UI gets a proper rich Sent card
/// instead of a Sent record containing the raw wire bytes as plaintext.
pub async fn record_sent_typed (
& self ,
contact_id : u32 ,
type_label : & str ,
display_text : & str ,
typed_payload : Option < serde_json ::Value > ,
2026-04-13 13:19:30 -04:00
sender_seq : u64 ,
2026-06-29 04:29:25 -04:00
transport : Option < String > ,
encrypted : bool ,
2026-04-13 08:01:10 -04:00
) -> MeshMessage {
let msg_id = self . state . next_id ( ) . await ;
let peer_name = self
. state
. peers
. read ( )
. await
. get ( & contact_id )
. map ( | p | p . advert_name . clone ( ) ) ;
let msg = MeshMessage {
id : msg_id ,
direction : MessageDirection ::Sent ,
peer_contact_id : contact_id ,
peer_name ,
plaintext : display_text . to_string ( ) ,
timestamp : chrono ::Utc ::now ( ) . to_rfc3339 ( ) ,
delivered : false ,
2026-06-29 04:29:25 -04:00
encrypted ,
transport ,
2026-04-13 08:01:10 -04:00
message_type : type_label . to_string ( ) ,
typed_payload ,
2026-04-13 13:19:30 -04:00
sender_pubkey : Some ( self . our_ed_pubkey_hex . clone ( ) ) ,
sender_seq : Some ( sender_seq ) ,
2026-04-13 08:01:10 -04:00
} ;
self . state . store_message ( msg . clone ( ) ) . await ;
{
let mut status = self . state . status . write ( ) . await ;
status . messages_sent + = 1 ;
}
msg
}
2026-04-12 12:11:00 -04:00
/// Send a message to a mesh channel (broadcast).
/// Routes through the background listener which owns the serial port.
pub async fn send_channel_message ( & self , channel : u8 , text : & str ) -> Result < MeshMessage > {
let status = self . state . status . read ( ) . await ;
if ! status . device_connected {
anyhow ::bail! ( " No mesh device connected " ) ;
}
drop ( status ) ;
let payload = text . as_bytes ( ) . to_vec ( ) ;
if payload . len ( ) > protocol ::MAX_MESSAGE_LEN {
anyhow ::bail! (
" Message too large for LoRa: {} bytes (max {}) " ,
payload . len ( ) ,
protocol ::MAX_MESSAGE_LEN
) ;
}
// Send through the listener's command channel
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self . state
. send_cmd ( listener ::MeshCommand ::BroadcastChannel { channel , payload } )
2026-04-12 12:11:00 -04:00
. await
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
let chan_contact_id = u32 ::MAX - ( channel as u32 ) ;
let chan_name = format! ( " Channel {} " , channel ) ;
let msg_id = self . state . next_id ( ) . await ;
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
let radio_transport = radio_transport_label ( self . state . status . read ( ) . await . device_type ) ;
2026-04-12 12:11:00 -04:00
let msg = MeshMessage {
id : msg_id ,
direction : MessageDirection ::Sent ,
peer_contact_id : chan_contact_id ,
peer_name : Some ( chan_name ) ,
plaintext : text . to_string ( ) ,
timestamp : chrono ::Utc ::now ( ) . to_rfc3339 ( ) ,
delivered : false ,
2026-06-29 04:29:25 -04:00
// Plain channel broadcast over the radio (shared PSK, not E2E).
2026-04-12 12:11:00 -04:00
encrypted : false ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
transport : Some ( radio_transport . to_string ( ) ) ,
2026-04-13 08:01:10 -04:00
message_type : " text " . to_string ( ) ,
typed_payload : None ,
feat(mesh): MessageKey foundation and debug-dump RPC
Adds sender_pubkey + sender_seq fields to MeshMessage so received
messages carry a stable cross-transport identity: (sender_pubkey,
sender_seq) pair. This is the foundation for the upcoming reply,
reaction, edit, and read-receipt variants — they need to target a
message by an ID that is meaningful on every node, not just locally.
Receive-side population lives in dispatch.rs::store_typed_message,
which now looks up the peer's pubkey_hex and copies envelope.seq from
the decoded TypedEnvelope. Sent-side population will land when we
plumb a per-node monotonic seq counter through the RPC layer.
Also adds mesh.debug-dump: a full in-memory state snapshot returning
peers, messages, status, shared-secret peer ids, encrypt_relay flag,
and stego mode — intended for smoke tests and bug investigation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:18:01 -04:00
sender_pubkey : None ,
sender_seq : None ,
2026-04-12 12:11:00 -04:00
} ;
self . state . store_message ( msg . clone ( ) ) . await ;
{
let mut status = self . state . status . write ( ) . await ;
status . messages_sent + = 1 ;
}
Ok ( msg )
}
2026-03-17 00:03:08 +00:00
/// Broadcast our advertisement over mesh so other nodes can discover us.
/// Sends an immediate advert via the listener's command channel.
pub async fn broadcast_identity ( & self ) -> Result < ( ) > {
let status = self . state . status . read ( ) . await ;
if ! status . device_connected {
anyhow ::bail! ( " No mesh device connected. Check USB connection. " ) ;
}
drop ( status ) ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self . state
. send_cmd ( listener ::MeshCommand ::SendAdvert )
2026-03-17 00:03:08 +00:00
. await
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
info! ( " Mesh self-advert broadcast triggered " ) ;
Ok ( ( ) )
}
2026-06-30 10:39:34 -04:00
/// Reboot the locally-connected radio firmware to recover a wedged /
/// RX-deaf radio (one that has stopped hearing the mesh while still able to
/// transmit). The device reconnects via the listener's reboot→reconnect
/// loop. `seconds` is the firmware reboot delay.
pub async fn reboot_radio ( & self , seconds : i64 ) -> Result < ( ) > {
let status = self . state . status . read ( ) . await ;
if ! status . device_connected {
anyhow ::bail! ( " No mesh device connected. Check USB connection. " ) ;
}
drop ( status ) ;
self . state
. send_cmd ( listener ::MeshCommand ::RebootRadio { seconds } )
. await
. map_err ( | _ | anyhow ::anyhow! ( " Mesh listener not running " ) ) ? ;
info! ( seconds , " Mesh radio reboot triggered " ) ;
Ok ( ( ) )
}
2026-06-17 18:29:36 -04:00
/// Current mesh-AI assistant settings (issue #50).
pub async fn assistant_config ( & self ) -> listener ::AssistantConfig {
self . state . assistant . read ( ) . await . clone ( )
}
2026-06-19 05:03:18 -04:00
/// Recently-denied `!ai` askers (newest first) so the UI can offer to allow
/// them. Cleared implicitly as new denials rotate older ones out.
pub async fn assistant_denied_askers ( & self ) -> Vec < listener ::DeniedAsker > {
2026-06-30 05:08:17 -04:00
self . state
. assist_denied
. read ( )
. await
. iter ( )
. cloned ( )
. collect ( )
2026-06-19 05:03:18 -04:00
}
2026-06-17 18:29:36 -04:00
/// Update the mesh-AI assistant settings live (no listener restart) and
/// persist them to the mesh config. `model: Some(None)` clears the override
/// (falls back to the built-in default); `None` leaves a field unchanged.
pub async fn configure_assistant (
& self ,
enabled : Option < bool > ,
model : Option < Option < String > > ,
trusted_only : Option < bool > ,
2026-06-17 19:19:32 -04:00
backend : Option < String > ,
2026-06-18 03:33:37 -04:00
allowed_contacts : Option < Vec < String > > ,
2026-06-17 18:29:36 -04:00
) -> Result < ( ) > {
{
let mut a = self . state . assistant . write ( ) . await ;
if let Some ( e ) = enabled {
a . enabled = e ;
}
if let Some ( m ) = model {
a . model = m ;
}
if let Some ( t ) = trusted_only {
a . trusted_only = t ;
}
2026-06-17 19:19:32 -04:00
if let Some ( b ) = backend {
a . backend = b ;
}
2026-06-18 03:33:37 -04:00
if let Some ( list ) = allowed_contacts {
a . allowed_contacts = list ;
}
2026-06-17 18:29:36 -04:00
}
// Persist by updating the on-disk config (the in-memory `self.config`
// snapshot stays as-is; the live `state.assistant` is the runtime
// source of truth and is re-seeded from disk on the next start).
let mut cfg = load_config ( & self . data_dir ) . await . unwrap_or_default ( ) ;
{
let a = self . state . assistant . read ( ) . await ;
cfg . assistant_enabled = a . enabled ;
cfg . assistant_model = a . model . clone ( ) ;
cfg . assistant_trusted_only = a . trusted_only ;
2026-06-17 19:19:32 -04:00
cfg . assistant_backend = a . backend . clone ( ) ;
2026-06-18 03:33:37 -04:00
cfg . assistant_allowed_contacts = a . allowed_contacts . clone ( ) ;
2026-06-17 18:29:36 -04:00
}
save_config ( & self . data_dir , & cfg ) . await ? ;
Ok ( ( ) )
}
2026-03-17 00:03:08 +00:00
/// Update mesh configuration.
pub async fn configure ( & mut self , config : MeshConfig ) -> Result < ( ) > {
save_config ( & self . data_dir , & config ) . await ? ;
let was_enabled = self . config . enabled ;
self . config = config . clone ( ) ;
// Update the status to reflect new config
{
let mut status = self . state . status . write ( ) . await ;
status . enabled = config . enabled ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
status . channel_name = config
. channel_name
. clone ( )
. unwrap_or_else ( | | " archipelago " . to_string ( ) ) ;
2026-03-17 00:03:08 +00:00
}
// If enabled state changed, start/stop the listener
if config . enabled & & ! was_enabled {
self . start ( ) ? ;
} else if ! config . enabled & & was_enabled {
self . stop ( ) . await ;
// Clear connected state
let mut status = self . state . status . write ( ) . await ;
status . device_connected = false ;
status . device_path = None ;
status . firmware_version = None ;
status . self_node_id = None ;
status . peer_count = 0 ;
}
Ok ( ( ) )
}
/// Get a reference to shared state (for RPC handlers).
pub fn shared_state ( & self ) -> Arc < MeshState > {
Arc ::clone ( & self . state )
}
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
/// Record user activity (resets dead man's switch timer).
pub async fn dead_man_check_in ( & self ) {
self . dead_man_switch . check_in ( ) . await ;
}
/// Get our DID.
pub fn our_did ( & self ) -> & str {
& self . our_did
}
2026-03-17 00:03:08 +00:00
}
const MAX_MESSAGES_DEFAULT : usize = 100 ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
// ─── Bitcoin RPC helpers for block header announcer ────────────────────
#[ derive(serde::Deserialize) ]
struct BitcoinRpcResponse < T > {
result : Option < T > ,
error : Option < serde_json ::Value > ,
}
struct BlockHeaderInfo {
hash : String ,
prev_hash : String ,
timestamp : u32 ,
}
async fn bitcoin_rpc_getblockcount ( client : & reqwest ::Client ) -> Result < u64 > {
feat: Phase 1 — per-installation credential generation, eliminate hardcoded passwords
Generate unique random passwords at first boot for Bitcoin RPC, all database
services (mempool, btcpay, immich, penpot, mysql-root), and Fedimint gateway.
Credentials stored in /var/lib/archipelago/secrets/ with 600 permissions.
Scripts: first-boot-containers.sh, deploy-to-target.sh, deploy-bitcoin-knots.sh,
container-doctor.sh all read from secrets files instead of hardcoded values.
Rust backend: new bitcoin_rpc module reads password from secrets file, env var,
or dev fallback. All .basic_auth() calls and container config strings now use
the shared credential reader instead of hardcoded "archipelago123".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:39:52 +00:00
let ( rpc_user , rpc_pass ) = crate ::bitcoin_rpc ::bitcoin_rpc_credentials ( ) . await ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
let body = serde_json ::json! ( {
" jsonrpc " : " 1.0 " , " id " : " mesh " , " method " : " getblockcount " , " params " : [ ]
} ) ;
2026-03-21 01:46:40 +00:00
let resp : BitcoinRpcResponse < u64 > = tokio ::time ::timeout (
Duration ::from_secs ( 10 ) ,
client
2026-03-21 01:54:35 +00:00
. post ( crate ::constants ::BITCOIN_RPC_URL )
2026-03-21 01:46:40 +00:00
. basic_auth ( & rpc_user , Some ( & rpc_pass ) )
. json ( & body )
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
. send ( ) ,
2026-03-21 01:46:40 +00:00
)
. await
. map_err ( | _ | anyhow ::anyhow! ( " Bitcoin RPC getblockcount timed out after 10s " ) ) ?
. map_err ( | e | anyhow ::anyhow! ( " Bitcoin RPC send failed: {} " , e ) ) ?
. json ( )
. await
. map_err ( | e | anyhow ::anyhow! ( " Bitcoin RPC parse failed: {} " , e ) ) ? ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
if let Some ( err ) = resp . error {
anyhow ::bail! ( " Bitcoin RPC: {} " , err ) ;
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
resp . result
. ok_or_else ( | | anyhow ::anyhow! ( " Bitcoin RPC null result " ) )
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
}
async fn bitcoin_rpc_getblockheader_by_height (
client : & reqwest ::Client ,
height : u64 ,
) -> Result < BlockHeaderInfo > {
feat: Phase 1 — per-installation credential generation, eliminate hardcoded passwords
Generate unique random passwords at first boot for Bitcoin RPC, all database
services (mempool, btcpay, immich, penpot, mysql-root), and Fedimint gateway.
Credentials stored in /var/lib/archipelago/secrets/ with 600 permissions.
Scripts: first-boot-containers.sh, deploy-to-target.sh, deploy-bitcoin-knots.sh,
container-doctor.sh all read from secrets files instead of hardcoded values.
Rust backend: new bitcoin_rpc module reads password from secrets file, env var,
or dev fallback. All .basic_auth() calls and container config strings now use
the shared credential reader instead of hardcoded "archipelago123".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 00:39:52 +00:00
let ( rpc_user , rpc_pass ) = crate ::bitcoin_rpc ::bitcoin_rpc_credentials ( ) . await ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
// First get block hash for this height
let body = serde_json ::json! ( {
" jsonrpc " : " 1.0 " , " id " : " mesh " , " method " : " getblockhash " , " params " : [ height ]
} ) ;
2026-03-21 01:46:40 +00:00
let resp : BitcoinRpcResponse < String > = tokio ::time ::timeout (
Duration ::from_secs ( 10 ) ,
client
2026-03-21 01:54:35 +00:00
. post ( crate ::constants ::BITCOIN_RPC_URL )
2026-03-21 01:46:40 +00:00
. basic_auth ( & rpc_user , Some ( & rpc_pass ) )
. json ( & body )
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
. send ( ) ,
2026-03-21 01:46:40 +00:00
)
. await
. map_err ( | _ | anyhow ::anyhow! ( " Bitcoin RPC getblockhash timed out after 10s " ) ) ?
. map_err ( | e | anyhow ::anyhow! ( " Bitcoin RPC getblockhash send failed: {} " , e ) ) ?
. json ( )
. await
. map_err ( | e | anyhow ::anyhow! ( " Bitcoin RPC getblockhash parse failed: {} " , e ) ) ? ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let hash = resp
. result
. ok_or_else ( | | anyhow ::anyhow! ( " No block hash " ) ) ? ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
// Then get full header
let body = serde_json ::json! ( {
" jsonrpc " : " 1.0 " , " id " : " mesh " , " method " : " getblockheader " , " params " : [ hash , true ]
} ) ;
2026-03-21 01:46:40 +00:00
let resp : BitcoinRpcResponse < serde_json ::Value > = tokio ::time ::timeout (
Duration ::from_secs ( 10 ) ,
client
2026-03-21 01:54:35 +00:00
. post ( crate ::constants ::BITCOIN_RPC_URL )
2026-03-21 01:46:40 +00:00
. basic_auth ( & rpc_user , Some ( & rpc_pass ) )
. json ( & body )
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
. send ( ) ,
2026-03-21 01:46:40 +00:00
)
. await
. map_err ( | _ | anyhow ::anyhow! ( " Bitcoin RPC getblockheader timed out after 10s " ) ) ?
. map_err ( | e | anyhow ::anyhow! ( " Bitcoin RPC getblockheader send failed: {} " , e ) ) ?
. json ( )
. await
. map_err ( | e | anyhow ::anyhow! ( " Bitcoin RPC getblockheader parse failed: {} " , e ) ) ? ;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let header = resp
. result
. ok_or_else ( | | anyhow ::anyhow! ( " No block header " ) ) ? ;
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
Ok ( BlockHeaderInfo {
hash : header [ " hash " ] . as_str ( ) . unwrap_or_default ( ) . to_string ( ) ,
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
prev_hash : header [ " previousblockhash " ]
. as_str ( )
. unwrap_or_default ( )
. to_string ( ) ,
feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:51:56 +00:00
timestamp : header [ " time " ] . as_u64 ( ) . unwrap_or ( 0 ) as u32 ,
} )
}
2026-03-17 00:03:08 +00:00
#[ cfg(test) ]
mod tests {
use super ::* ;
2026-06-20 08:01:14 -04:00
fn mk_peer ( contact_id : u32 , name : & str , arch : Option < & str > , reachable : bool ) -> MeshPeer {
MeshPeer {
contact_id ,
advert_name : name . to_string ( ) ,
did : None ,
pubkey_hex : Some ( format! ( " fw {contact_id} " ) ) ,
arch_pubkey_hex : arch . map ( | s | s . to_string ( ) ) ,
x25519_pubkey : None ,
rssi : None ,
snr : None ,
last_heard : String ::new ( ) ,
hops : 0 ,
last_advert : 0 ,
reachable ,
feat(mesh): Reticulum LoRa hardware gates pass + RNS Resource transfer + image/voice attachments
Phase 0 gates #2/#3 (two-node LXMF-over-LoRa, external Sideband interop) passed
on real hardware (.116's flashed Heltec V3 RNode <-> a phone-flashed RNode running
Sideband) — RNS announce, encrypted DM round-trip, and contact binding all verified
live. Fixed two bugs found in the process: the Reticulum send path wasn't stamping
outbound messages as E2E despite LXMF being unconditionally encrypted, and the
per-message transport pill collapsed Meshcore/Meshtastic into one generic "lora"
color instead of distinguishing the three radio transports.
Built on top of that link: a Columba-style image/file send experience —
compression-quality presets with a real transfer-time estimate (mesh.transport-advice,
now device-throughput-aware), receive-side thumbnail previews + auto-render for
already-local attachments, and async voice messages, all reusing the existing
ContentRef/ContentInline attachment pipeline. The headline addition is genuine RNS
Resource transfer support (daemon-side RNS.Link + RNS.Resource, Rust-side
send_resource/resource_recv plumbing, a new "resource-mesh" transport-advice tier)
so compressed photos up to 2MB now actually transfer over LoRa for Reticulum peers
instead of always falling back to Tor past the small inline-chunk cap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 19:57:01 -04:00
pkc_capable : false ,
feat(mesh): Meshtastic RSSI/SNR + peer-location map wiring (backlog #14/#15, part 1)
Backend: parse_mesh_packet now decodes MeshPacket.rx_snr (field 8, float) and
rx_rssi (field 12, int32), and a new POSITION_APP branch decodes Position.
latitude_i/longitude_i (fields 1/2, sfixed32) -- all field numbers confirmed
against the canonical meshtastic/protobufs mesh.proto, not guessed. Threaded
through ParsedContact -> refresh_contacts -> MeshPeer (mirroring how
pkc_capable was wired for #17), so mesh.peers now surfaces real rssi/snr/lat/
lon instead of always-null. Fixed a real bug found along the way:
update_node_info's unconditional contact replace would have silently wiped
any already-tracked signal/position data on the next NodeInfo packet -- now
preserves it.
Frontend: mesh.ts's updateNodePositionsFromPeers() feeds real position data
into the SAME nodePositions map MeshMap.vue already renders from (parallel to
the existing Coordinate/Alert-message path) -- MeshMap.vue itself needed zero
changes, it was already built for this.
105/105 mesh tests pass (4 new: rx_snr/rx_rssi decode, position decode +
incomplete-field handling, full packet_to_inbound_frame integration).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-06-30 22:52:42 -04:00
lat : None ,
lon : None ,
2026-06-20 08:01:14 -04:00
}
}
#[ test ]
fn test_group_peer_twins_collapses_radio_and_federation ( ) {
let radio = mk_peer ( 42 , " Archy-X250-EXP " , Some ( " ABCD " ) , false ) ;
let fed = mk_peer ( 0x8000_0001 , " Archy-X250-EXP " , Some ( " abcd " ) , true ) ;
let groups = group_peer_twins ( & [ radio , fed ] ) ;
assert_eq! ( groups . len ( ) , 1 , " twins must collapse to one conversation " ) ;
let g = & groups [ 0 ] ;
// Canonical = the radio twin (mesh-first send), but reachability is the
// OR across twins (federation is reachable off-radio).
assert_eq! ( g . canonical . contact_id , 42 ) ;
assert! ( g . canonical . reachable ) ;
// Both ids retained so messages can be unioned across them.
assert! ( g . contact_ids . contains ( & 42 ) & & g . contact_ids . contains ( & 0x8000_0001 ) ) ;
}
#[ test ]
fn test_group_peer_twins_keeps_distinct_identities_and_unbound_radio ( ) {
// Two different identities + one radio peer that was never bound to a
// federation twin (arch = None) → three separate conversations.
let a = mk_peer ( 1 , " Alice " , Some ( " aa " ) , false ) ;
let b = mk_peer ( 2 , " Bob " , Some ( " bb " ) , true ) ;
let lonely = mk_peer ( 3 , " Carol-radio " , None , false ) ;
let groups = group_peer_twins ( & [ a , b , lonely ] ) ;
assert_eq! ( groups . len ( ) , 3 ) ;
}
#[ test ]
fn test_group_peer_twins_federation_only_uses_federation_id ( ) {
let fed = mk_peer ( 0x8000_00ff , " Arch Dev " , Some ( " dead " ) , true ) ;
let groups = group_peer_twins ( & [ fed ] ) ;
assert_eq! ( groups . len ( ) , 1 ) ;
assert_eq! ( groups [ 0 ] . canonical . contact_id , 0x8000_00ff ) ;
}
2026-03-17 00:03:08 +00:00
#[ test ]
fn test_mesh_config_default ( ) {
let config = MeshConfig ::default ( ) ;
assert! ( ! config . enabled ) ;
assert_eq! ( config . channel_name , Some ( " archipelago " . to_string ( ) ) ) ;
assert! ( config . broadcast_identity ) ;
}
#[ test ]
fn test_mesh_config_serialization ( ) {
let config = MeshConfig {
enabled : true ,
device_path : Some ( " /dev/ttyUSB0 " . to_string ( ) ) ,
channel_name : Some ( " test " . to_string ( ) ) ,
broadcast_identity : false ,
advert_name : Some ( " MyNode " . to_string ( ) ) ,
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
.. Default ::default ( )
2026-03-17 00:03:08 +00:00
} ;
let json = serde_json ::to_string ( & config ) . unwrap ( ) ;
let parsed : MeshConfig = serde_json ::from_str ( & json ) . unwrap ( ) ;
assert! ( parsed . enabled ) ;
assert_eq! ( parsed . device_path , Some ( " /dev/ttyUSB0 " . to_string ( ) ) ) ;
assert_eq! ( parsed . advert_name , Some ( " MyNode " . to_string ( ) ) ) ;
}
#[ tokio::test ]
async fn test_load_config_default_when_no_file ( ) {
let dir = tempfile ::tempdir ( ) . unwrap ( ) ;
let config = load_config ( dir . path ( ) ) . await . unwrap ( ) ;
assert! ( ! config . enabled ) ;
}
#[ tokio::test ]
async fn test_save_and_load_config_roundtrip ( ) {
let dir = tempfile ::tempdir ( ) . unwrap ( ) ;
let config = MeshConfig {
enabled : true ,
device_path : Some ( " /dev/ttyUSB0 " . to_string ( ) ) ,
channel_name : Some ( " archy " . to_string ( ) ) ,
broadcast_identity : true ,
advert_name : None ,
feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling
Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
(bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha
Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:56:37 +00:00
.. Default ::default ( )
2026-03-17 00:03:08 +00:00
} ;
save_config ( dir . path ( ) , & config ) . await . unwrap ( ) ;
let loaded = load_config ( dir . path ( ) ) . await . unwrap ( ) ;
assert! ( loaded . enabled ) ;
assert_eq! ( loaded . device_path , Some ( " /dev/ttyUSB0 " . to_string ( ) ) ) ;
}
2026-07-03 18:08:35 +00:00
/// End-to-end: `MeshService::start()` spawns a real `reticulum-daemon` in
/// plain-TCP CLIENT mode (no serial RNode, no `probe_rnode`), dials a
/// second stand-alone daemon instance running in TCP SERVER mode (the
/// Aurora-side role — Aurora's `RnsTcpInterface` dials the same way), and
/// reaches `device_connected: true` via the exact `mesh.status`-backing
/// `MeshService::status()` call the RPC layer uses. This is the Rust-side
/// half of the archy<->Aurora TCP interop gate (see
/// docs/RETICULUM-TRANSPORT-PROGRESS.md); the LXMF wire-level proof
/// itself already passed via a scripted RNS/LXMF stand-in (Steps 1-2 of
/// that gate), so this test only needs to prove Rust's spawn/connect
/// path, not re-prove LXMF content delivery.
///
/// Requires `reticulum-daemon/.venv` (`python3 -m venv .venv &&
/// .venv/bin/pip install -r requirements.txt`) — skips (not fails) if
/// absent, since CI doesn't provision a Python/RNS/LXMF environment for
/// the rest of the mesh test suite either.
#[ tokio::test ]
#[ ignore = " spawns real reticulum-daemon subprocesses over loopback TCP; run manually with `cargo test -p archipelago -- --ignored mesh_service_connects_over_reticulum_tcp` " ]
async fn mesh_service_connects_over_reticulum_tcp_client ( ) {
use ed25519_dalek ::SigningKey ;
use rand ::rngs ::OsRng ;
let repo_root = std ::path ::Path ::new ( env! ( " CARGO_MANIFEST_DIR " ) )
. parent ( )
. and_then ( | p | p . parent ( ) )
. expect ( " core/archipelago has two ancestors up to the repo root " )
. to_path_buf ( ) ;
let venv_py = repo_root . join ( " reticulum-daemon/.venv/bin/python " ) ;
let daemon_script = repo_root . join ( " reticulum-daemon/reticulum_daemon.py " ) ;
if ! venv_py . exists ( ) {
eprintln! (
" SKIP mesh_service_connects_over_reticulum_tcp_client: {} not found — \
run ` python3 - m venv . venv & & . venv / bin / pip install - r requirements . txt ` \
in reticulum - daemon / " ,
venv_py . display ( )
) ;
return ;
}
let root = tempfile ::tempdir ( ) . unwrap ( ) ;
// Stand-in "Aurora-side" server daemon: a second, independent
// reticulum-daemon instance in --tcp-listen mode. Any free loopback
// port works; this test doesn't need it fixed, so ask the OS for one.
let bind_addr = {
let sock = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let addr = sock . local_addr ( ) . unwrap ( ) ;
drop ( sock ) ; // release it so the daemon can bind — small TOCTOU window, acceptable for a manual/ignored test
addr
} ;
let server_dir = root . path ( ) . join ( " standin-server " ) ;
std ::fs ::create_dir_all ( & server_dir ) . unwrap ( ) ;
let server_seed = SigningKey ::generate ( & mut OsRng ) ;
std ::fs ::write ( server_dir . join ( " seed.bin " ) , server_seed . to_bytes ( ) ) . unwrap ( ) ;
let mut server = tokio ::process ::Command ::new ( & venv_py )
. arg ( & daemon_script )
. arg ( " --identity-key " )
. arg ( server_dir . join ( " seed.bin " ) )
. arg ( " --socket " )
. arg ( server_dir . join ( " server.sock " ) )
. arg ( " --rns-config " )
. arg ( server_dir . join ( " rns-config " ) )
. arg ( " --tcp-listen " )
. arg ( bind_addr . to_string ( ) )
. arg ( " --display-name " )
. arg ( " standin-server " )
. kill_on_drop ( true )
. spawn ( )
. expect ( " failed to spawn stand-in reticulum-daemon (server) " ) ;
// Give the server daemon time to bind before the client dials it.
tokio ::time ::sleep ( Duration ::from_secs ( 2 ) ) . await ;
// Point ReticulumLink::daemon_command's dev-fallback at our venv —
// these env vars are read fresh on every spawn (mesh/reticulum.rs),
// so setting them here (test-process-global, but this test owns the
// only mesh session in this process) is sufficient.
std ::env ::set_var ( " ARCHY_RETICULUM_DAEMON_PY " , & venv_py ) ;
std ::env ::set_var ( " ARCHY_RETICULUM_DAEMON_SCRIPT " , & daemon_script ) ;
// Force the dev fallback even if a packaged binary happens to exist
// on this machine's PATH convention — this test wants exactly the
// freshly-built venv daemon under test.
std ::env ::set_var ( " ARCHY_RETICULUM_DAEMON_BIN " , " /nonexistent-force-dev-fallback " ) ;
let data_dir = root . path ( ) . join ( " node " ) ;
std ::fs ::create_dir_all ( data_dir . join ( " identity " ) ) . unwrap ( ) ;
let node_signing_key = SigningKey ::generate ( & mut OsRng ) ;
std ::fs ::write (
data_dir . join ( " identity " ) . join ( " node_key " ) ,
node_signing_key . to_bytes ( ) ,
)
. unwrap ( ) ;
let ed_pubkey_hex = hex ::encode ( node_signing_key . verifying_key ( ) . to_bytes ( ) ) ;
let did = crate ::identity ::did_key_from_pubkey_hex ( & ed_pubkey_hex ) . unwrap ( ) ;
let config = MeshConfig {
enabled : true ,
reticulum_tcp : Some ( types ::ReticulumTcpConfig ::Client {
connect : vec ! [ bind_addr . to_string ( ) ] ,
} ) ,
.. Default ::default ( )
} ;
save_config ( & data_dir , & config ) . await . unwrap ( ) ;
let mut service = MeshService ::new ( & data_dir , & node_signing_key , & did , & ed_pubkey_hex )
. await
. expect ( " MeshService::new failed " ) ;
service . start ( ) . expect ( " MeshService::start failed " ) ;
let deadline = tokio ::time ::Instant ::now ( ) + Duration ::from_secs ( 20 ) ;
let mut last_status = service . status ( ) . await ;
while tokio ::time ::Instant ::now ( ) < deadline {
last_status = service . status ( ) . await ;
if last_status . device_connected {
break ;
}
tokio ::time ::sleep ( Duration ::from_millis ( 300 ) ) . await ;
}
let _ = server . start_kill ( ) ;
assert! (
last_status . device_connected ,
" MeshService never reached device_connected: true over Reticulum TCP client \
mode within 20 s ( last status : { last_status :? } ) "
) ;
assert_eq! ( last_status . device_type , DeviceType ::Reticulum ) ;
}
2026-03-17 00:03:08 +00:00
}