Files
archy/.planning/phases/01-federation-mesh-hardening/01-04-SUMMARY.md

9.0 KiB

phase, plan, subsystem, tags, status, requires, provides, affects, tech-stack, key-files, decisions, requirements-completed, metrics
phase plan subsystem tags status requires provides affects tech-stack key-files decisions requirements-completed metrics
01-federation-mesh-hardening 04 mesh
lightning
mesh
FED-05
typed-envelope
channel-open
complete
01-CONTEXT.md's LOCKED FED-05 scope: the picker's public/other list is meshed peers that have Lightning installed
RESEARCH.md Pitfall 5 — neither datum existed; PATTERNS.md — peer capability advertisement has no analog
lnd.getinfo carries identity_pubkey + uris (or an honest absence)
MeshMessageType::LightningInfo = 26 + LightningInfoPayload + is_valid_lightning_uri()
MeshPeer.lightning_uri, populated only by an explicit advertisement
mesh.lightning-peers (deduplicated, deterministically ordered) and mesh.send-lightning-info (target required)
core/archipelago/src/api/rpc/lnd/info.rs
core/archipelago/src/mesh/message_types.rs
core/archipelago/src/mesh/types.rs
core/archipelago/src/mesh/listener/dispatch.rs
core/archipelago/src/mesh/listener/decode.rs (unplanned — see Deviations)
core/archipelago/src/mesh/listener/session.rs (unplanned — see Deviations)
core/archipelago/src/mesh/mod.rs (unplanned — see Deviations)
core/archipelago/src/api/rpc/mesh/typed_messages.rs
core/archipelago/src/api/rpc/dispatcher.rs
added patterns
Extract a pure function at the seam so a contract is testable without a live service (map_identity, build_lightning_peer_list, parse_send_lightning_target) — the same shape Task 1's plan prescribed, reused for Task 3 where no handler test harness exists
Validate unauthenticated RF input BEFORE touching stored state, so a malformed message cannot destroy a good prior value
Mutation testing as evidence that tests are load-bearing, where pre-implementation failure output was not captured
created modified
core/archipelago/src/api/rpc/lnd/info.rs
core/archipelago/src/mesh/message_types.rs
core/archipelago/src/mesh/types.rs
core/archipelago/src/mesh/listener/dispatch.rs
core/archipelago/src/mesh/listener/decode.rs
core/archipelago/src/mesh/listener/session.rs
core/archipelago/src/mesh/mod.rs
core/archipelago/src/api/rpc/mesh/typed_messages.rs
core/archipelago/src/api/rpc/dispatcher.rs
is_valid_lightning_uri deliberately does NOT resolve or dial the host — that would turn a received advertisement into an outbound connection an attacker chose
Dedup keys on identity_pubkey_hex() (the authenticating key), lowercased, never the firmware routing key — T-01-11
last_heard compared as a parsed RFC3339 timestamp, not as a string, so a differing UTC offset cannot misorder 'newest wins'
Three unplanned files were touched: all three rebuild MeshPeer wholesale and would have silently wiped lightning_uri
duration completed tasks_completed tasks_total
~1h 2026-08-02 3 3

01-04 — the two Lightning facts the channel-open picker needs (FED-05)

What shipped

Task Delivered
1 lnd.getinfo deserializes and returns identity_pubkey + uris; a pubkey that is not 66 hex chars maps to None rather than being forwarded
2 MeshMessageType::LightningInfo = 26, LightningInfoPayload { uri, alias? }, is_valid_lightning_uri(), MeshPeer.lightning_uri, and a validating inbound dispatch arm
3 mesh.lightning-peers (filtered, deduplicated, stable-ordered) and mesh.send-lightning-info (explicit target required), both registered in the dispatcher

The part that was not in the plan, and mattered most

MeshPeer.lightning_uri was specified as a field addition. It is, but three separate code paths rebuild a MeshPeer wholesale, and every one of them would have silently discarded the new field:

  1. listener/decode.rs — the identity-advert path. A wholesale peers.insert() that hand-preserves only advert_name and lat/lon. Its own comment records why those two are there: Reticulum "re-emits identity adverts every announce tick", which had previously been renaming every federated contact once a minute. A stored Lightning URI would have been erased on the same schedule.
  2. listener/session.rsrefresh_contacts. Rebuilds the record from the radio snapshot, which carries no Lightning datum.
  3. mesh/mod.rs — federation seeding. Same shape.

