Files

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-federation-mesh-hardening 07 execute 2
01-04
core/archipelago/src/mesh/message_types.rs
core/archipelago/src/mesh/listener/dispatch.rs
core/archipelago/src/api/rpc/mesh/typed_messages.rs
core/archipelago/src/api/rpc/dispatcher.rs
true
FED-05
truths prohibitions artifacts key_links
A user can send a channel-open request to a meshed Lightning peer, carrying this node's own Lightning URI, an optional amount, and an optional message
A received channel-open request appears in the recipient's mesh conversation as a typed message showing the requester's URI and note, using the existing typed-message rendering path
Sending a channel-open request requires an explicit target peer — there is no broadcast form
A channel-open request whose payload URI is malformed is rejected on receipt and never stored as a message
Two channel-open requests sent to the same peer in quick succession produce two distinct messages with distinct sender sequence numbers, and neither is silently dropped (FED-05 concurrency edge, mesh half)
A request is never rendered or reported as an opened or funded channel — it carries no channel state
statement category
A channel-open request MUST NOT be presented anywhere as an accepted, open, or funded channel — a request that has not been acted on by the recipient must never appear in a channel list, a balance, or a connected-peer count transparency
statement category
Receiving a channel-open request MUST NOT cause the node to open a channel, connect to the requester, or move funds on its own — acting on a request is always a separate, explicit human decision safety
path provides contains
core/archipelago/src/mesh/message_types.rs ChannelOpenRequest typed message + payload ChannelOpenRequest
from to via pattern
core/archipelago/src/api/rpc/dispatcher.rs core/archipelago/src/api/rpc/mesh/typed_messages.rs mesh.request-channel match arm mesh.request-channel
from to via pattern
core/archipelago/src/mesh/listener/dispatch.rs core/archipelago/src/mesh/types.rs inbound ChannelOpenRequest is stored as a MeshMessage with its typed payload ChannelOpenRequest
Give a meshed Lightning peer a way to be *asked* for a channel: a typed mesh message carrying the requester's Lightning URI and an optional note, sent to one chosen peer and rendered in the recipient's conversation.

Purpose: FED-05's second list. CONTEXT.md locks the semantics — meshed peers with Lightning installed are nodes you "request to open a channel with", not nodes you open against directly, because mesh peers are not bilaterally trusted the way federated nodes are. 01-UI-SPEC.md fixes the UI verb ("Request Channel", reusing PeerRequestModal.vue's message field and busy states). PATTERNS.md records that the send/receive shape for this is typed_messages.rs's existing reaction/reply family — a struct-per-message-type serialized into the standard envelope — and that no capability/request mechanism exists yet to extend. Output: MeshMessageType::ChannelOpenRequest, its payload, an inbound arm that stores it as a conversation message, and a mesh.request-channel RPC.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/STATE.md @.planning/phases/01-federation-mesh-hardening/01-CONTEXT.md @.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md @.planning/phases/01-federation-mesh-hardening/01-PATTERNS.md @.planning/phases/01-federation-mesh-hardening/01-04-SUMMARY.md @core/archipelago/src/mesh/message_types.rs

Artifacts this phase produces

Created or changed by this plan:

