Files
archy/docs/dual-ecash-design.md
T
archipelagoandClaude Opus 5 8e814ca06a
Demo images / Build & push demo images (push) Failing after 2m22s
feat(registry): move image and OTA references to the public domain
Replaces the registry host across 86 files: 309 references, covering all 40
app manifests, the orchestrator and container crates, the release and catalog
scripts, both demo-images workflows, the ISO builder, demo-deploy, and the
frontend marketplace data.

Verified the domain actually serves the registry before rewriting anything,
rather than assuming the web host implies the registry:
- TLS verifies clean, HTTP/2 on the web root
- an anonymous token grants a manifest fetch (HTTP 200) with no credentials
- skopeo inspect --no-creds resolves an image and lists its tags

That last check is the one that matters: an outside developer with no account
can now pull, which was the functional blocker for publishing at all.

Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop
crossing the network in the clear.

Deliberately NOT rewritten:
- The public FIPS anchor on port 8444. It is a functional network endpoint
  every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded
  seeds than to leaked infrastructure. The domain does resolve to the same
  host, so it could become a hostname, but that adds a DNS dependency to the
  path used precisely when things are broken. Worth a deliberate decision,
  not a side effect of this change.
- The companion APK on port 2100. The domain returns 404 for that path, so
  rewriting it would swap a working URL for a broken one. The Releases page
  does serve (200), which is where the plan already wants those binaries.
- releases/app-catalog.json, releases/manifest.json and release-manifest.json.
  These carry `signature` and `signed_by`; editing their contents invalidates
  the signature and the fleet refuses artifacts that fail verification. They
  were rewritten in a first pass and reverted — they must be regenerated and
  re-signed through the signing ceremony instead, which needs the mnemonic.

So the catalog still advertises the old host until that ceremony runs. Nodes
resolve images through the signed catalog, not the on-disk manifests, so this
commit alone does not change what a node pulls.

Verified: archipelago-container 75/75; every manifest still parses with a
top-level app block; no signed artifact modified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 11:31:20 -04:00

7.9 KiB

Dual-ecash: Cashu + Fedimint, seamlessly

Status: in progress (2026-06-17). FE scaffolding + Fedimint HTTP bridge landed and compile-checked; live federation round-trip and networking-sats routing are not yet validated.

Why

Today the node's wallet (core/archipelago/src/wallet/ecash.rs, mint_client.rs, cashu.rs) speaks only the Cashu NUT HTTP protocol (BDHKE, cashuA… tokens). There is no Fedimint clientapps/fedimint is only the guardian server, and the "local Fedimint" default mint at 127.0.0.1:8175 is just the guardian UI nginx, which does not expose the Cashu NUT API. So:

  • The node can hold/spend generic Cashu tokens, but cannot hold Fedimint ecash or join federations.
  • "Networking sats" (streaming/seeding revenue) is hardcoded to the Cashu wallet.

Goal: support both ecash protocols seamlessly — hold balances in either, join arbitrary federations, and let networking-sats be paid/received over whichever protocol the peer accepts.

Architecture decision

Containerized fedimint-clientd + thin HTTP bridge (chosen over linking the native fedimint-client Rust SDK into the binary, and over a Lightning-only bridge).