All three now carry the previous value forward. Without this the feature would have appeared to work in tests and quietly emptied the picker on a live node — the failure mode is an absence, which is exactly the kind that does not announce itself.

Security posture

  • T-01-12 (tampering): the inbound arm validates the URI before the write and returns early on failure, so a malformed advertisement from anyone in range cannot blank out a real peer's entry. Asserted by test, not just by reading.
  • T-01-13 (disclosure): mesh.send-lightning-info requires an explicit contact_id. There is no broadcast form, and the test asserts that {}, {"broadcast": true} and an out-of-range id are all refused rather than treated as "send to everyone".
  • T-01-11 (spoofing): dedup keys on the authenticating key, never the firmware routing key.
  • T-01-15 (EoP): server.rs is untouched — git diff HEAD on it is empty, and is_peer_allowed_path still occurs 13 times. The peer allow-list was not widened.
  • Wire compatibility: discriminant 26 was unused, so a node predating this fails to decode the message rather than mis-decoding it as another type. The optional alias is skip_serializing_if, asserted to cost fewer bytes on air when absent — this rides LoRa.

Evidence

  • cargo test -p archipelago: 1087 passed, 0 failed, 2 ignored.
  • New tests: 5 (lnd::info), 5 (mesh::message_types), 6 (lightning_peer_tests).
  • cargo clippy --all-targets: no warnings in any touched module (two useless_format lints in the new test code were fixed, not waived).
  • Every acceptance-criteria grep met, including the negative one on server.rs.

Deviation: TDD ordering on Task 1

The plan required the SUMMARY to record "the pre-implementation failing output of the fixture tests". Tests and implementation were written in the same pass, so that output does not exist and is not reproduced here.

Rather than drop the requirement's intent — prove the tests are load-bearing — a mutation test was run in its place. is_valid_identity_pubkey was replaced with true, and the suite re-run:

3 failed:
  api::rpc::lnd::info::tests::malformed_pubkey_is_dropped_rather_than_propagated
  api::rpc::lnd::info::tests::a_malformed_pubkey_does_not_discard_the_advertised_uris
  api::rpc::lnd::info::tests::valid_pubkey_shape_matches_the_openchannel_rule

The mutation was reverted and its absence verified. This is stronger evidence than a pre-implementation red run (which only shows the code is absent, not that the assertions bind), but it is a deviation from the ordering the plan asked for, and is recorded as one.

On-node verification (archi-dev-box, deployed 23:16)

Deployed at commit 6b3693dc off a clean tree and exercised over the real RPC surface:

Call Result
lnd.getinfo identity_pubkey: 024a5fd7de13623aeec81095… — a real key, deserialized by the field that did not exist before this plan. alias: "Archipelago Node"
mesh.lightning-peers {"peers":[]}success with an empty array, not an error. The FED-05 empty edge, proven on hardware rather than only in a unit test
mesh.send-lightning-info {} refused: "Missing contact_id: … requires an explicit target and has no broadcast form" — T-01-13's mitigation observed live
mesh.send-lightning-info with a target refused: "This node has no advertised Lightning URI to share — LND may be down, or configured with no externally reachable address"

A real finding from that last row: this node's lnd.getinfo returns uris: []. Its LND has no externally reachable address configured, so it cannot advertise itself to a peer — the handler correctly refuses rather than sending an empty advertisement a peer would store as an undialable target. The receive and list halves work; the send half is inert on any node whose LND advertises no URI. 01-06 must not assume the local node can always share its own URI, and the picker needs a sensible state for "you have nothing to share yet".

Open / handed on

  • The mesh leg is still unproven end to end. Two archy nodes with LND and a radio link are needed to watch a real advertisement traverse the air and land in a peer's lightning_uri. Everything above is either unit-level or single-node RPC.
  • mesh.lightning-peers is a data source with no consumer until 01-06 builds the picker UI (which depends_on this plan).
  • MeshPeer.lightning_uri's doc mentions federation seeding as a future source; this plan does not implement it, and mesh/mod.rs only preserves an existing value.