Symbol Kind File
MeshMessageType::ChannelOpenRequest = 27 (label channel_open_request) new wire message type core/archipelago/src/mesh/message_types.rs
ChannelOpenRequestPayload { uri, amount_sats, message } new CBOR payload struct same
inbound ChannelOpenRequest arm listener dispatch arm storing the request as a MeshMessage core/archipelago/src/mesh/listener/dispatch.rs
handle_mesh_request_channel new RPC handler (mesh.request-channel) core/archipelago/src/api/rpc/mesh/typed_messages.rs
mesh.request-channel dispatcher match arm core/archipelago/src/api/rpc/dispatcher.rs
Task 1: End-to-end — a channel-open request is sent to one peer and lands in their conversation `MeshMessageType` is a radio wire format read by every fleet node after the next OTA; the discriminant and payload shape become externally visible, so a later change needs a coordinated fleet upgrade. Kept additive on an unused discriminant with serde-default optional payload fields, so older nodes ignore it rather than erroring. `MeshMessageType::LightningInfo = 26` exists (plan 01-04, Task 2) — confirm the highest current discriminant by reading `core/archipelago/src/mesh/message_types.rs` before choosing this type's number. core/archipelago/src/mesh/message_types.rs, core/archipelago/src/mesh/listener/dispatch.rs, core/archipelago/src/api/rpc/mesh/typed_messages.rs, core/archipelago/src/api/rpc/dispatcher.rs - `core/archipelago/src/mesh/message_types.rs` — the enum and the four places every variant is registered (`enum`, `from_u8`, `from_label`, `label`), `InvoicePayload` (from L413) as the closest payload analog (it also carries a payment-ish string plus an optional amount), and the `TypedEnvelope` doc comment on `compact_bytes` and LoRa frame size. - `core/archipelago/src/api/rpc/mesh/typed_messages.rs` lines 637-760 — `handle_mesh_send_reply` and `handle_mesh_send_reaction`: param extraction, target-peer resolution, sequence-number allocation, `TypedEnvelope::new(...).with_seq(seq)`, and the send call. - `core/archipelago/src/mesh/listener/dispatch.rs` lines 430-500 — the `Reaction` and `Presence` inbound arms, and how an inbound typed message is turned into a stored `MeshMessage` with `message_type` and `typed_payload` set. - `core/archipelago/src/mesh/types.rs` lines 139-180 — the `MeshMessage` fields the stored request must populate (`plaintext` is the human-readable fallback shown in list views). - `core/archipelago/src/api/rpc/dispatcher.rs` lines 390-440 — the one-line registration convention. - `.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md` — the Copywriting Contract row for "Primary CTA — meshed Lightning peer", which fixes what the UI in plan 01-08 will send. - `MeshMessageType` round-trips the new variant through `from_u8`, `from_label`, and `label`. - `handle_mesh_request_channel` with no target peer in params returns an error; with an unknown target peer it returns an error naming the peer. - `handle_mesh_request_channel` with a target sends exactly one envelope of the new type, whose payload carries this node's own Lightning URI and the caller's optional amount and message. - Two consecutive calls to the same target allocate two different sequence numbers. - An inbound envelope of the new type with a well-formed URI is stored as a `MeshMessage` whose `message_type` is the new label and whose `typed_payload` carries the request fields. - An inbound envelope whose payload URI is malformed stores nothing and logs a warning. Write the tests first and confirm they fail.
Add `ChannelOpenRequest = 27` to `MeshMessageType` (confirm 27 is unused first) with a doc comment
stating that this is a *request*, that it carries no channel state, and that receiving one never
causes the node to act. Register it in `from_u8`, `from_label` ("channel_open_request"), and
`label`.

