archy/.claude/plans/twinkly-baking-ladybug.md
Dorian 1e283daf13 fix: overhaul container lifecycle — recovery, health, uninstall, UI state
Container recovery:
- Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s
- Dependency-aware restarts: won't restart services before their deps
- Reset dependent counters when a dependency recovers
- Handle "created" state containers (were invisible to health monitor)
- Added IndeedHub, mempool-api, mysql to tier system
- Crash recovery: podman start timeout 30s→120s with retry
- Podman client: socket timeout 5s→30s, added restart policy

UI state representation:
- Exit code 0 shows "stopped" (gray), not "crashed" (red)
- Exit code 137 shows "killed (OOM)"
- Non-zero exit shows "crashed" (red)
- Added exit_code field to PackageDataEntry

Install/uninstall fixes:
- Install returns error when container doesn't start (was silent success)
- Post-install hooks awaited instead of fire-and-forget tokio::spawn
- Uninstall: graceful rm before force, volume prune, network cleanup
- Uninstall returns error on partial failure (was 200 OK)

Config consistency:
- DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded)
- Bitcoin: added ZMQ ports 28332/28333 for LND block notifications
- IndeedHub port 7777→8190 (was conflicting with strfry)
- Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0

Performance:
- Metrics collector interval 60s→300s (was duplicating health monitor)
- Podman client: proper error propagation instead of unwrap_or_default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:03:57 +01:00

9.3 KiB

BIP-39 Master Seed — Unified Key Derivation for Archipelago

Context

Archipelago's current identity system is broken:

  • Node key generated randomly at boot, before the user exists
  • Each identity creates independent random Ed25519 + secp256k1 keys
  • ADR-008 says "both keys derived from same master seed" but code doesn't do this
  • Backup only covers the node key, not identity keys
  • No seed phrase — backup is an opaque encrypted blob with a user passphrase
  • Restore path disabled ("Coming Soon")
  • No connection between node identity and Bitcoin/LND wallet keys

Goal: One 24-word BIP-39 seed phrase derives ALL keys. User writes down 24 words, can recover everything on a fresh install.


Derivation Scheme

BIP-39 Mnemonic (24 words, 256-bit entropy)
  -> PBKDF2-HMAC-SHA512 (2048 rounds, empty passphrase)
  -> Master Seed (64 bytes)
     |
     +-- HKDF-SHA256(seed, info="archipelago/node/ed25519/v1")         -> Node Ed25519 key -> did:key
     +-- HKDF-SHA256(seed, info="archipelago/nostr-node/secp256k1/v1") -> Node Nostr key
     +-- HKDF-SHA256(seed, info="archipelago/identity/{i}/ed25519/v1") -> Identity i Ed25519 -> did:key
     +-- BIP-32 m/44'/1237'/0'/0/{i}                                   -> Identity i Nostr key (NIP-06)
     +-- BIP-32 m/84'/0'/0'                                            -> Bitcoin Core wallet (native segwit)
     +-- HKDF-SHA256(seed, info="archipelago/lnd/entropy/v1")          -> 16 bytes -> LND aezeed entropy

Phase 1: Seed Module (foundation)

New crates in core/archipelago/Cargo.toml

bip39 = "=2.1.0"
bitcoin = { version = "=0.32.5", features = ["rand-std"] }

New file: core/archipelago/src/seed.rs

MasterSeed struct — wraps Zeroizing<[u8; 64]>, implements ZeroizeOnDrop

Functions:

  • MasterSeed::generate() -> (Mnemonic, MasterSeed) — 256-bit entropy, 24 words
  • MasterSeed::from_mnemonic(mnemonic) -> MasterSeed — for restore
  • MasterSeed::from_mnemonic_words(words: &str) -> Result<(Mnemonic, MasterSeed)> — parse + validate
  • derive_node_ed25519(&MasterSeed) -> SigningKey — HKDF with info archipelago/node/ed25519/v1
  • derive_identity_ed25519(&MasterSeed, index: u32) -> SigningKey — HKDF with info archipelago/identity/{index}/ed25519/v1
  • derive_nostr_identity_key(&MasterSeed, index: u32) -> nostr_sdk::Keys — BIP-32 m/44'/1237'/0'/0/{index}
  • derive_node_nostr_key(&MasterSeed) -> nostr_sdk::Keys — HKDF with info archipelago/nostr-node/secp256k1/v1
  • derive_bitcoin_xprv(&MasterSeed) -> Xpriv — BIP-32 m/84'/0'/0'
  • derive_lnd_entropy(&MasterSeed) -> [u8; 16] — HKDF with info archipelago/lnd/entropy/v1
  • save_seed_encrypted(data_dir, mnemonic, passphrase) — Argon2+ChaCha20 to master_seed.enc
  • load_seed_encrypted(data_dir, passphrase) -> Mnemonic
  • seed_exists(data_dir) -> bool
  • save_identity_index(data_dir, next_index: u32) / load_identity_index(data_dir) -> u32

Security: Never log seed/mnemonic. All seed types implement ZeroizeOnDrop. File permissions 0o600.

Existing building blocks to reuse:

  • mesh/crypto.rs:hkdf_sha256() / hkdf_sha256_32() — already implemented
  • backup/identity.rs encryption pattern — Argon2+ChaCha20 (reuse for save_seed_encrypted)
  • ed25519-dalek, sha2, hmac, hkdf, zeroize — all in Cargo.toml already

Phase 2: Onboarding UI

New Vue views:

OnboardingSeedGenerate.vue — calls seed.generate, displays 24 words in grid, "I wrote these down" checkbox

OnboardingSeedVerify.vue — picks 4 random word positions, user types them back, calls seed.verify, shows DID + npub on success

