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>
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 wordsMasterSeed::from_mnemonic(mnemonic) -> MasterSeed— for restoreMasterSeed::from_mnemonic_words(words: &str) -> Result<(Mnemonic, MasterSeed)>— parse + validatederive_node_ed25519(&MasterSeed) -> SigningKey— HKDF with infoarchipelago/node/ed25519/v1derive_identity_ed25519(&MasterSeed, index: u32) -> SigningKey— HKDF with infoarchipelago/identity/{index}/ed25519/v1derive_nostr_identity_key(&MasterSeed, index: u32) -> nostr_sdk::Keys— BIP-32m/44'/1237'/0'/0/{index}derive_node_nostr_key(&MasterSeed) -> nostr_sdk::Keys— HKDF with infoarchipelago/nostr-node/secp256k1/v1derive_bitcoin_xprv(&MasterSeed) -> Xpriv— BIP-32m/84'/0'/0'derive_lnd_entropy(&MasterSeed) -> [u8; 16]— HKDF with infoarchipelago/lnd/entropy/v1save_seed_encrypted(data_dir, mnemonic, passphrase)— Argon2+ChaCha20 tomaster_seed.encload_seed_encrypted(data_dir, passphrase) -> Mnemonicseed_exists(data_dir) -> boolsave_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 implementedbackup/identity.rsencryption pattern — Argon2+ChaCha20 (reuse forsave_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 fromderive_nostr_identity_key(seed, index) - Increment and persist
identity_index - Add
derivation_index: Option<u32>toIdentityFile(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: booltoServerInfo
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 tomaster_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 initseed.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-seedhandler — derives 16-byte entropy, calls LND RESTPOST /v1/initwalletwithseed_entropy- Triggered during LND first-install flow
Bitcoin Core wallet from seed:
bitcoin.init-wallet-from-seedhandler — derives BIP-84 xprv, callscreatewallet+importdescriptorsvia 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
- Unit tests: Deterministic derivation (same mnemonic -> same keys), invalid mnemonic rejection, index increment, zeroization
- Integration: Fresh install flow end-to-end, restore flow (generate on node A, enter words on node B, verify same DID/npub)
- Security: Grep seed.rs for tracing macros that interpolate seed vars, verify file permissions
- LND: Derive entropy, init wallet, verify deterministic aezeed
- Bitcoin Core: Derive xprv, import descriptors, verify addresses match
- Legacy: Existing node without seed starts normally, can still create identities
- Type check:
cd neode-ui && npx vue-tsc -b --noEmit