47 KiB
Phase 1: Federation & Mesh Hardening - Research
Researched: 2026-07-29 Domain: Federation node sync/removal (Rust/Tokio async daemon), mesh RPC parity (Rust + Node.js mock backend), Lightning channel-open UX (Vue 3), on-brand success animation (Vue 3/CSS) Confidence: HIGH (backend federation/mesh code — read directly, git-blamed); MEDIUM (FED-05 Lightning-URI UX — net-new surface, no prior art in repo); HIGH (FED-06 — both source components read directly)
<user_constraints>
User Constraints (from CONTEXT.md)
No CONTEXT.md exists for this phase (not yet run through /gsd-discuss-phase). No locked decisions or discretion areas to honor beyond REQUIREMENTS.md and the phase description supplied by the orchestrator. Treat all implementation choices below as recommendations for the planner, not locked decisions — the planner should flag any of these that warrant a user check-in (see ## Assumptions Log).
</user_constraints>
Summary
The federation and mesh code is more mature than CONCERNS.md suggests — several concerns it lists (tombstone-write-swallowed, DID-join without signature verification) were already fixed in commit 01cbec27 (2026-07-02) and the handle_federation_peer_joined signature-verification path respectively. Do not treat CONCERNS.md as current truth for this phase; the structured review (FED-03) must re-verify each claim against the code read in this research before acting on it.
The real, currently-live bug class behind the user's "nodes reappear / sync issues" reports is almost certainly a concurrency race on federation/nodes.json: federation/storage.rs has zero locking (no Mutex, no atomic temp-file+rename) around load_nodes() → mutate → save_nodes(), yet the daemon runs two independent, overlapping periodic federation-sync loops (server.rs ~line 497, every 90s; server.rs ~line 840, every 1800s) plus the manual federation.sync-state RPC and federation.remove-node RPC — all of which do their own read-modify-write cycle against the same file with no coordination. A remove_node() call racing against an in-flight sync_with_peer()'s update_node_state() (which read the node list before the removal landed) will have the sync's stale read clobber the just-written removal when it saves — the removed node reappears with no error, exactly matching the reported symptom, and it is invisible to logs because both loops only debug!() on failure. This is the primary hypothesis to design a fix and a regression test around for FED-01/FED-02.
Mesh attachment-send parity (FED-04) was fixed just before this phase started (commit c2ce71c6, uncommitted → committed by another concurrent agent during this research session): mock-backend.js now implements mesh.send-content-inline / mesh.send-content / mesh.fetch-content / mesh.transport-advice mirroring the daemon's real tier logic. What remains for full "rest of the mesh chat surface" parity: mesh.contacts-list / mesh.contacts-save (peer aliasing, called live from Mesh.vue on mount and on rename) are not implemented in mock-backend.js at all and will 404 with "Method not found" on the demo; and mesh.send-reaction / send-reply / edit-message / delete-message / forward-message are stubbed as bare {ok:true} acks that never mutate meshStore.dynamic, so reactions/edits/deletes silently don't render on the demo even though the RPC call "succeeds."
FED-05 (Lightning channel-open UX) is greenfield: no RPC anywhere in the codebase currently returns this node's own Lightning identity_pubkey/uris (LND's own /v1/getinfo provides both, but handle_lnd_getinfo in core/archipelago/src/api/rpc/lnd/info.rs doesn't parse or forward them), and NodeStateSnapshot (the federation sync payload) carries no Lightning fields for a peer's pubkey/host, so there is no way today to look up a federated peer's channel-open target. handle_lnd_openchannel (channels.rs) already accepts pubkey + optional address + amount/fee params and does the connect-then-open sequence correctly — reuse it as-is. "Public nodes" browse/request has no existing data source in this codebase (no LN graph query, no curated list) and needs a scope decision from the user before planning task breakdown.
FED-06 is a straightforward swap: ScreensaverRing.vue (compact size = 240px/320px) renders only the radiating EQ segments (no circle of its own — the "circle" is the separately-layered content in the center, exactly as Screensaver.vue does with ScreensaverLogo). SendBitcoinModal.vue's .send-success-burst is 112px (7rem) with 3 CSS-ripple .burst-ring elements plus a .burst-core circle+checkmark — swap the .burst-ring elements for <ScreensaverRing size="compact" />, keep .burst-core+checkmark centered on top, and reconcile the size mismatch (ring is 2-3x larger than the current burst container; either scale it down via CSS transform: scale() or accept the larger footprint since the modal is max-w-2xl). WalletScanModal.vue has a second, simpler "paid tick" (.success-ring, no ripple animation at all) that the phase's "wherever else the paid tick appears" clause covers — plan to update both.
Primary recommendation: Start FED-03's structured review by (1) auditing every federation::storage read-modify-write call site for the missing-lock race described above and design a fix (a tokio::sync::Mutex per data_dir, or collapsing the two periodic sync loops into one), (2) re-verifying every CONCERNS.md federation/mesh claim against current code before acting on it, (3) filling the two demo-parity gaps in mock-backend.js (contacts-list/save + stateful reaction/edit/delete), (4) treating FED-05 as new RPC surface (own-node Lightning URI, peer Lightning info propagation, and a scoped "public nodes" answer) before any UI work, and (5) the FED-06 CSS/component swap.
Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|---|---|---|---|
| Federation node list, tombstones, sync loops | API / Backend (core/archipelago/src/federation/) |
— | Disk-persisted state; must be race-free at the storage layer, not patched in the RPC or UI layer |
| Federation removal propagation to mesh chat | API / Backend (core/archipelago/src/mesh/mod.rs::purge_federation_peer) |
Frontend (Pinia stores/mesh.ts) |
Backend already purges peer/messages/contacts server-side; frontend must not cache a stale contact after the WebSocket state bump |
| Mesh RPC surface (attachments, reactions, contacts) | API / Backend (core/archipelago/src/api/rpc/mesh/) |
Dev tooling (neode-ui/mock-backend.js) |
The demo backend is a parity shim over the same RPC surface — it must mirror backend behavior, never invent its own contract |
| FIPS/Tor transport dial + fallback | API / Backend (core/archipelago/src/fips/dial.rs, transport/) |
— | Transport selection is a backend concern; UI only displays the resulting badge (last_transport) |
| Lightning node URI (own + peer) | API / Backend (new: lnd.getinfo extension, federation sync payload extension) |
Frontend (new modal) | LND is the source of truth for identity_pubkey/uris; federation sync is the transport for sharing a peer's LN info |
| Channel-open UX (initiate) | Frontend (new modal, Teleport-to-body, house style) |
API / Backend (lnd.openchannel — already exists) |
Backend channel-open RPC is complete; only the UI (URI share, trusted-peer picker, public-node browse) is missing |
| Paid-tick success animation | Frontend (SendBitcoinModal.vue, WalletScanModal.vue, ScreensaverRing.vue) |
— | Pure presentation; no backend involvement |
Standard Stack
This phase does not introduce new external dependencies. It is a hardening + UI-surface pass over an existing Rust (Tokio/Hyper/reqwest/serde) backend and Vue 3 (Pinia, Vue Router, Teleport) frontend, plus a Node.js/Express demo backend (neode-ui/mock-backend.js). No new libraries are needed for any of FED-01 through FED-06 — ScreensaverRing.vue and handle_lnd_openchannel already exist and should be reused, not reimplemented.
Core (existing, reused)
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| tokio | (workspace pin) | Async runtime, interval()/spawn() for the periodic sync loops |
Already the project's async foundation |
| reqwest | (workspace pin) | HTTP client for FIPS/Tor peer dial and LND REST calls | Already used throughout fips/dial.rs and api/rpc/lnd/ |
| serde/serde_json | (workspace pin) | Wire format for NodeStateSnapshot, RPC params |
Project-wide convention |
| Vue 3 + Pinia | (package.json pin) | Frontend reactivity/state | Existing frontend stack |
Version verification: No new packages are being added; skip registry verification per protocol (nothing to verify). If the planner introduces any new crate/npm package during execution, verify it then.
Package Legitimacy Audit
No external packages are being introduced by this phase — this section is not applicable. If a later plan step decides to add a dependency (e.g., a curated public-LSP list requires a small crate), run the Package Legitimacy Gate at that time.
Architecture Patterns
System Architecture Diagram
┌─────────────────────────────────────────┐
│ Federation node list (disk) │
│ federation/{nodes,removed-nodes}.json │
│ NO LOCK — read-modify-write per call │
└───────────────┬─────────────────────────┘
│ load_nodes() / save_nodes()
┌───────────────────────────┼───────────────────────────┬─────────────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ 90s auto-sync │ │ 1800s auto-sync │ │ RPC: sync-state, │ │ RPC: remove-node, │
│ loop (server.rs│ │ loop (server.rs │ │ (manual "Sync") │ │ set-trust, join, │
│ ~L497) │ │ ~L840) │ │ │ │ peer-joined │
└───────┬───────┘ └────────┬──────────┘ └────────┬──────────┘ └───────────┬─────────┘
│ sync_with_peer() │ sync_with_peer() │ │
│ → update_node_state() │ → update_node_state() │ │
└──────────────┬─────────────┴──────────────┬─────────────────┘ │
▼ ▼ ▼
(race: stale in-memory list from an in-flight sync's earlier load_nodes()
overwrites a concurrent remove_node()'s just-saved tombstoned list)
│
▼
┌─────────────────────────────────────┐
│ mesh/mod.rs: purge_federation_peer │
│ (peers, messages, contacts, presence)│
└───────────────┬───────────────────────┘
▼
┌─────────────────────────────────────┐
│ StateManager broadcast → WebSocket │
│ → Pinia stores → Federation.vue / │
│ Mesh.vue re-render │
└─────────────────────────────────────┘
Mesh attachment/parity path (FED-04):
Frontend attach flow → mesh.transport-advice → {auto-mesh|choose|tor-only}
→ mesh.send-content-inline (small) | mesh.send-content (large, via /api/blob)
real daemon: api/rpc/mesh/typed_messages.rs demo: mock-backend.js (mirrors tier logic — DONE)
Frontend contacts/reactions/edit/delete
real daemon: api/rpc/mesh/typed_messages.rs (contacts-list/save, send-reaction, edit-message, ...)
demo: mock-backend.js — contacts-list/save MISSING (404); reaction/edit/delete are no-op acks (GAP)
Recommended Project Structure
No new directories needed. Touch points:
core/archipelago/src/federation/storage.rs # add locking around load/save
core/archipelago/src/server.rs # collapse or coordinate the two sync loops
core/archipelago/src/api/rpc/lnd/info.rs # extend handle_lnd_getinfo with identity_pubkey/uris
core/archipelago/src/federation/types.rs # (maybe) add lightning fields to NodeStateSnapshot
core/archipelago/src/api/rpc/federation/handlers.rs # (maybe) new RPC to fetch a peer's LN info
neode-ui/mock-backend.js # add mesh.contacts-list/save, stateful reaction/edit/delete
neode-ui/src/components/LightningChannelModal.vue # NEW — FED-05 (name TBD by planner)
neode-ui/src/components/SendBitcoinModal.vue # FED-06 swap
neode-ui/src/components/WalletScanModal.vue # FED-06 swap (secondary paid-tick site)
Pattern 1: Federation removal is already "belt and suspenders" — reuse, don't rewrite
What: handle_federation_remove_node (handlers.rs:273) captures the peer's pubkey before calling federation::remove_node, then after removal calls mesh::purge_federation_peer to drop the synthetic mesh contact, its messages, presence, and persisted mesh-contacts entry. federation::remove_node (storage.rs:180) already tombstones the DID before saving the filtered node list and propagates a tombstone-write failure as an error (fixed in 01cbec27).
When to use: This is the correct pattern for FED-01 already. Don't redesign it — the actual gap is the concurrency race in the storage layer underneath it (see Pitfall 1), not the removal logic itself.
Example:
// Source: core/archipelago/src/federation/storage.rs:180-198 (already fixed, 01cbec27)
pub async fn remove_node(data_dir: &Path, did: &str) -> Result<Vec<FederatedNode>> {
let mut nodes = load_nodes(data_dir).await?;
let before = nodes.len();
nodes.retain(|n| n.did != did);
if nodes.len() == before {
anyhow::bail!("No federated node with DID {}", did);
}
// Tombstone FIRST and propagate failure — a remove whose tombstone
// never landed isn't a remove.
tombstone_did(data_dir, did).await.context("persist removal tombstone")?;
save_nodes(data_dir, &nodes).await?;
Ok(nodes)
}
Pattern 2: Transitive sync already respects tombstones — verify, don't re-add protection
What: merge_transitive_peers (sync.rs:120) loads load_removed_dids() and skips any hint whose DID is tombstoned, and handle_federation_peer_joined (handlers.rs:641) independently rejects a peer-joined callback for a tombstoned DID. Both paths that could resurrect a removed node already check the tombstone list.
When to use: The FED-03 review should write a test that concurrently exercises remove + an in-flight sync (see Pitfall 1) rather than re-deriving the (already-correct) tombstone-check logic.
Pattern 3: Demo backend must be a byte-for-byte RPC mirror, not a "close enough" mock
What: mock-backend.js's mesh.transport-advice case (line 4318) explicitly duplicates the daemon's size thresholds (MESH_AUTO_MAX = 1024, MESH_HARD_MAX = 2300) with a comment pointing at typed_messages.rs handle_mesh_transport_advice as the source of truth.
When to use: Apply the same pattern to mesh.contacts-list/mesh.contacts-save and to the reaction/edit/delete stubs — read the real handler in core/archipelago/src/api/rpc/mesh/typed_messages.rs (lines 1180-1371 for contacts/presence, 637-976 for reply/reaction/receipt/forward, 1065-1180 for edit/delete) and mirror its actual state transitions in meshStore.dynamic, not just an {ok:true} ack.
Anti-Patterns to Avoid
- Unlocked read-modify-write on shared JSON files:
federation/storage.rshas none ofload_nodes()/save_nodes()behind a mutex, and writes are a directfs::write()(not atomic temp+rename). Any new federation code must NOT add a third code path that does its own read-modify-write without going through a shared lock — that widens the race window instead of closing it. - Silent
debug!()on periodic-loop errors: Both sync loops inserver.rslog sync failures atdebug!level only (not surfaced to the state broadcast, not visible in the UI). FED-02 explicitly requires operator-visible sync errors — don't add a third silent loop; extend the existing ones to persist alast_sync_erroralongsidelast_seen. - Reinventing
handle_lnd_openchannel's connect-then-open sequence: It already doesperm=falsesynchronous peer connect before opening (with a documented reason:perm=trueraces and fails with "peer is not online"). Reuse it; do not write a second Lightning-channel RPC. - Assuming
ScreensaverRingis pre-sized for a 112px success badge: itscompactclass is 240px (mobile) / 320px (≥768px) — 2-3x the current.send-success-burst. Naive drop-in will overflow the modal card; must be explicitly scaled or the surrounding layout redesigned.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Connecting to + opening a channel with an LN peer | A new RPC | lnd.openchannel (api/rpc/lnd/channels.rs:238) |
Already validates pubkey format, amount bounds, fee params, and does the connect-before-open sequence correctly with the documented perm=false fix |
| Paid/success radial visual | A new ring/particle component | ScreensaverRing.vue (compact size) |
Explicitly required by FED-06; also already ships a reduced-motion-friendly animation pattern to copy for consistency |
| Peer transport badge (FIPS/Tor) | New transport-tracking logic | FederatedNode.last_transport/last_transport_at (already written by record_peer_transport) |
Already ground-truth (records what was actually used, not predicted) — FED-05's peer picker can reuse this field to show reachability |
| Federated-peer list for the "trusted nodes" picker | A new RPC | federation.list-nodes (already returns did, name, onion, trust_level, last_seen) |
FED-05 only needs to ADD Lightning fields to this payload/peer lookup, not build a parallel peer list |
Key insight: Almost every piece of infrastructure FED-01/02/04/05 need already exists in some form — the gaps are narrow (a missing lock, a missing mock method, a missing struct field) rather than missing subsystems. Resist the urge to redesign the federation sync architecture; patch the specific race and the specific parity/field gaps identified here.
Common Pitfalls
Pitfall 1: Unlocked concurrent read-modify-write on federation/nodes.json (PRIMARY SUSPECT for FED-01/FED-02)
What goes wrong: A federated node the operator removes reappears after "some sync cycles," with no error anywhere — exactly the symptom reported. federation::remove_node() and federation::sync::update_node_state() (called from sync_with_peer()) both do load_nodes() → mutate in memory → save_nodes() with zero mutex and a non-atomic fs::write(). Two async tasks (e.g., the 90s auto-sync loop mid-flight for peer X, and a federation.remove-node RPC for the same peer X arriving concurrently) can interleave: the sync task's load_nodes() snapshot (taken before the removal) still contains X; the removal completes and saves a list without X; the sync task then finishes and calls save_nodes() with its stale in-memory list, silently restoring X.
Why it happens: No Mutex/RwLock guards the federation JSON files, and there are TWO independent periodic sync loops (server.rs ~line 497, every 90s; ~line 840, every 1800s — the second loop's comment even says "every 30 min" while the interval literal is Duration::from_secs(1800), i.e. that arithmetic is correct but the redundancy with the 90s loop is not otherwise explained or justified anywhere in the code) plus manual "Sync All" and remove/join/set-trust RPCs, all writing the same file.
How to avoid: Add a per-data-dir tokio::sync::Mutex<()> (or an Arc<Mutex<Vec<FederatedNode>>> cache) that every federation::storage read-modify-write function acquires for the duration of its load+mutate+save; consider switching save_nodes to atomic temp-file+rename to avoid partial-write corruption on crash. Separately, evaluate collapsing the two periodic sync loops into one (the 90s loop already does everything the 1800s loop does, plus asymmetry self-heal) — the 1800s loop appears vestigial/redundant and doubles the race exposure for no described benefit.
Warning signs: tests/multinode/smoke.sh's "removed-node tombstone" section (already covers the transitive-reappear case) intermittently fails only under load/timing variance, or a removed node's last_seen timestamp updates after a federation.remove-node call succeeded — that's the race manifesting as reappearance without any logged error.
Pitfall 2: Trusting CONCERNS.md as current state for this phase
What goes wrong: Re-fixing an already-fixed bug (tombstone-write-swallowed was fixed in 01cbec27, 2026-07-02) wastes the FED-03 review's time and risks reintroducing a regression if the "fix" reverts working code.
Why it happens: CONCERNS.md was generated 2026-07-29 from a static codebase snapshot/analysis pass that in at least two documented cases (tombstone swallow, DID-join-without-verification) predates fixes already on main.
How to avoid: For every CONCERNS.md federation/mesh item, git log -p the referenced file/line range before deciding it's still open. Two items already verified fixed in this research: "Federation node removal tombstone gap" (fixed 01cbec27) and part of "Federation DID validation incomplete" (the peer-joined RPC does require and verify an ed25519 signature — handlers.rs:588-607). The remaining un-verified part of that concern — no proof-of-ownership check on the original DID mint, i.e. can anyone claim any DID string on first contact — may still be valid; verify it during the review rather than assuming either way.
Warning signs: A "finding" in the FED-03 review that exactly matches a CONCERNS.md bullet without a fresh code read is a signal to re-verify before filing it.
Pitfall 3: Demo mock silently no-ops instead of erroring on unmirrored RPCs
What goes wrong: mesh.contacts-list/mesh.contacts-save are called live from Mesh.vue (lines 113, 896) but have no case in mock-backend.js's switch — they fall through to the default case which returns a proper JSON-RPC error (Method not found), but the frontend call sites wrap them in try {} catch { /* non-fatal */ }, so the failure is invisible during manual demo testing unless you watch the browser console or server log (console.log('[RPC] Unknown method: ...')).
Why it happens: New frontend RPC call sites get added over time; mock-backend.js parity is manual and easy to miss for methods that aren't on the "main" flow (aliasing a peer is a secondary action, not part of onboarding/attach-file).
How to avoid: Grep neode-ui/src/api/rpc-client.ts for every mesh.*/federation.* method string and cross-reference against mock-backend.js's switch cases as an explicit FED-03/FED-04 checklist item, not just the attachment-send path already fixed.
Warning signs: Browser console shows [RPC] Unknown method: mesh.contacts-list while testing the demo at :8100.
Pitfall 4: ScreensaverRing's size classes don't have a "success-badge" variant
What goes wrong: Dropping <ScreensaverRing size="compact" /> directly into .send-success-burst (currently 112px) either overflows the card or looks disproportionate at 240-320px without adjusting the surrounding layout.
Why it happens: ScreensaverRing.vue's two size classes (viz-ring-default, viz-ring-compact) were designed for full-screen screensaver and settings-panel contexts (SystemDangerZone.vue), not for an inline modal success pane.
How to avoid: Either (a) wrap the component in a container with transform: scale(0.5) (112/240 ≈ 0.47) and compensate for the transform not affecting layout box size (use negative margins or a fixed wrapping box), or (b) add a third compact-sm/badge size variant to ScreensaverRing.vue sized for this use case (cleaner, and reusable for WalletScanModal.vue's .success-ring too). Confirm the choice with a UI-spec/sketch before implementation given this affects two components.
Warning signs: Visual QA on :8100 shows the ring clipped by the modal's max-h-[90vh] overflow-y-auto container or the checkmark badge floating disconnected from the ring's visual center.
Pitfall 5: FED-05 has no backend field for a peer's Lightning identity
What goes wrong: Building the "trusted nodes by hostname, one-click channel open" UI before the backend can supply a federated peer's LN pubkey/host:port results in a UI that can list names but has nothing to pass to lnd.openchannel.
Why it happens: NodeStateSnapshot (the payload federation.get-state/sync exchanges) has no Lightning fields at all — it was designed for app/CPU/mem/tor status, not payment-channel metadata.
How to avoid: Plan FED-05 backend-first: (1) extend handle_lnd_getinfo to parse and return identity_pubkey + uris from LND's real /v1/getinfo response (both fields already exist in LND's REST API — the daemon's LndGetInfoResponse struct just doesn't deserialize them yet), (2) add optional lightning_pubkey/lightning_uri fields to NodeStateSnapshot so a synced peer's info includes it (defaulted via #[serde(default)] for backward compat, matching every other optional field in that struct), (3) decide and scope the "public nodes" browse/request feature — no existing data source; recommend a small curated static list (documented, versioned) rather than a live LN graph query (DescribeGraph is heavy and not currently proxied anywhere in this codebase) unless the user specifically wants live graph browsing.
Warning signs: A plan step that starts building LightningChannelModal.vue before a corresponding backend RPC/field change is scoped — check the plan's task ordering.
Code Examples
Reuse: opening a channel (backend already correct)
// Source: core/archipelago/src/api/rpc/lnd/channels.rs:238-336 (excerpted)
// Params: { pubkey: <66-hex>, amount: <sats>, address?: <host:port>, private?, target_conf?, sat_per_vbyte? }
// Validates pubkey format + amount bounds (20,000..=16,777,215 sats) before touching LND.
// Connects to the peer synchronously (perm=false) before opening so "peer not online" is
// surfaced deterministically instead of racing the open.
Reuse: transport badge already ground-truth per peer
// Source: core/archipelago/src/federation/storage.rs:120-147
// record_peer_transport() writes last_transport/last_transport_at after every
// successful PeerRequest — the FED-05 peer picker can show "reachable via FIPS"
// / "reachable via Tor" per trusted node without any new plumbing.
Gap: demo mesh chat action stubs don't mutate state
// Source: neode-ui/mock-backend.js:4479-4490 (current — needs to become stateful)
case 'mesh.send-reaction':
case 'mesh.send-reply':
case 'mesh.send-read-receipt':
case 'mesh.edit-message':
case 'mesh.delete-message':
case 'mesh.forward-message':
case 'mesh.send-channel':
case 'mesh.refresh':
case 'mesh.reboot-radio': {
return res.json({ result: { ok: true, sent: true } }) // no meshStore.dynamic mutation
}
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
Tombstone write silently dropped (let _ = tombstone_did(...)) |
Tombstone write propagated as a hard error, written before the node-list save | 2026-07-02, 01cbec27 |
A failed tombstone write now fails the whole remove — matches FED-01's "a failed removal surfaces an error" requirement already, at the single-call level (the remaining gap is the cross-call race in Pitfall 1) |
| Mesh attachment send: demo threw "Method not found" and force-opened a demo-only chooser modal | mock-backend.js implements the same RPC surface + mirrors the real size-tier logic |
2026-07-29, c2ce71c6 |
FED-04's core attachment-parity requirement is met; remaining gaps are contacts and reaction/edit/delete (see Pitfall 3) |
Deprecated/outdated: None specific to this phase's tech; no framework/library version churn involved.
Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|---|---|---|
| A1 | The unlocked read-modify-write race on federation/nodes.json (Pitfall 1) is the primary cause of the user-reported "nodes reappear / sync issues" — this is a code-derived hypothesis, not confirmed via a reproduced failure in this research session |
Summary, Pitfall 1 | If wrong, the planner should still fix the race (it's a real bug regardless) but should budget review time for other causes too — start FED-03 with a broader look, not a narrow patch-and-close on this one hypothesis |
| A2 | The 1800s periodic federation sync loop (server.rs ~line 840) is redundant given the 90s loop and safe to remove/collapse |
Pitfall 1, Anti-Patterns | If a maintainer added it for a specific reason not documented in the surrounding comments (e.g. covering a case the 90s loop misses), removing it could regress that unstated behavior — confirm via git log -p / git blame on that block before deleting |
| A3 | "Public nodes" for channel-open browse/request (FED-05) should be a small curated static list rather than a live LN network graph query | Pitfall 5 | If the user actually wants live graph discovery, the curated-list approach under-delivers; this needs explicit user confirmation before FED-05 backend work starts |
| A4 | ScreensaverRing's size mismatch with .send-success-burst should be solved with a CSS scale-down rather than a new component size variant |
Pitfall 4 | Either approach works technically; scale-down is faster but may look slightly different under prefers-reduced-motion; a new size variant is cleaner but touches the shared component. Low risk either way — a UI sketch/spec pass can decide before implementation |
| A5 | The remaining "Federation DID validation incomplete" concern (no proof-of-ownership check on first DID mint) is still an open gap, not yet fixed like its sibling claims | Pitfall 2 | Not independently re-verified in this session (only the peer-joined signature check was confirmed); FED-03 should explicitly re-check this specific sub-claim before filing or dismissing it |
If this table is empty: N/A — see rows above.
Open Questions
-
Is the two-loop federation sync redundancy intentional?
- What we know: Both loops call
sync_with_peerover allTrusted/Observernodes; only the 90s loop does the "asymmetry self-heal"notify_joinre-assertion; the 1800s loop additionally callsrefresh_federation_mesh_peers()after its full pass (the 90s loop does not). - What's unclear: Whether the 1800s loop's
refresh_federation_mesh_peers()call covers a gap the 90s loop leaves (e.g. name/roster propagation to mesh chat), which would mean simply deleting it regresses something. - Recommendation:
git log -p/ blame both loop-insertion commits during FED-03; if the mesh-peer-refresh behavior is the only unique value of the 1800s loop, move that single call into the 90s loop's completion and delete the 1800s loop entirely, closing half the race window.
- What we know: Both loops call
-
What UI/UX should "browse/request channels with public nodes" (FED-05) actually look like?
- What we know: No existing data source;
lnd.openchannelsupports a manual pubkey+address entry today (a user could theoretically paste a public node's URI already, just with no picker/browse UI). - What's unclear: Whether "public nodes" means (a) a curated list Archipelago ships/updates, (b) a live query against some LSP directory API, or (c) simply a well-labeled manual-paste field with format help (lowest-effort, matches what the backend already supports).
- Recommendation: Flag for
/gsd-discuss-phaseor a direct user check-in before FED-05 planning — this is a scope decision, not a technical one.
- What we know: No existing data source;
-
Does
federation.get-state'sfederated_peershint list need a Lightning field, or should peer LN info be a separate on-demand RPC?- What we know:
NodeStateSnapshot.federated_peersalready carries a lightweightFederationPeerHint(did/pubkey/onion/name/fips_npub) shared during sync; addinglightning_urithere means every synced peer's LN info is cached locally without an extra round-trip. - What's unclear: Whether peers want to opt out of advertising their LN URI transitively (privacy consideration, similar to the existing
shared_locationopt-in pattern for lat/lon). - Recommendation: Follow the
shared_locationprecedent (Option<(f64,f64)>only sent when the node opts in viaserver.set-location) — add an explicit opt-in setting for Lightning URI sharing rather than defaulting it on, since exposing a payment channel target more broadly than intended has real-money implications.
- What we know:
Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|---|---|---|---|---|
Rust toolchain / cargo (from core/) |
FED-01/02/03/04 backend work | Not probed this session (assume present per CLAUDE.md build instructions) | — | — |
Node.js / npm (neode-ui/) |
FED-04/05/06 frontend + mock-backend.js work | Not probed this session (assume present per CLAUDE.md build instructions) | — | — |
:8100 dev preview proxying to archi-dev |
FED-05/06 required verification step per phase description | Not probed this session — verify at execution time per docs/../reference_neode_ui_dev_testing.md (mock=5959, pw password123) |
— | — |
LND REST API (LND_REST_BASE_URL, local macaroon) |
FED-05 lnd.getinfo extension + lnd.openchannel reuse |
Assumed present on real nodes per existing channels.rs/info.rs code; demo backend has no real LND — FED-05 UI must be exercised against archi-dev (real LND) per phase description, not the pure-mock demo |
— | Demo-only mock stub for lnd.getinfo identity fields if archi-dev is unavailable during a work session |
tests/multinode/smoke.sh |
Regression coverage for FED-01/02 fix | Present, already covers removed-node tombstone + transitive-reappear scenarios; does NOT currently exercise the concurrent-race scenario (Pitfall 1) | — | Extend smoke.sh with a concurrent remove+sync test, or add a Rust-level #[tokio::test] in federation/storage.rs that spawns concurrent remove/save calls |
Missing dependencies with no fallback: None identified — this phase is code-only, no new external services.
Missing dependencies with fallback: LND-backed FED-05 verification (see row above) — use archi-dev per phase instructions; demo-only stubbing is a fallback if archi-dev is temporarily unavailable, but the phase's own success criteria require verification against archi-dev before deploy, so this fallback should not be treated as sufficient sign-off.
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework (backend) | cargo test (Rust, workspace root core/) — federation/storage.rs and federation/sync.rs already have #[tokio::test] unit coverage |
| Framework (frontend) | Vitest (neode-ui/vitest.config.ts, vitest run) |
| Config file | core/Cargo.toml (workspace); neode-ui/vitest.config.ts |
| Quick run command | cd core && cargo test -p archipelago federation:: --lib (backend); cd neode-ui && npx vitest run src/components/__tests__/ (frontend, scope to touched files) |
| Full suite command | cd core && CARGO_INCREMENTAL=0 cargo test (backend, full); cd neode-ui && npm run test (frontend, full); tests/multinode/smoke.sh (cross-node, requires 2+ live nodes, run on-node per CLAUDE.md gate policy) |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| FED-01 | Removed node never reappears, incl. under concurrent sync | unit + integration | cargo test -p archipelago federation::storage::tests (existing) + NEW concurrent-race test |
✅ existing tests / ❌ Wave 0 for the new race test |
| FED-01 | Failed removal surfaces an error, not a silent no-op | unit | cargo test -p archipelago federation::storage::tests::test_remove_nonexistent_node_errors (existing, covers the "not found" case; add one for a simulated tombstone-write I/O failure) |
✅ existing / ❌ Wave 0 for I/O-failure case |
| FED-02 | Sync converges; fleet nodes agree on node list | integration (multinode) | tests/multinode/smoke.sh section "federation pairing" + "removed-node tombstone" (existing) |
✅ |
| FED-02 | Sync errors are operator-visible | manual / UI | No automated test yet — requires a UI element (e.g. per-node "last sync error" badge) that doesn't exist yet | ❌ Wave 0 (needs the field to exist first) |
| FED-03 | Structured review findings fixed or deferred with reason | N/A (process requirement) | N/A — tracked via the plan's findings list, not a single automated test | — |
| FED-04 | Attachment send parity demo vs real | manual (visual) + existing mock-backend.js logic mirrors daemon tier thresholds |
Manual walk-through on :8100 per phase description; consider a Vitest test asserting mesh.transport-advice tier boundaries match MESH_AUTO_MAX/MESH_HARD_MAX constants |
❌ Wave 0 (no existing frontend test pins these thresholds) |
| FED-04 | Contacts list/save + reaction/edit/delete parity | manual + NEW mock-backend.js stateful behavior | Manual on :8100; no existing automated coverage of mock-backend.js behavior (it's a dev tool, not covered by npm run test) |
❌ Wave 0 if automated coverage is wanted; otherwise manual-only is acceptable for a demo shim |
| FED-05 | Own node Lightning URI is shareable | unit (backend) + manual (UI) | NEW cargo test for handle_lnd_getinfo's identity_pubkey/uris parsing (mock LND response fixture); manual UI check on archi-dev |
❌ Wave 0 |
| FED-05 | Trusted-node picker + channel open flow | manual (UI, requires archi-dev + a live peer) | Manual per phase description ("tested live on the :8100 dev preview against archi-dev") | ❌ Wave 0 — inherently a live/manual check per the phase's own success criteria |
| FED-06 | Paid-tick animation matches screensaver ring everywhere it appears | manual (visual) | Manual visual check of SendBitcoinModal.vue + WalletScanModal.vue on :8100 |
❌ Wave 0 — visual-only requirement, no meaningful automated assertion beyond "component renders" |
Sampling Rate
- Per task commit: Backend:
cargo test -p archipelago federation:: mesh::(scoped). Frontend:npx vitest runscoped to touched component test files, or a full quick run if none exist yet for touched files. - Per wave merge: Full
cargo test(backend) +npm run test(frontend). - Phase gate: Full backend + frontend suites green, plus
tests/multinode/smoke.shfederation sections green on a real 2-node pair, plus a manual FED-05/FED-06 walkthrough on:8100against archi-dev before any deploy (per phase description, "fixed there before any deploy").
Wave 0 Gaps
- New
#[tokio::test]incore/archipelago/src/federation/storage.rs(or a new integration test) that spawns concurrentremove_node()+update_node_state()/sync_with_peer-equivalent calls against the samedata_dirand asserts the removed node stays removed — this is the regression test for Pitfall 1 and does not exist today. - Test/fixture for
handle_lnd_getinfoparsingidentity_pubkey/urisfrom a mocked LND/v1/getinfoJSON response (FED-05) — no existing test touches this handler's response shape. - Decide whether
mock-backend.jsbehavior warrants automated (Vitest/Playwright-against-mock) coverage, or manual-only is acceptable given it's a dev-preview tool, not shipped code — recommend manual-only unless the team already has a pattern for testing the mock backend elsewhere (none found in this research). - Framework install: none — all frameworks (
cargo test, Vitest) are already configured and running.
Security Domain
Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---|---|---|
| V2 Authentication | Partial | Federation peer identity is DID/ed25519-key-based, not password auth; peer-joined/peer-did-changed/peer-address-changed all require and verify an ed25519 signature over a canonical message before mutating state — already correct, verify no new RPC bypasses this |
| V3 Session Management | No | Not applicable — federation/mesh RPCs are peer-signed, not session-cookie based |
| V4 Access Control | Yes | is_peer_allowed_path() (server.rs:1270, tested at server.rs:2075+) restricts which HTTP paths a peer-only listener will serve — any new FED-05 RPC (e.g. "fetch peer's Lightning URI") that's meant to be peer-reachable must be added to this allow-list explicitly, not left to fall through |
| V5 Input Validation | Yes | lnd.openchannel already validates pubkey format (66-hex) and amount bounds server-side (channels.rs:252-268) — reuse, and apply the same rigor to any new Lightning-URI-sharing field (validate the URI format before persisting/displaying it) |
| V6 Cryptography | Yes | ed25519 signature verification via identity::NodeIdentity::verify — never hand-roll signature checks; reuse this existing verification path if FED-05 needs to authenticate a peer's advertised Lightning info |
Known Threat Patterns for this stack
| Pattern | STRIDE | Standard Mitigation |
|---|---|---|
| Federation peer spoofing a DID they don't control | Spoofing | ed25519 signature verification (already implemented for join/address-change/did-rotation — confirm any new FED-05 peer-info exchange follows the same pattern) |
| Concurrent-write race corrupting/reverting federation state (Pitfall 1) | Tampering (unintentional, but security-relevant since it undermines the "removal sticks" guarantee — a removed/untrusted peer regaining federation membership is a real access-control regression) | Add locking around the storage layer (see Pitfall 1's fix) |
| Unbounded transitive federation exposure (a Trusted peer's peer list auto-added as Observer) | Elevation of Privilege (bounded) | Already mitigated — merge_transitive_peers only runs for Trusted-level sources and only adds new peers as Observer (never auto-escalates to Trusted); this is intentional and correct, don't loosen it |
| Advertising this node's Lightning payment-channel target more broadly than intended (FED-05 new surface) | Information Disclosure | Follow the existing shared_location opt-in pattern — do not default Lightning URI sharing to "on" for all federated peers (see Open Question 3) |
Sources
Primary (HIGH confidence)
core/archipelago/src/federation/storage.rs,sync.rs,types.rs,invites.rs— read directly, currentmaincore/archipelago/src/api/rpc/federation/handlers.rs— read directly, currentmaincore/archipelago/src/server.rs(periodic sync loop sections,is_peer_allowed_path) — read directlycore/archipelago/src/mesh/mod.rs(purge_federation_peer,upsert_federation_peer,seed_federation_peers_into_mesh) — read directlycore/archipelago/src/api/rpc/lnd/channels.rs,info.rs— read directlycore/archipelago/src/fips/dial.rs— read directlyneode-ui/mock-backend.js(mesh RPC switch cases) — read directly, currentmain(post commitc2ce71c6)neode-ui/src/views/Federation.vue,neode-ui/src/api/rpc-client.ts,neode-ui/src/components/ScreensaverRing.vue,neode-ui/src/components/Screensaver.vue,neode-ui/src/components/SendBitcoinModal.vue,neode-ui/src/components/WalletScanModal.vue,neode-ui/src/views/Mesh.vue— read directlygit log -poncore/archipelago/src/federation/storage.rs(commit01cbec27) andgit show c2ce71c6— verified fix history directly, not from documentationtests/multinode/smoke.sh— read directly for existing federation test coverage.planning/REQUIREMENTS.md,.planning/STATE.md,.planning/codebase/ARCHITECTURE.md,.planning/codebase/CONCERNS.md— project-provided context (CONCERNS.md's federation claims were then verified/refuted against live code per Pitfall 2)
Secondary (MEDIUM confidence)
- None — this research relied entirely on direct codebase reads and git history, not external web sources, since the phase is about hardening this specific project's existing code rather than adopting new external technology.
Tertiary (LOW confidence)
- None.
Metadata
Confidence breakdown:
- Standard stack: HIGH — no new dependencies; existing stack confirmed by direct file reads
- Architecture (FED-01/02/03/04): HIGH — read the actual implementation, confirmed fix history via git log, identified a concrete unverified race condition with file:line citations
- Architecture (FED-05): MEDIUM — greenfield UI/RPC surface; confirmed what's missing (no existing Lightning-URI field/RPC) but the design (opt-in sharing, public-nodes scope) needs a user decision, not just engineering judgment
- Pitfalls: HIGH for Pitfalls 1-3 (backend/demo, code-verified); MEDIUM for Pitfalls 4-5 (frontend sizing and FED-05 scope, judgment calls flagged in Assumptions Log)
Research date: 2026-07-29 Valid until: 2026-08-12 (14 days — this is a fast-moving area of an actively-developed codebase; other agents were committing federation/mesh-adjacent changes during this very research session, per the mock-backend.js commit observed mid-session)