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:
co-authored by
Claude Opus 4.6
parent
5da9e217e6
commit
19dcfd4f31
@@ -2,6 +2,7 @@ use crate::api::rpc::RpcHandler;
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine;
|
||||
use tracing::info;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
impl RpcHandler {
|
||||
/// Generate a new on-chain Bitcoin address.
|
||||
@@ -381,4 +382,71 @@ impl RpcHandler {
|
||||
"broadcast": false,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Initialize LND wallet with entropy derived from the node's BIP-39 master seed.
|
||||
/// The 16-byte entropy deterministically produces an aezeed mnemonic inside LND.
|
||||
/// Requires: password re-verification via params.password, encrypted seed on disk.
|
||||
pub(in crate::api::rpc) async fn handle_lnd_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_password = params.get("wallet_password")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'wallet_password' for LND"))?;
|
||||
|
||||
// Verify user password before granting seed access.
|
||||
self.auth_manager.verify_password(password).await
|
||||
.context("Password verification failed")?;
|
||||
|
||||
// Load encrypted seed from disk.
|
||||
let mnemonic = crate::seed::load_seed_encrypted(&self.config.data_dir, password).await
|
||||
.context("Failed to load encrypted seed. Was a seed phrase saved during onboarding?")?;
|
||||
let seed = crate::seed::MasterSeed::from_mnemonic(&mnemonic);
|
||||
|
||||
// Derive 16 bytes of LND entropy.
|
||||
let mut entropy = crate::seed::derive_lnd_entropy(&seed)?;
|
||||
let entropy_b64 = base64::engine::general_purpose::STANDARD.encode(&entropy);
|
||||
entropy.zeroize();
|
||||
|
||||
let wallet_password_b64 = base64::engine::general_purpose::STANDARD.encode(wallet_password.as_bytes());
|
||||
|
||||
// Call LND REST API to initialize wallet with derived entropy.
|
||||
// LND must be running but NOT yet initialized (no existing wallet).
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
let init_body = serde_json::json!({
|
||||
"wallet_password": wallet_password_b64,
|
||||
"seed_entropy": entropy_b64,
|
||||
});
|
||||
|
||||
let resp = client
|
||||
.post("https://127.0.0.1:8080/v1/initwallet")
|
||||
.json(&init_body)
|
||||
.send()
|
||||
.await
|
||||
.context("LND initwallet request failed — is LND running and uninitialized?")?;
|
||||
|
||||
let status = resp.status();
|
||||
let body: serde_json::Value = resp.json().await
|
||||
.context("Failed to parse initwallet response")?;
|
||||
|
||||
if !status.is_success() {
|
||||
let msg = body.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error");
|
||||
return Err(anyhow::anyhow!("LND wallet init failed: {}", msg));
|
||||
}
|
||||
|
||||
info!("LND wallet initialized from master seed entropy");
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"initialized": true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user