Add `ChannelOpenRequestPayload` beside the other payload structs: a required `uri: String` (the
requester's own `pubkey@host:port`), plus `#[serde(default)] amount_sats: Option<u64>` and
`#[serde(default)] message: Option<String>`. Bound the optional message length before send so a
long note cannot blow past the LoRa framing budget the `TypedEnvelope` doc comment warns about;
truncate at the send side rather than rejecting, and say so in a comment.

Add `handle_mesh_request_channel` following the `handle_mesh_send_reply` shape: require a target
peer identifier in params and error without one (there is no broadcast form); read this node's
own Lightning URI via the identity path plan 01-04 added to `lnd.getinfo`, and error with a clear
message when it is unavailable rather than sending an empty request; build the payload, wrap it in
a `TypedEnvelope` with a freshly allocated sequence number, and send it to that peer only.
Register it in the dispatcher as `"mesh.request-channel"`.

Add the inbound arm in `dispatch.rs` mirroring the `Reaction` arm: deserialize the payload,
validate the URI shape (66-hex pubkey part, optional `@host[:port]`), and on success store a
`MeshMessage` with the new label as `message_type`, the payload as `typed_payload`, and a
human-readable `plaintext` summary naming the requester and the requested amount when present.
On a malformed URI, log at `warn!` and store nothing.

Do not add any code path that connects to, opens a channel with, or funds the requester on
receipt. The inbound arm's only effect is storing a message.
cd core && cargo test -p archipelago mesh::message_types mesh::listener api::rpc::mesh - `cd core && cargo test -p archipelago mesh::message_types mesh::listener api::rpc::mesh` exits 0 with the six behaviors above present as named cases. - `grep -c 'ChannelOpenRequest' core/archipelago/src/mesh/message_types.rs` is at least 5. - `grep -c 'channel_open_request' core/archipelago/src/mesh/message_types.rs` is at least 2. - `grep -c '"mesh.request-channel"' core/archipelago/src/api/rpc/dispatcher.rs` equals 1. - `grep -c 'ChannelOpenRequest' core/archipelago/src/mesh/listener/dispatch.rs` is at least 1. - The inbound arm contains no call to any `openchannel`, `connectpeer`, or send-funds path — verified by reading the arm and recorded in the SUMMARY. - `cd core && cargo test -p archipelago` exits 0. - The SUMMARY records the pre-implementation failing test output. A channel-open request travels from an RPC call to a chosen peer's conversation, with no side effect beyond a stored message. Task 2: Harden the request path against duplicates, oversize, and misuse core/archipelago/src/api/rpc/mesh/typed_messages.rs, core/archipelago/src/mesh/listener/dispatch.rs - `core/archipelago/src/api/rpc/mesh/typed_messages.rs` as left by Task 1, plus the `handle_mesh_send_content_inline` size-tier logic for how this codebase bounds payload size before a send. - `core/archipelago/src/mesh/outbox.rs` — whether an outbound send is queued and retried, so the duplicate-suppression window is placed where it will actually see both attempts. - `core/archipelago/src/mesh/types.rs` — `MeshPeer`'s authenticating-key accessor doc comment (never use the firmware routing key for authentication). Add a short duplicate-suppression window to `handle_mesh_request_channel`: a second request to the same target peer within a bounded interval returns a distinct, non-error result reporting that a request was already sent, rather than emitting a second envelope. The UI in plan 01-08 also disables its button while a send is in flight, but a backend guard is what actually stops a double-click or a retried RPC from spamming a peer over a slow radio link. Two requests separated by more than the window must both go out — the window suppresses accidental duplicates, not legitimate repeat requests. Add tests for both sides of the window.
Bound the inbound side too: reject an inbound payload whose message field exceeds the same
length bound the send side truncates at, and reject an `amount_sats` outside the range
`handle_lnd_openchannel` accepts (its existing 20,000..=16,777,215 sat bounds) so a request can
never carry an amount the recipient could not act on. Read `channels.rs` for those exact bounds
rather than restating them from memory.

Attribute the stored inbound message to the peer's authenticating identity key, not the firmware
routing key, following the `MeshPeer` accessor's documented rule — a request that claims to be
from a trusted peer must be attributable.
cd core && cargo test -p archipelago mesh api::rpc::mesh - `cd core && cargo test -p archipelago mesh api::rpc::mesh` exits 0, including a within-window suppression case and an outside-window pass-through case. - A test asserts an inbound request with an out-of-range `amount_sats` is rejected, using the bounds read from `channels.rs` rather than hardcoded duplicates of them. - `cd core && cargo test -p archipelago` exits 0. - `cd core && cargo clippy -p archipelago --all-targets` produces no new warnings in `mesh` or `api::rpc::mesh`. Accidental duplicate requests are suppressed, oversize and out-of-range requests are refused, and every stored request is attributable to a verified identity.

<threat_model>

Trust Boundaries

Boundary Description
radio peer → typed-envelope decode → stored message Untrusted RF input becomes a conversation entry naming a payment endpoint
operator RPC → outbound request An operator action discloses this node's payment endpoint to a chosen mesh peer

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-01-26 Spoofing a peer sending a request that appears to come from a trusted node, luring a channel open to an attacker's URI high mitigate The stored message is attributed to the peer's verified archipelago identity key, never the firmware routing key (Task 2); the recipient's action on a request is always explicit and human
T-01-27 Elevation of Privilege a received request causing an automatic channel open or fund movement high mitigate The inbound arm's only effect is storing a message; the acceptance criteria require reading the arm and recording that it contains no open/connect/send-funds call
T-01-28 Denial of Service request flooding filling a peer's conversation or saturating a LoRa link high mitigate Send-side duplicate-suppression window plus inbound length and amount bounds (Task 2)
T-01-29 Information Disclosure broadcasting this node's payment endpoint to every contact in range high mitigate A target peer is required; the handler errors without one and there is no broadcast form
T-01-30 Tampering an oversize payload fragmenting into unreassemblable LoRa chunks medium mitigate The message field is truncated at the send side against the framing budget the TypedEnvelope doc comment describes; inbound oversize is rejected
T-01-SC Tampering npm/pip/cargo installs high mitigate No new crates. If one becomes necessary, stop and run the Package Legitimacy Gate before installing
</threat_model>
- `cd core && cargo test -p archipelago` — green. - `cd core && cargo clippy -p archipelago --all-targets` — no new warnings in the touched modules. - The SUMMARY states, from a direct read, that the inbound arm performs no Lightning action.

<success_criteria>

  • A new typed mesh message carries a channel-open request to one named peer.
  • Receiving one stores a conversation message and does nothing else.
  • Duplicates within a short window are suppressed; legitimate repeats are not.
  • Malformed URIs, oversize notes, and out-of-range amounts are refused. </success_criteria>
Create `.planning/phases/01-federation-mesh-hardening/01-07-SUMMARY.md` when done. Stage by explicit path, commit, and `git push gitea-ai main`.