Skip to content

Commit

Permalink
fix watch-only support + add watch_only test
Browse files Browse the repository at this point in the history
  • Loading branch information
nicbus committed Feb 6, 2024
1 parent 9163bd6 commit ad22049
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 73 deletions.
19 changes: 17 additions & 2 deletions src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ use bdk::keys::bip39::{Language, Mnemonic, WordCount};
use bdk::keys::{DerivableKey, ExtendedKey, GeneratableKey};
use serde::{Deserialize, Serialize};

use crate::utils::{derive_account_xprv_from_mnemonic, get_xpub_from_xprv};
use crate::{BitcoinNetwork, Error};

/// A set of Bitcoin keys used by the wallet.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Keys {
/// Mnemonic phrase
pub mnemonic: String,
/// xPub corresponding to the mnemonic phrase
/// Master xPub
pub xpub: String,
/// Fingerprint of the xPub
pub xpub_fingerprint: String,
/// Account-level xPub
pub account_xpub: String,
}

/// Generate a set of [`Keys`] for the given Bitcoin network.
Expand All @@ -31,16 +34,22 @@ pub fn generate_keys(bitcoin_network: BitcoinNetwork) -> Keys {
.into_extended_key()
.expect("a valid key should have been provided");
let xpub = &xkey.into_xpub(bdk_network, &Secp256k1::new());
let mnemonic_str = mnemonic.to_string();
let account_xprv = derive_account_xprv_from_mnemonic(bitcoin_network, &mnemonic_str).unwrap();
let account_xpub = get_xpub_from_xprv(&account_xprv);
Keys {
mnemonic: mnemonic.to_string(),
mnemonic: mnemonic_str,
xpub: xpub.clone().to_string(),
xpub_fingerprint: xpub.fingerprint().to_string(),
account_xpub: account_xpub.to_string(),
}
}

/// Recreate a set of [`Keys`] from the given mnemonic phrase.
pub fn restore_keys(bitcoin_network: BitcoinNetwork, mnemonic: String) -> Result<Keys, Error> {
let bdk_network = BdkNetwork::from(bitcoin_network);
let account_xprv = derive_account_xprv_from_mnemonic(bitcoin_network, &mnemonic).unwrap();
let account_xpub = get_xpub_from_xprv(&account_xprv);
let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)?;
let xkey: ExtendedKey = mnemonic
.clone()
Expand All @@ -51,6 +60,7 @@ pub fn restore_keys(bitcoin_network: BitcoinNetwork, mnemonic: String) -> Result
mnemonic: mnemonic.to_string(),
xpub: xpub.clone().to_string(),
xpub_fingerprint: xpub.fingerprint().to_string(),
account_xpub: account_xpub.to_string(),
})
}

Expand All @@ -66,12 +76,15 @@ mod test {
mnemonic,
xpub,
xpub_fingerprint,
account_xpub,
} = generate_keys(BitcoinNetwork::Regtest);

assert!(Mnemonic::from_str(&mnemonic).is_ok());
let pubkey = ExtendedPubKey::from_str(&xpub);
assert!(pubkey.is_ok());
assert_eq!(pubkey.unwrap().fingerprint().to_string(), xpub_fingerprint);
let account_pubkey = ExtendedPubKey::from_str(&account_xpub);
assert!(account_pubkey.is_ok());
}

#[test]
Expand All @@ -81,10 +94,12 @@ mod test {
mnemonic,
xpub,
xpub_fingerprint,
account_xpub,
} = generate_keys(network);

let keys = restore_keys(network, mnemonic).unwrap();
assert_eq!(keys.xpub, xpub);
assert_eq!(keys.xpub_fingerprint, xpub_fingerprint);
assert_eq!(keys.account_xpub, account_xpub);
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
//! bitcoin_network: BitcoinNetwork::Regtest,
//! database_type: DatabaseType::Sqlite,
//! max_allocations_per_utxo: 5,
//! pubkey: keys.xpub,
//! pubkey: keys.account_xpub,
//! mnemonic: Some(keys.mnemonic),
//! vanilla_keychain: None,
//! };
Expand Down
86 changes: 63 additions & 23 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ use bdk::bitcoin::bip32::{DerivationPath, ExtendedPubKey, KeySource};
use bdk::bitcoin::secp256k1::Secp256k1;
use bdk::bitcoin::Network as BdkNetwork;
use bdk::descriptor::Segwitv0;
use bdk::keys::DescriptorKey::Public;
use bdk::keys::{DerivableKey, DescriptorKey};
use bdk::keys::bip39::{Language, Mnemonic};
use bdk::keys::DescriptorKey::{Public, Secret};
use bdk::keys::{DerivableKey, DescriptorKey, DescriptorSecretKey};
use bdk::miniscript::DescriptorPublicKey;
use bitcoin::bip32::ChildNumber;
use bp::{Outpoint, Txid};
use commit_verify::mpc::MerkleBlock;
use rgb::Runtime;
Expand Down Expand Up @@ -162,35 +165,56 @@ pub(crate) fn get_valid_txid_for_network(bitcoin_network: &BitcoinNetwork) -> St
}
}

