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 |
|
complete |
|
|
|
|
|
|
|
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:
listener/decode.rs— the identity-advert path. A wholesalepeers.insert()that hand-preserves onlyadvert_nameandlat/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.listener/session.rs—refresh_contacts. Rebuilds the record from the radio snapshot, which carries no Lightning datum.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-inforequires an explicitcontact_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.rsis untouched —git diff HEADon it is empty, andis_peer_allowed_pathstill 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
aliasisskip_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 (twouseless_formatlints 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-peersis a data source with no consumer until 01-06 builds the picker UI (whichdepends_onthis plan).MeshPeer.lightning_uri's doc mentions federation seeding as a future source; this plan does not implement it, andmesh/mod.rsonly preserves an existing value.