feat: BIP-39 master seed for unified key derivation

Replace fragmented random key generation with a single 24-word BIP-39
mnemonic that deterministically derives all node keys: Ed25519 (DID),
secp256k1 (Nostr/Bitcoin), BIP-84 xprv (Bitcoin Core), and LND aezeed
entropy. New onboarding flow: seed generate → word verification → identity
naming. Restore path enabled via 24-word entry. Includes seed RPC handlers,
mock backend support, LND/Bitcoin Core wallet-from-seed integration, and
UI polish across settings and discover views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-31 01:41:24 +01:00
co-authored by Claude Opus 4.6
parent 5da9e217e6
commit 19dcfd4f31
50 changed files with 2200 additions and 258 deletions
+114
View File
@@ -1,6 +1,7 @@
use super::RpcHandler;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
#[derive(Debug, Serialize)]
struct BitcoinInfo {
@@ -106,4 +107,117 @@ impl RpcHandler {
.result
.ok_or_else(|| anyhow::anyhow!("Bitcoin RPC returned null result"))
}
/// Initialize a Bitcoin Core descriptor wallet with keys derived from the master seed.
/// Creates a blank wallet and imports BIP-84 (native segwit) descriptors.
/// Requires: password re-verification, encrypted seed on disk.
pub(super) async fn handle_bitcoin_init_wallet_from_seed(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let password = params.get("password")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'password' for seed access"))?;
let wallet_name = params.get("wallet_name")
.and_then(|v| v.as_str())
.unwrap_or("archipelago");
// Verify user password.
self.auth_manager.verify_password(password).await
.context("Password verification failed")?;
// Load encrypted seed.
let mnemonic = crate::seed::load_seed_encrypted(&self.config.data_dir, password).await
.context("Failed to load encrypted seed")?;
let seed = crate::seed::MasterSeed::from_mnemonic(&mnemonic);
// Derive BIP-84 account xprv.
let xprv = crate::seed::derive_bitcoin_xprv(&seed)?;
let mut xprv_str = xprv.to_string();
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.context("Failed to create HTTP client")?;
// Step 1: Create a blank descriptor wallet.
let create_result = self.bitcoin_rpc_call::<serde_json::Value>(
&client,
"createwallet",
&[
serde_json::json!(wallet_name), // wallet_name
serde_json::json!(false), // disable_private_keys
serde_json::json!(true), // blank
serde_json::json!(""), // passphrase
serde_json::json!(false), // avoid_reuse
serde_json::json!(true), // descriptors
],
).await;
match create_result {
Ok(_) => tracing::info!("Created blank descriptor wallet '{}'", wallet_name),
Err(e) => {
let msg = e.to_string();
if msg.contains("already exists") {
tracing::info!("Wallet '{}' already exists, importing descriptors", wallet_name);
} else {
xprv_str.zeroize();
return Err(e.context("Failed to create wallet"));
}
}
}
// Step 2: Import BIP-84 descriptors (external + internal/change).
// Format: wpkh(xprv/0/*) for receive, wpkh(xprv/1/*) for change.
let external_desc = format!("wpkh({}/0/*)", xprv_str);
let internal_desc = format!("wpkh({}/1/*)", xprv_str);
// Get checksums from Bitcoin Core.
let ext_info: serde_json::Value = self.bitcoin_rpc_call(
&client, "getdescriptorinfo", &[serde_json::json!(external_desc)],
).await.context("getdescriptorinfo failed for external descriptor")?;
let int_info: serde_json::Value = self.bitcoin_rpc_call(
&client, "getdescriptorinfo", &[serde_json::json!(internal_desc)],
).await.context("getdescriptorinfo failed for internal descriptor")?;
let ext_desc_with_checksum = ext_info.get("descriptor")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("No descriptor in getdescriptorinfo response"))?;
let int_desc_with_checksum = int_info.get("descriptor")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("No descriptor in getdescriptorinfo response"))?;
let import_params = serde_json::json!([
{
"desc": ext_desc_with_checksum,
"timestamp": "now",
"active": true,
"internal": false,
"range": [0, 1000],
},
{
"desc": int_desc_with_checksum,
"timestamp": "now",
"active": true,
"internal": true,
"range": [0, 1000],
}
]);
let _import_result: serde_json::Value = self.bitcoin_rpc_call(
&client, "importdescriptors", &[import_params],
).await.context("importdescriptors failed")?;
// Zeroize the xprv string from memory.
xprv_str.zeroize();
tracing::info!("Bitcoin Core wallet '{}' initialized from master seed (BIP-84)", wallet_name);
Ok(serde_json::json!({
"initialized": true,
"wallet_name": wallet_name,
}))
}
}