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
@@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user