archy/.claude/plans/twinkly-baking-ladybug.md

206 lines
9.3 KiB
Markdown
Raw Normal View History

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
# 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`
```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`