pub(crate) fn _get_derivation_path(
watch_only: bool,
fn get_coin_type(bitcoin_network: BitcoinNetwork) -> u32 {
u32::from(bitcoin_network != BitcoinNetwork::Mainnet)
}

pub(crate) fn derive_account_xprv_from_mnemonic(
bitcoin_network: BitcoinNetwork,
keychain: u8,
) -> String {
let coin_type = i32::from(bitcoin_network != BitcoinNetwork::Mainnet);
let hardened = if watch_only { "" } else { "'" };
let child_number = if watch_only { "" } else { "/*" };
let master = if watch_only { "m" } else { "" };
format!("{master}/{PURPOSE}{hardened}/{coin_type}{hardened}/{ACCOUNT}{hardened}/{keychain}{child_number}")
mnemonic: &str,
) -> Result<ExtendedPrivKey, Error> {
let coin_type = get_coin_type(bitcoin_network);
let account_derivation_path = vec![
ChildNumber::from_hardened_idx(PURPOSE as u32).unwrap(),
ChildNumber::from_hardened_idx(coin_type).unwrap(),
ChildNumber::from_hardened_idx(ACCOUNT as u32).unwrap(),
];
let mnemonic = Mnemonic::parse_in(Language::English, mnemonic.to_string())?;
let master_xprv =
ExtendedPrivKey::new_master(bitcoin_network.into(), &mnemonic.to_seed("")).unwrap();
Ok(master_xprv.derive_priv(&Secp256k1::new(), &account_derivation_path)?)
}

pub(crate) fn calculate_descriptor_from_xprv(
pub(crate) fn get_xpub_from_xprv(xprv: &ExtendedPrivKey) -> ExtendedPubKey {
ExtendedPubKey::from_priv(&Secp256k1::new(), xprv)
}

fn get_descriptor_priv_key(
xprv: ExtendedPrivKey,
bitcoin_network: BitcoinNetwork,
keychain: u8,
) -> String {
let derivation_path = _get_derivation_path(false, bitcoin_network, keychain);
format!("wpkh({xprv}{derivation_path})")
) -> Result<DescriptorSecretKey, Error> {
let derivation_path = vec![ChildNumber::from_normal_idx(keychain as u32).unwrap()];
let path = DerivationPath::from_iter(derivation_path.clone());
let der_xprv = &xprv
.derive_priv(&Secp256k1::new(), &path)
.expect("provided path should be derivable in an xprv");
let origin_prv: KeySource = (xprv.fingerprint(&Secp256k1::new()), path);
let der_xprv_desc_key: DescriptorKey<Segwitv0> = der_xprv
.into_descriptor_key(Some(origin_prv), DerivationPath::default())
.expect("should be able to convert xprv in a descriptor key");
if let Secret(key, _, _) = der_xprv_desc_key {
Ok(key)
} else {
Err(InternalError::Unexpected)?
}
}

pub(crate) fn calculate_descriptor_from_xpub(
fn get_descriptor_pub_key(
xpub: ExtendedPubKey,
bitcoin_network: BitcoinNetwork,
keychain: u8,
) -> Result<String, Error> {
let derivation_path = _get_derivation_path(true, bitcoin_network, keychain);
let path =
DerivationPath::from_str(&derivation_path).expect("derivation path should be well-formed");
) -> Result<DescriptorPublicKey, Error> {
let derivation_path = vec![ChildNumber::from_normal_idx(keychain as u32).unwrap()];
let path = DerivationPath::from_iter(derivation_path.clone());
let der_xpub = &xpub
.derive_pub(&Secp256k1::new(), &path)
.expect("provided path should be derivable in an xpub");
Expand All @@ -199,12 +223,28 @@ pub(crate) fn calculate_descriptor_from_xpub(
.into_descriptor_key(Some(origin_pub), DerivationPath::default())
.expect("should be able to convert xpub in a descriptor key");
if let Public(key, _, _) = der_xpub_desc_key {
Ok(format!("wpkh({key})"))
Ok(key)
} else {
Err(InternalError::Unexpected)?
}
}

pub(crate) fn calculate_descriptor_from_xprv(
xprv: ExtendedPrivKey,
keychain: u8,
) -> Result<String, Error> {
let key = get_descriptor_priv_key(xprv, keychain)?;
Ok(format!("wpkh({key})"))
}

pub(crate) fn calculate_descriptor_from_xpub(
xpub: ExtendedPubKey,
keychain: u8,
) -> Result<String, Error> {
let key = get_descriptor_pub_key(xpub, keychain)?;
Ok(format!("wpkh({key})"))
}

fn convert_time_fmt_error(cause: time::error::Format) -> io::Error {
io::Error::new(io::ErrorKind::Other, cause)
}
Expand Down
39 changes: 12 additions & 27 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ use bdk::database::{
AnyDatabase, BatchDatabase, ConfigurableDatabase as BdkConfigurableDatabase, MemoryDatabase,
};
use bdk::descriptor::IntoWalletDescriptor;
use bdk::keys::bip39::{Language, Mnemonic};
use bdk::keys::{DerivableKey, ExtendedKey};
use bdk::keys::ExtendedKey;
use bdk::wallet::AddressIndex;
pub use bdk::BlockTime;
use bdk::{FeeRate, KeychainKind, LocalUtxo, SignOptions, SyncOptions, Wallet as BdkWallet};
Expand Down Expand Up @@ -105,9 +104,9 @@ use crate::database::{
};
use crate::error::{Error, InternalError};
use crate::utils::{
calculate_descriptor_from_xprv, calculate_descriptor_from_xpub, get_genesis_hash,
get_valid_txid_for_network, load_rgb_runtime, now, setup_logger, BitcoinNetwork, RgbRuntime,
LOG_FILE,
calculate_descriptor_from_xprv, calculate_descriptor_from_xpub,
derive_account_xprv_from_mnemonic, get_genesis_hash, get_valid_txid_for_network,
get_xpub_from_xprv, load_rgb_runtime, now, setup_logger, BitcoinNetwork, RgbRuntime, LOG_FILE,
};

#[cfg(test)]
Expand Down Expand Up @@ -1216,7 +1215,7 @@ pub struct WalletData {
pub database_type: DatabaseType,
/// The max number of RGB allocations allowed per UTXO
pub max_allocations_per_utxo: u32,
/// Wallet xPub
/// Wallet account-level xPub
pub pubkey: String,
/// Wallet mnemonic phrase
pub mnemonic: Option<String>,
Expand Down Expand Up @@ -1292,25 +1291,13 @@ impl Wallet {
let bdk_database =
AnyDatabase::from_config(&bdk_config.into()).map_err(InternalError::from)?;
let bdk_wallet = if let Some(mnemonic) = wdata.mnemonic {
let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)?;
let xkey: ExtendedKey = mnemonic
.clone()
.into_extended_key()
.expect("a valid key should have been provided");
let xpub_from_mnemonic = &xkey.into_xpub(bdk_network, &Secp256k1::new());
if *xpub_from_mnemonic != xpub {
let account_xprv = derive_account_xprv_from_mnemonic(wdata.bitcoin_network, &mnemonic)?;
let account_xpub = get_xpub_from_xprv(&account_xprv);
if account_xpub != xpub {
return Err(Error::InvalidBitcoinKeys);
}
let xkey: ExtendedKey = mnemonic
.into_extended_key()
.expect("a valid key should have been provided");
let xprv = xkey
.into_xprv(bdk_network)
.expect("should be possible to get an extended private key");
let descriptor =
calculate_descriptor_from_xprv(xprv, wdata.bitcoin_network, KEYCHAIN_RGB_OPRET);
let change_descriptor =
calculate_descriptor_from_xprv(xprv, wdata.bitcoin_network, vanilla_keychain);
let descriptor = calculate_descriptor_from_xprv(account_xprv, KEYCHAIN_RGB_OPRET)?;
let change_descriptor = calculate_descriptor_from_xprv(account_xprv, vanilla_keychain)?;
BdkWallet::new(
&descriptor,
Some(&change_descriptor),
Expand All @@ -1319,10 +1306,8 @@ impl Wallet {
)
.map_err(InternalError::from)?
} else {
let descriptor_pub =
calculate_descriptor_from_xpub(xpub, wdata.bitcoin_network, KEYCHAIN_RGB_OPRET)?;
let change_descriptor_pub =
calculate_descriptor_from_xpub(xpub, wdata.bitcoin_network, vanilla_keychain)?;
let descriptor_pub = calculate_descriptor_from_xpub(xpub, KEYCHAIN_RGB_OPRET)?;
let change_descriptor_pub = calculate_descriptor_from_xpub(xpub, vanilla_keychain)?;
BdkWallet::new(
&descriptor_pub,
Some(&change_descriptor_pub),
Expand Down
Loading

0 comments on commit ad22049

Please sign in to comment.