archipelago binary
   ├─ CashuMintClient  ──HTTP (NUT /v1/*)──▶  cashu mint
   └─ FedimintClient   ──REST (/v2/*)─────▶  fedimint-clientd container ──▶ federation guardians

Rationale: keeps the heavy, fast-moving Fedimint SDK out of the main binary (no compile-time coupling, no rebuild bloat, OTA-friendly), and fits the existing app/container architecture (apps/fedimint, apps/fedimint-gateway). The Rust side is just a reqwest client, mirroring MintClient.

fedimint-clientd REST surface (v0.3.x)

  • Auth: Authorization: Bearer <password>. Default port 8080 (we map it to host 8178 because 8080 is LND REST). Base path /v2/....
  • GET /v2/admin/info — per-federation balances (totalAmountMsat, denominations, meta).
  • POST /v2/admin/join{ "inviteCode": "fed1…", "useManualSecret": false } → joins / returns federationId.
  • POST /v2/mint/spend{ federationId, amountMsat } → serialized notes (ecash to send).
  • POST /v2/mint/reissue{ federationId, notes } → redeem received notes; returns reissued amount.
  • POST /v2/ln/invoice / POST /v2/ln/pay — Lightning in/out (used for cross-protocol swaps).
  • GET /health.

Multi-federation: requests carry a federationId; clientd's multimint manages many clients. Exact JSON field names must be pinned to the clientd image tag we vendor — code defensively.

Components

1. Container app — apps/fedimint-clientd/manifest.yml

Sidecar running fedimint-clientd, mirroring apps/fedimint-gateway. Host port 8178 → container 8080. Password from node secret fedimint-clientd-password. State volume at /var/lib/archipelago/fedimint-clientd. Added to RESERVED_PORTS (port_allocator.rs) and fallback_package_port() (server.rs).

2. Rust bridge — core/archipelago/src/wallet/fedimint_client.rs

Thin reqwest client: info(), join(), spend(), reissue(), ln_invoice(), ln_pay(). from_node(data_dir) resolves base URL + password (env FEDIMINT_CLIENTD_URL / FEDIMINT_CLIENTD_PASSWORD, else defaults + secret file). Tor-proxy support via with_client, mirroring MintClient.

3. RPCs — core/archipelago/src/api/rpc/fedimint.rs

  • wallet.fedimint-list → joined federations + balances ({federation_id, name, balance_sats}[]).
  • wallet.fedimint-join {invite_code} → joins via clientd, persists to wallet/fedimint_federations.json, returns {federation_id}.
  • wallet.fedimint-leave {federation_id} → untracks locally.
  • wallet.fedimint-balance → total sats across federations (from clientd info).

Local registry wallet/fedimint_federations.json = { federations: [{federation_id, name}] } so the list survives clientd being temporarily down; balances are live from clientd.

4. Frontend — WalletSettingsModal.vue

Tabbed: Cashu Mints (live: streaming.list-mints / streaming.configure-mints) and Fedimint Federations (wallet.fedimint-list / -join / -leave). Gear icon on HomeWalletCard. fedimintBackendReady flips to true once the RPCs ship; join degrades gracefully with a clear error if the clientd app isn't installed.

4b. Default federation (zero-touch)

clientd auto-joins a default federation at boot via FEDIMINT_CLIENTD_INVITE_CODE (manifest), and the Rust bridge ensure_default_federation() idempotently joins + tracks it (called from wallet.fedimint-list) so already-running nodes pick it up too. Constant DEFAULT_FEDERATION_INVITE in fedimint_client.rs is the single source on the Rust side; keep it in sync with the manifest env. clientd is a client, not the guardian — it needs no local fedimintd, so it bundles standalone.

4c. Bundling on every node

  • Bundled ISO: add image to scripts/image-versions.sh, the ISO bundle .tar list, and a core-create block in scripts/first-boot-containers.sh; mark tier: "core" in app-catalog/catalog.json.
  • Unbundled ISO: first-boot-containers.sh exits after FileBrowser only — add clientd to that early-exit block so unbundled nodes also get it out of the box.
  • CAVEAT: confirm the current ISO assembler before editing the bundle list — the one found is under image-recipe/_archived/ (likely stale); first-boot-containers.sh/image-versions.sh are current.
  • Image: build from source (no official image; flake.nix only) → push to vps2 source.archipelago-foundation.org/lfg2025/fedimint-clientd:v0.4.0.

5. Unified balance

HomeWalletCard ecash row = Cashu wallet.ecash-balance + Fedimint wallet.fedimint-balance. (Home already calls wallet.ecash-balance; add fedimint and sum.)

Networking sats — dual protocol (phase 6, NOT yet wired)

The economic layer (streaming.rs, streaming/gate.rs, sessions, pricing, metering) is already protocol-agnostic — it just calls into the wallet. The injection points are:

  1. Protocol-tag accepted mints. accepted_mints: Vec<String> → carry protocol, e.g. cashu:https://mint… / fedimint:<federation_id>. Migrate wallet/accepted_mints.json with a back-compat reader (bare URL ⇒ cashu:).
  2. MintClient::new() is the bottleneck (~10 call sites). Introduce a MintBackend trait with CashuBackend (wraps current code) and FedimintBackend (calls FedimintClient).
  3. Token enum Cashu(CashuToken) | Fedimint(notes); serialize/verify by variant.
  4. build_payment_token() picks a (backend, id) the peer accepts; verify_and_receive_payment() auto-detects the token variant and reissues/swaps on the right backend.
  5. Cross-protocol settlement (Cashu↔Fedimint) bridges over Lightning (BOLT11) — both sides already have mint/melt (Cashu) and ln/invoice+ln/pay (Fedimint).
  6. Web5NetworkingProfitsSettings.vue: per-service payout protocol/mint selector.

Phases

  • P0 FE tabbed Wallet Settings modal + gear (Cashu live, Fedimint tab structured).
  • P1 fedimint-clientd container manifest + ports.
  • P2 FedimintClient HTTP bridge + wallet.fedimint-* RPCs (compiles).
  • P3 Validate join / balance / spend / reissue against a live clientd + real federation on a scratch node.
  • P4 Unified ecash balance in the wallet card (Cashu + Fedimint).
  • P5 Flip FE fully live; surface "install Fedimint client app" when clientd unreachable.
  • P6 Networking-sats dual-protocol routing (the MintBackend/Token refactor above).

Validation (per project testing discipline)

clientd image + a real federation are required; cannot be validated from the dev tree. Validate on a scratch node: install Fedimint app + clientd, join a known test federation, confirm wallet.fedimint-balance, then a spend→reissue round-trip between two nodes, then networking-sats payment over Fedimint. Heavy/iterative work belongs in a worktree (see CLAUDE.md memory).