OnboardingSeedRestore.vue — 24 input fields with BIP-39 wordlist autocomplete, calls seed.restore

New onboarding flow:

Intro -> Options (Fresh / Restore) -> [branch]

FRESH:  SeedGenerate -> SeedVerify -> Identity (name/purpose) -> Done
RESTORE: SeedRestore -> Done

Router changes (neode-ui/src/router/index.ts):

  • Add routes: onboarding/seed, onboarding/seed-verify, onboarding/seed-restore
  • Remove: onboarding/did, onboarding/backup, onboarding/verify
  • Enable Restore path in OnboardingOptions.vue

RPC client (neode-ui/src/api/rpc-client.ts):

  • generateSeed(), verifySeed(), restoreSeed(), saveSeedEncrypted(), seedStatus()

Phase 3: Backend Integration

identity.rs — add NodeIdentity::from_seed(identity_dir, &MasterSeed)

  • Derives Ed25519 node key via seed::derive_node_ed25519()
  • Writes to node_key / node_key.pub (same format as today)
  • Existing load_or_create() unchanged (loads from disk, works for both seed-derived and legacy keys)

identity_manager.rs — seed-aware create()

  • When seed available: derive Ed25519 from derive_identity_ed25519(seed, index), Nostr from derive_nostr_identity_key(seed, index)
  • Increment and persist identity_index
  • Add derivation_index: Option<u32> to IdentityFile (serde default, backward-compatible)
  • When no seed (legacy): fall back to current random generation

server.rs — startup flow:

seed exists + node_key exists  -> Normal seed-backed operation
no seed + node_key exists      -> Legacy node, show migration prompt
no seed + no node_key          -> Fresh install, await onboarding
seed exists + no node_key      -> Re-derive from seed (recovery)
  • Add seed_backed: bool to ServerInfo

New RPC endpoints in api/rpc/seed.rs:

  • seed.generate — generates mnemonic, derives & writes node keys, returns words (onboarding only, unauth)
  • seed.verify — validates user re-entered correct words (onboarding only)
  • seed.restore — accepts 24 words, derives all keys, writes to disk (onboarding only, unauth)
  • seed.save-encrypted — encrypts mnemonic to master_seed.enc (optional convenience)
  • seed.status — returns { has_seed, is_legacy, identity_count, next_index }
  • seed.derive-lnd-entropy — password-protected, returns 16 bytes for LND wallet init
  • seed.derive-bitcoin-xprv — password-protected, returns xprv for Bitcoin Core import

In-memory mnemonic between seed.generate and seed.verify: held in Mutex<Option<Zeroizing<String>>> with 10-minute auto-clear timeout.


Phase 4: Bitcoin/LND Integration

LND wallet from seed:

  • lnd.init-wallet-from-seed handler — derives 16-byte entropy, calls LND REST POST /v1/initwallet with seed_entropy
  • Triggered during LND first-install flow

Bitcoin Core wallet from seed:

  • bitcoin.init-wallet-from-seed handler — derives BIP-84 xprv, calls createwallet + importdescriptors via Bitcoin Core RPC
  • Triggered during Bitcoin Core first-install flow

Both endpoints require password re-verification.


Phase 5: Migration & Polish

Legacy node migration:

  • Detect legacy nodes (node_key exists, no master_seed.enc)
  • Settings page shows prompt: "Set up seed phrase to protect future identities"
  • Existing keys preserved — only NEW identities use seed derivation
  • Optional full migration (seed.migrate-legacy) can be added later

Cleanup:

  • Remove old OnboardingDid.vue, OnboardingBackup.vue, OnboardingVerify.vue
  • Update Settings backup section to show seed status
  • Update ADR-008 to reflect implementation matches description

File Layout After Implementation

{data_dir}/identity/
  node_key              # 32 bytes Ed25519 secret (derived from seed or legacy)
  node_key.pub          # 32 bytes Ed25519 public
  master_seed.enc       # NEW: encrypted mnemonic (optional convenience backup)
  identity_index        # NEW: next derivation index (plain text integer)
{data_dir}/identities/
  {uuid}.json           # Same format + optional derivation_index field

Critical Files to Modify

File Change
core/archipelago/Cargo.toml Add bip39, bitcoin crates
core/archipelago/src/seed.rs NEW — all seed logic
core/archipelago/src/identity.rs Add from_seed() constructor
core/archipelago/src/identity_manager.rs Seed-aware create(), add derivation_index
core/archipelago/src/server.rs Startup state detection (seed/legacy/fresh)
core/archipelago/src/api/rpc/seed.rs NEW — seed RPC handlers
core/archipelago/src/api/rpc/dispatcher.rs Register seed.* endpoints
neode-ui/src/views/OnboardingSeedGenerate.vue NEW — show 24 words
neode-ui/src/views/OnboardingSeedVerify.vue NEW — verify written words
neode-ui/src/views/OnboardingSeedRestore.vue NEW — enter 24 words to restore
neode-ui/src/views/OnboardingOptions.vue Enable Restore path
neode-ui/src/router/index.ts Update onboarding routes
neode-ui/src/api/rpc-client.ts Add seed RPC methods

Verification

  1. Unit tests: Deterministic derivation (same mnemonic -> same keys), invalid mnemonic rejection, index increment, zeroization
  2. Integration: Fresh install flow end-to-end, restore flow (generate on node A, enter words on node B, verify same DID/npub)
  3. Security: Grep seed.rs for tracing macros that interpolate seed vars, verify file permissions
  4. LND: Derive entropy, init wallet, verify deterministic aezeed
  5. Bitcoin Core: Derive xprv, import descriptors, verify addresses match
  6. Legacy: Existing node without seed starts normally, can still create identities
  7. Type check: cd neode-ui && npx vue-tsc -b --noEmit