diff --git a/bindings/c-ffi/example.c b/bindings/c-ffi/example.c index d97534b5..983a3147 100644 --- a/bindings/c-ffi/example.c +++ b/bindings/c-ffi/example.c @@ -4,7 +4,9 @@ int main() { const char *bitcoin_network = "Regtest"; - CResultString keys_res = rgblib_generate_keys(bitcoin_network); + const char *witness_version = "Taproot"; + CResultString keys_res = + rgblib_generate_keys(bitcoin_network, witness_version); if (keys_res.result == Err) { printf("ERR: %s\n", keys_res.inner); return EXIT_FAILURE; diff --git a/bindings/c-ffi/src/lib.rs b/bindings/c-ffi/src/lib.rs index 3f53924e..de6b9b7d 100644 --- a/bindings/c-ffi/src/lib.rs +++ b/bindings/c-ffi/src/lib.rs @@ -13,6 +13,7 @@ use std::{ use rgb_lib::{ AssetSchema, Assignment, Error as RgbLibError, + keys::WitnessVersion, utils::BitcoinNetwork, wallet::{ Online, Recipient, RefreshFilter, RgbWalletOpsOffline, RgbWalletOpsOnline, SinglesigKeys, @@ -171,8 +172,11 @@ pub extern "C" fn rgblib_finalize_psbt( } #[unsafe(no_mangle)] -pub extern "C" fn rgblib_generate_keys(bitcoin_network: *const c_char) -> CResultString { - generate_keys(bitcoin_network).into() +pub extern "C" fn rgblib_generate_keys( + bitcoin_network: *const c_char, + witness_version: *const c_char, +) -> CResultString { + generate_keys(bitcoin_network, witness_version).into() } #[unsafe(no_mangle)] @@ -379,8 +383,9 @@ pub extern "C" fn rgblib_restore_backup( pub extern "C" fn rgblib_restore_keys( bitcoin_network: *const c_char, mnemonic: *const c_char, + witness_version: *const c_char, ) -> CResultString { - restore_keys(bitcoin_network, mnemonic).into() + restore_keys(bitcoin_network, mnemonic, witness_version).into() } #[unsafe(no_mangle)] diff --git a/bindings/c-ffi/src/utils.rs b/bindings/c-ffi/src/utils.rs index 402dde25..c3611fe9 100644 --- a/bindings/c-ffi/src/utils.rs +++ b/bindings/c-ffi/src/utils.rs @@ -295,9 +295,13 @@ pub(crate) fn finalize_psbt( Ok(wallet.finalize_psbt(signed_psbt, None)?) } -pub(crate) fn generate_keys(bitcoin_network: *const c_char) -> Result { +pub(crate) fn generate_keys( + bitcoin_network: *const c_char, + witness_version: *const c_char, +) -> Result { let bitcoin_network = BitcoinNetwork::from_str(&ptr_to_string(bitcoin_network))?; - let res = rgb_lib::keys::generate_keys(bitcoin_network); + let witness_version = WitnessVersion::from_str(&ptr_to_string(witness_version))?; + let res = rgb_lib::keys::generate_keys(bitcoin_network, witness_version); Ok(serde_json::to_string(&res)?) } @@ -558,10 +562,12 @@ pub(crate) fn restore_backup( pub(crate) fn restore_keys( bitcoin_network: *const c_char, mnemonic: *const c_char, + witness_version: *const c_char, ) -> Result { let bitcoin_network = BitcoinNetwork::from_str(&ptr_to_string(bitcoin_network))?; let mnemonic = ptr_to_string(mnemonic); - let res = rgb_lib::keys::restore_keys(bitcoin_network, mnemonic)?; + let witness_version = WitnessVersion::from_str(&ptr_to_string(witness_version))?; + let res = rgb_lib::keys::restore_keys(bitcoin_network, mnemonic, witness_version)?; Ok(serde_json::to_string(&res)?) } diff --git a/bindings/uniffi/src/lib.rs b/bindings/uniffi/src/lib.rs index fbe6c6da..200a5d5a 100644 --- a/bindings/uniffi/src/lib.rs +++ b/bindings/uniffi/src/lib.rs @@ -9,7 +9,7 @@ use std::{ use rgb_lib::{ AssetSchema, Assignment as RgbLibAssignment, CloseMethod, Error as RgbLibError, TransferStatus, TransportType, - keys::Keys, + keys::{Keys, WitnessVersion}, utils::BitcoinNetwork, wallet::{ Address as RgbLibAddress, AssetCFA, AssetIFA, AssetNIA, AssetUDA, Assets, @@ -690,12 +690,16 @@ impl From for RgbLibRespondToOperation { } } -fn generate_keys(bitcoin_network: BitcoinNetwork) -> Keys { - rgb_lib::keys::generate_keys(bitcoin_network) +fn generate_keys(bitcoin_network: BitcoinNetwork, witness_version: WitnessVersion) -> Keys { + rgb_lib::keys::generate_keys(bitcoin_network, witness_version) } -fn restore_keys(bitcoin_network: BitcoinNetwork, mnemonic: String) -> Result { - rgb_lib::keys::restore_keys(bitcoin_network, mnemonic) +fn restore_keys( + bitcoin_network: BitcoinNetwork, + mnemonic: String, + witness_version: WitnessVersion, +) -> Result { + rgb_lib::keys::restore_keys(bitcoin_network, mnemonic, witness_version) } fn restore_backup( diff --git a/bindings/uniffi/src/rgb-lib.udl b/bindings/uniffi/src/rgb-lib.udl index 034ea5fc..c29678a2 100644 --- a/bindings/uniffi/src/rgb-lib.udl +++ b/bindings/uniffi/src/rgb-lib.udl @@ -1,8 +1,8 @@ namespace rgb_lib { - Keys generate_keys(BitcoinNetwork bitcoin_network); + Keys generate_keys(BitcoinNetwork bitcoin_network, WitnessVersion witness_version); [Throws=RgbLibError] - Keys restore_keys(BitcoinNetwork bitcoin_network, string mnemonic); + Keys restore_keys(BitcoinNetwork bitcoin_network, string mnemonic, WitnessVersion witness_version); [Throws=RgbLibError] void restore_backup(string backup_path, string password, string data_dir); @@ -71,6 +71,7 @@ interface RgbLibError { InvalidTransportEndpoints(string details); InvalidTxid(); InvalidVanillaKeychain(); + InvalidWitnessVersion(string witness_version); MaxFeeExceeded(string txid); MinFeeNotMet(string txid); MultisigHubService(string details); @@ -271,6 +272,12 @@ enum DatabaseType { "Sqlite", }; +[Remote] +enum WitnessVersion { + "SegWitV0", + "Taproot", +}; + interface Address { [Throws=RgbLibError] constructor(string address_string, BitcoinNetwork bitcoin_network); @@ -330,6 +337,7 @@ dictionary Keys { string account_xpub_vanilla; string account_xpub_colored; string master_fingerprint; + WitnessVersion witness_version; }; [Remote] @@ -525,6 +533,7 @@ dictionary SinglesigKeys { u8? vanilla_keychain; string master_fingerprint; string? mnemonic; + WitnessVersion witness_version; }; [Remote] diff --git a/src/error.rs b/src/error.rs index 5d94f228..063d1311 100644 --- a/src/error.rs +++ b/src/error.rs @@ -373,6 +373,13 @@ pub enum Error { #[error("Invalid vanilla keychain")] InvalidVanillaKeychain, + /// Invalid witness version + #[error("Invalid witness version: {witness_version}")] + InvalidWitnessVersion { + /// The invalid witness version + witness_version: String, + }, + /// The maximum fee has been exceeded #[error("Max fee exceeded for transfer with TXID: {txid}")] MaxFeeExceeded { diff --git a/src/keys.rs b/src/keys.rs index a8ac492f..8ac702a9 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -4,6 +4,54 @@ use super::*; +/// Supported bitcoin witness versions. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub enum WitnessVersion { + /// Segregated Witness version 0 + SegWitV0, + /// Segregated Witness version 1 + #[default] + Taproot, +} + +impl WitnessVersion { + pub(crate) fn purpose(&self) -> u32 { + match self { + WitnessVersion::SegWitV0 => 84, + WitnessVersion::Taproot => 86, + } + } + + pub(crate) fn descriptor_fn(&self) -> &'static str { + match self { + WitnessVersion::SegWitV0 => "wpkh", + WitnessVersion::Taproot => "tr", + } + } +} + +impl fmt::Display for WitnessVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{self:?}") + } +} + +impl FromStr for WitnessVersion { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s.to_lowercase().as_str() { + "segwitv0" => WitnessVersion::SegWitV0, + "taproot" => WitnessVersion::Taproot, + _ => { + return Err(Error::InvalidWitnessVersion { + witness_version: s.to_string(), + }); + } + }) + } +} + /// A set of Bitcoin keys used by the wallet. #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "camel_case", serde(rename_all = "camelCase"))] @@ -18,10 +66,13 @@ pub struct Keys { pub account_xpub_colored: String, /// Fingerprint of the master xPub pub master_fingerprint: String, + /// Witness version these keys were derived with + #[serde(default)] + pub witness_version: WitnessVersion, } -/// Generate a set of [`Keys`] for the given Bitcoin network. -pub fn generate_keys(bitcoin_network: BitcoinNetwork) -> Keys { +/// Generate a set of [`Keys`] for the given Bitcoin network and witness version. +pub fn generate_keys(bitcoin_network: BitcoinNetwork, witness_version: WitnessVersion) -> Keys { let bdk_network = BdkNetwork::from(bitcoin_network); let mnemonic = Mnemonic::generate((WordCount::Words12, Language::English)) .expect("to be able to generate a new mnemonic"); @@ -32,7 +83,7 @@ pub fn generate_keys(bitcoin_network: BitcoinNetwork) -> Keys { let xpub = &xkey.into_xpub(bdk_network, &Secp256k1::new()); let mnemonic_str = mnemonic.to_string(); let (account_xpub_vanilla, account_xpub_colored) = - get_account_xpubs(&bitcoin_network, &mnemonic_str).unwrap(); + get_account_xpubs(&bitcoin_network, &mnemonic_str, witness_version).unwrap(); let master_fingerprint = xpub.fingerprint().to_string(); Keys { mnemonic: mnemonic_str, @@ -40,14 +91,19 @@ pub fn generate_keys(bitcoin_network: BitcoinNetwork) -> Keys { account_xpub_vanilla: account_xpub_vanilla.to_string(), account_xpub_colored: account_xpub_colored.to_string(), master_fingerprint, + witness_version, } } -/// Recreate a set of [`Keys`] from the given mnemonic phrase. -pub fn restore_keys(bitcoin_network: BitcoinNetwork, mnemonic: String) -> Result { +/// Recreate a set of [`Keys`] from the given mnemonic phrase for the given witness version. +pub fn restore_keys( + bitcoin_network: BitcoinNetwork, + mnemonic: String, + witness_version: WitnessVersion, +) -> Result { let bdk_network = BdkNetwork::from(bitcoin_network); let (account_xpub_vanilla, account_xpub_colored) = - get_account_xpubs(&bitcoin_network, &mnemonic)?; + get_account_xpubs(&bitcoin_network, &mnemonic, witness_version)?; let mnemonic_parsed = Mnemonic::parse_in(Language::English, &mnemonic)?; let xkey: ExtendedKey = mnemonic_parsed .clone() @@ -61,6 +117,7 @@ pub fn restore_keys(bitcoin_network: BitcoinNetwork, mnemonic: String) -> Result account_xpub_vanilla: account_xpub_vanilla.to_string(), account_xpub_colored: account_xpub_colored.to_string(), master_fingerprint, + witness_version, }) } @@ -68,6 +125,23 @@ pub fn restore_keys(bitcoin_network: BitcoinNetwork, mnemonic: String) -> Result mod test { use super::*; + #[test] + fn witness_version_display_and_parse() { + for wv in [WitnessVersion::SegWitV0, WitnessVersion::Taproot] { + let s = wv.to_string(); + assert_eq!(WitnessVersion::from_str(&s).unwrap(), wv); + assert_eq!(WitnessVersion::from_str(&s.to_lowercase()).unwrap(), wv); + } + + let err = WitnessVersion::from_str("nonsense").unwrap_err(); + assert_eq!( + err, + Error::InvalidWitnessVersion { + witness_version: "nonsense".to_string(), + }, + ); + } + #[test] fn generate_success() { let Keys { @@ -76,7 +150,8 @@ mod test { account_xpub_vanilla, account_xpub_colored, master_fingerprint, - } = generate_keys(BitcoinNetwork::Regtest); + witness_version, + } = generate_keys(BitcoinNetwork::Regtest, WitnessVersion::Taproot); assert!(Mnemonic::from_str(&mnemonic).is_ok()); let pubkey = Xpub::from_str(&xpub); @@ -89,23 +164,40 @@ mod test { assert!(account_pubkey_rgb.is_ok()); let account_pubkey_btc = Xpub::from_str(&account_xpub_vanilla); assert!(account_pubkey_btc.is_ok()); + assert_eq!(witness_version, WitnessVersion::Taproot); } #[test] fn restore_success() { let network = BitcoinNetwork::Regtest; - let Keys { - mnemonic, - xpub, - account_xpub_vanilla, - account_xpub_colored, - master_fingerprint, - } = generate_keys(network); - let keys = restore_keys(network, mnemonic).unwrap(); - assert_eq!(keys.xpub, xpub); - assert_eq!(keys.master_fingerprint, master_fingerprint); - assert_eq!(keys.account_xpub_colored, account_xpub_colored); - assert_eq!(keys.account_xpub_vanilla, account_xpub_vanilla); + // round-trip generate → restore for each supported witness version + for wv in [WitnessVersion::Taproot, WitnessVersion::SegWitV0] { + let Keys { + mnemonic, + xpub, + account_xpub_vanilla, + account_xpub_colored, + master_fingerprint, + witness_version, + } = generate_keys(network, wv); + + let keys = restore_keys(network, mnemonic, witness_version).unwrap(); + assert_eq!(keys.xpub, xpub); + assert_eq!(keys.master_fingerprint, master_fingerprint); + assert_eq!(keys.account_xpub_colored, account_xpub_colored); + assert_eq!(keys.account_xpub_vanilla, account_xpub_vanilla); + assert_eq!(keys.witness_version, witness_version); + assert_eq!(witness_version, wv); + } + + // same mnemonic + different witness versions ⇒ same master xpub + // and fingerprint but different account xpubs (different BIP purpose) + let tr = generate_keys(network, WitnessVersion::Taproot); + let wpkh = restore_keys(network, tr.mnemonic.clone(), WitnessVersion::SegWitV0).unwrap(); + assert_eq!(tr.xpub, wpkh.xpub); + assert_eq!(tr.master_fingerprint, wpkh.master_fingerprint); + assert_ne!(tr.account_xpub_colored, wpkh.account_xpub_colored); + assert_ne!(tr.account_xpub_vanilla, wpkh.account_xpub_vanilla); } } diff --git a/src/lib.rs b/src/lib.rs index 904f97bd..3656912a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,13 +39,13 @@ //! ## Examples //! ### Create an RGB singlesig wallet //! ``` -//! use rgb_lib::keys::generate_keys; +//! use rgb_lib::keys::{generate_keys, WitnessVersion}; //! use rgb_lib::wallet::{DatabaseType, SinglesigKeys, Wallet, WalletData}; //! use rgb_lib::{AssetSchema, BitcoinNetwork}; //! //! fn main() -> Result<(), rgb_lib::Error> { //! let data_dir = tempfile::tempdir()?; -//! let keys = generate_keys(BitcoinNetwork::Regtest); +//! let keys = generate_keys(BitcoinNetwork::Regtest, WitnessVersion::Taproot); //! let single_sig_keys = SinglesigKeys::from_keys(&keys, None); //! let wallet_data = WalletData { //! data_dir: data_dir.path().to_str().unwrap().to_string(), @@ -300,7 +300,7 @@ use crate::{ enums::{ColoringType, RecipientTypeFull, WalletTransactionType}, }, error::InternalError, - keys::Keys, + keys::{Keys, WitnessVersion}, utils::{ ACCOUNT, DumbResolver, KEYCHAIN_BTC, KEYCHAIN_RGB, LOG_FILE, PURPOSE, RgbRuntime, adjust_canonicalization, beneficiary_from_script_buf, from_str_or_number_mandatory, diff --git a/src/utils.rs b/src/utils.rs index a933dea8..4a617e47 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -272,9 +272,12 @@ pub(crate) fn get_coin_type(bitcoin_network: &BitcoinNetwork, rgb: bool) -> u32 } } -pub(crate) fn get_account_derivation_children(coin_type: u32) -> Vec { +pub(crate) fn get_account_derivation_children( + witness_version: WitnessVersion, + coin_type: u32, +) -> Vec { vec![ - ChildNumber::from_hardened_idx(PURPOSE as u32).unwrap(), + ChildNumber::from_hardened_idx(witness_version.purpose()).unwrap(), ChildNumber::from_hardened_idx(coin_type).unwrap(), ChildNumber::from_hardened_idx(ACCOUNT as u32).unwrap(), ] @@ -284,9 +287,10 @@ fn derive_account_xprv_from_mnemonic( bitcoin_network: &BitcoinNetwork, mnemonic: &str, rgb: bool, + witness_version: WitnessVersion, ) -> Result<(Xpriv, Fingerprint), Error> { let coin_type = get_coin_type(bitcoin_network, rgb); - let account_derivation_children = get_account_derivation_children(coin_type); + let account_derivation_children = get_account_derivation_children(witness_version, coin_type); let mnemonic = Mnemonic::parse_in(Language::English, mnemonic.to_string())?; let master_xprv = Xpriv::new_master(*bitcoin_network, &mnemonic.to_seed("")).unwrap(); let master_xpub = Xpub::from_priv(&Secp256k1::new(), &master_xprv); @@ -299,15 +303,16 @@ fn get_xpub_from_xprv(xprv: &Xpriv) -> Xpub { Xpub::from_priv(&Secp256k1::new(), xprv) } -/// Get the account-level xPriv and xPub for the given mnemonic and Bitcoin network based on the -/// requested wallet side (colored or vanilla) +/// Get the account-level xPriv and xPub for the given mnemonic, Bitcoin network and witness +/// version, based on the requested wallet side (colored or vanilla). pub fn get_account_data( bitcoin_network: &BitcoinNetwork, mnemonic: &str, rgb: bool, + witness_version: WitnessVersion, ) -> Result<(Xpriv, Xpub, Fingerprint), Error> { let (account_xprv, master_fingerprint) = - derive_account_xprv_from_mnemonic(bitcoin_network, mnemonic, rgb)?; + derive_account_xprv_from_mnemonic(bitcoin_network, mnemonic, rgb, witness_version)?; let account_xpub = get_xpub_from_xprv(&account_xprv); Ok((account_xprv, account_xpub, master_fingerprint)) } @@ -315,9 +320,12 @@ pub fn get_account_data( pub(crate) fn get_account_xpubs( bitcoin_network: &BitcoinNetwork, mnemonic: &str, + witness_version: WitnessVersion, ) -> Result<(Xpub, Xpub), Error> { - let (_, account_xpub_vanilla, _) = get_account_data(bitcoin_network, mnemonic, false)?; - let (_, account_xpub_colored, _) = get_account_data(bitcoin_network, mnemonic, true)?; + let (_, account_xpub_vanilla, _) = + get_account_data(bitcoin_network, mnemonic, false, witness_version)?; + let (_, account_xpub_colored, _) = + get_account_data(bitcoin_network, mnemonic, true, witness_version)?; Ok((account_xpub_vanilla, account_xpub_colored)) } @@ -327,14 +335,21 @@ fn derive_descriptor( rgb: bool, keychain: u8, expected_xpub: &Xpub, + witness_version: WitnessVersion, ) -> Result { let (account_xprv, account_xpub, master_fingerprint) = - get_account_data(bitcoin_network, mnemonic, rgb)?; + get_account_data(bitcoin_network, mnemonic, rgb, witness_version)?; if account_xpub != *expected_xpub { return Err(Error::InvalidBitcoinKeys); } let coin_type = get_coin_type(bitcoin_network, rgb); - calculate_descriptor_from_xprv(&master_fingerprint, coin_type, account_xprv, keychain) + calculate_descriptor_from_xprv( + &master_fingerprint, + coin_type, + account_xprv, + keychain, + witness_version, + ) } pub(crate) fn get_descriptors( @@ -343,6 +358,7 @@ pub(crate) fn get_descriptors( vanilla_keychain: Option, expected_xpub_btc: &Xpub, expected_xpub_rgb: &Xpub, + witness_version: WitnessVersion, ) -> Result { let colored = derive_descriptor( bitcoin_network, @@ -350,6 +366,7 @@ pub(crate) fn get_descriptors( true, KEYCHAIN_RGB, expected_xpub_rgb, + witness_version, )?; let vanilla = derive_descriptor( bitcoin_network, @@ -357,6 +374,7 @@ pub(crate) fn get_descriptors( false, vanilla_keychain.unwrap_or(KEYCHAIN_BTC), expected_xpub_btc, + witness_version, )?; Ok(WalletDescriptors { colored, vanilla }) } @@ -367,6 +385,7 @@ pub(crate) fn get_descriptors_from_xpubs( xpub_rgb: &Xpub, xpub_btc: &Xpub, vanilla_keychain: Option, + witness_version: WitnessVersion, ) -> Result { let master_fingerprint = Fingerprint::from_str(master_fingerprint).map_err(|_| Error::InvalidFingerprint)?; @@ -375,12 +394,14 @@ pub(crate) fn get_descriptors_from_xpubs( get_coin_type(bitcoin_network, true), xpub_rgb, KEYCHAIN_RGB, + witness_version, )?; let vanilla = calculate_descriptor_from_xpub( &master_fingerprint, get_coin_type(bitcoin_network, false), xpub_btc, vanilla_keychain.unwrap_or(KEYCHAIN_BTC), + witness_version, )?; Ok(WalletDescriptors { colored, vanilla }) } @@ -445,6 +466,7 @@ pub(crate) fn calculate_descriptor_from_xprv( coin_type: u32, xprv: Xpriv, keychain: u8, + witness_version: WitnessVersion, ) -> Result { // derive final xpub from account-level xpub let path = get_derivation_path(keychain); @@ -452,7 +474,7 @@ pub(crate) fn calculate_descriptor_from_xprv( .derive_priv(&Secp256k1::new(), &path) .expect("provided path should be derivable in an xprv"); // derive descriptor with master fingerprint and full derivation path - let account_derivation_children = get_account_derivation_children(coin_type); + let account_derivation_children = get_account_derivation_children(witness_version, coin_type); let full_path = get_extended_derivation_path(account_derivation_children, keychain); let origin_prv: KeySource = (*master_fingerprint, full_path.clone()); let der_xprv_desc_key: DescriptorKey = der_xprv @@ -461,7 +483,7 @@ pub(crate) fn calculate_descriptor_from_xprv( let Secret(key, _, _) = der_xprv_desc_key else { unreachable!("into_descriptor_key on an Xpriv always yields a Secret variant") }; - Ok(format!("tr({key})")) + Ok(format!("{}({key})", witness_version.descriptor_fn())) } pub(crate) fn calculate_descriptor_from_xpub( @@ -469,6 +491,7 @@ pub(crate) fn calculate_descriptor_from_xpub( coin_type: u32, xpub: &Xpub, keychain: u8, + witness_version: WitnessVersion, ) -> Result { // derive final xpub from account-level xpub let path = get_derivation_path(keychain); @@ -476,7 +499,7 @@ pub(crate) fn calculate_descriptor_from_xpub( .derive_pub(&Secp256k1::new(), &path) .expect("provided path should be derivable in an xpub"); // derive descriptor with master fingerprint and full derivation path - let account_derivation_children = get_account_derivation_children(coin_type); + let account_derivation_children = get_account_derivation_children(witness_version, coin_type); let full_path = get_extended_derivation_path(account_derivation_children, keychain); let origin_pub: KeySource = (*master_fingerprint, full_path); let der_xpub_desc_key: DescriptorKey = der_xpub @@ -485,7 +508,7 @@ pub(crate) fn calculate_descriptor_from_xpub( let Public(key, _, _) = der_xpub_desc_key else { unreachable!("into_descriptor_key on an Xpub always yields a Public variant") }; - Ok(format!("tr({key})")) + Ok(format!("{}({key})", witness_version.descriptor_fn())) } #[cfg(any(feature = "electrum", feature = "esplora"))] diff --git a/src/wallet/multisig.rs b/src/wallet/multisig.rs index 730db901..edf8d862 100644 --- a/src/wallet/multisig.rs +++ b/src/wallet/multisig.rs @@ -2085,7 +2085,7 @@ mod test { #[test] fn cosigner_display_and_parse() { - let keys = generate_keys(BitcoinNetwork::Regtest); + let keys = generate_keys(BitcoinNetwork::Regtest, WitnessVersion::Taproot); // vanilla_keychain None let cosigner = Cosigner::from_keys(&keys, None); diff --git a/src/wallet/singlesig.rs b/src/wallet/singlesig.rs index bf5c7a7f..cd1ac1f3 100644 --- a/src/wallet/singlesig.rs +++ b/src/wallet/singlesig.rs @@ -19,6 +19,9 @@ pub struct SinglesigKeys { pub master_fingerprint: String, /// Wallet mnemonic phrase pub mnemonic: Option, + /// Witness version these keys were derived with + #[serde(default)] + pub witness_version: WitnessVersion, } impl SinglesigKeys { @@ -36,6 +39,7 @@ impl SinglesigKeys { self.vanilla_keychain, &xpub_btc, &xpub_rgb, + self.witness_version, )?; // check master fingerprint derived from mnemonic matches provided one let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)?; @@ -56,6 +60,7 @@ impl SinglesigKeys { &xpub_rgb, &xpub_btc, self.vanilla_keychain, + self.witness_version, )?; (descs, true) }) @@ -69,6 +74,7 @@ impl SinglesigKeys { vanilla_keychain, master_fingerprint: keys.master_fingerprint.clone(), mnemonic: Some(keys.mnemonic.clone()), + witness_version: keys.witness_version, } } @@ -80,6 +86,7 @@ impl SinglesigKeys { vanilla_keychain, master_fingerprint: keys.master_fingerprint.clone(), mnemonic: None, + witness_version: keys.witness_version, } } } diff --git a/src/wallet/test/get_wallet_data.rs b/src/wallet/test/get_wallet_data.rs index 473cea04..a5a4b510 100644 --- a/src/wallet/test/get_wallet_data.rs +++ b/src/wallet/test/get_wallet_data.rs @@ -7,7 +7,7 @@ fn success() { let test_data_dir_str = test_data_dir.to_string_lossy().to_string(); // test manual values - let keys = generate_keys(BitcoinNetwork::Signet); + let keys = generate_keys(BitcoinNetwork::Signet, WitnessVersion::Taproot); let wallet_1 = Wallet::new( WalletData { data_dir: test_data_dir_str.clone(), diff --git a/src/wallet/test/get_wallet_dir.rs b/src/wallet/test/get_wallet_dir.rs index 9429e5a2..5948c6d9 100644 --- a/src/wallet/test/get_wallet_dir.rs +++ b/src/wallet/test/get_wallet_dir.rs @@ -5,7 +5,7 @@ use super::*; fn success() { let test_data_dir = create_test_data_dir(); - let keys = generate_keys(BitcoinNetwork::Regtest); + let keys = generate_keys(BitcoinNetwork::Regtest, WitnessVersion::Taproot); let wallet = Wallet::new( get_test_wallet_data(test_data_dir.to_string_lossy().as_ref()), SinglesigKeys::from_keys(&keys, None), diff --git a/src/wallet/test/inflate.rs b/src/wallet/test/inflate.rs index 1c675e85..d24b07a3 100644 --- a/src/wallet/test/inflate.rs +++ b/src/wallet/test/inflate.rs @@ -421,7 +421,7 @@ fn fail() { // - schema not supported create_test_data_dir(); let bitcoin_network = BitcoinNetwork::Regtest; - let keys = generate_keys(bitcoin_network); + let keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); let mut wallet_nia = Wallet::new( WalletData { data_dir: get_test_data_dir_string(), diff --git a/src/wallet/test/multisig/mod.rs b/src/wallet/test/multisig/mod.rs index faa8440e..cad70146 100644 --- a/src/wallet/test/multisig/mod.rs +++ b/src/wallet/test/multisig/mod.rs @@ -21,10 +21,10 @@ fn success() { .collect(); // multisig wallet keys - let wlt_1_keys = generate_keys(bitcoin_network); - let wlt_2_keys = generate_keys(bitcoin_network); - let wlt_3_keys = generate_keys(bitcoin_network); - let wlt_4_keys = generate_keys(bitcoin_network); + let wlt_1_keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); + let wlt_2_keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); + let wlt_3_keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); + let wlt_4_keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); // cosigners let cosigners = vec![ @@ -928,9 +928,9 @@ fn fail() { let _ = fs::create_dir_all(&data_dir); // multisig wallet keys - let wlt_1_keys = generate_keys(bitcoin_network); - let wlt_2_keys = generate_keys(bitcoin_network); - let wlt_3_keys = generate_keys(bitcoin_network); + let wlt_1_keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); + let wlt_2_keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); + let wlt_3_keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); // cosigners let cosigners = vec![ @@ -1079,7 +1079,7 @@ fn fail() { assert_matches!(res.as_ref().err().unwrap(), Error::InvalidCosigner { details: d } if *d == format!("invalid vanilla xpub '{invalid_xpub}'")); // invalid xpub network - let invalid_keys = generate_keys(BitcoinNetwork::Mainnet); + let invalid_keys = generate_keys(BitcoinNetwork::Mainnet, WitnessVersion::Taproot); let invalid_cosigner = Cosigner::from_keys(&invalid_keys, None); // - colored xpub let mut invalid_cosigners = cosigners.clone(); @@ -1127,7 +1127,7 @@ fn fail() { assert_matches!(err, Error::MultisigHubService { details: d } if d == "Missing or invalid credentials"); // token for cosigner not in hub config - let wlt_badtoken_keys = generate_keys(bitcoin_network); + let wlt_badtoken_keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); let invalid_cosigner_token = create_token( &root_keypair, Role::Cosigner(wlt_badtoken_keys.account_xpub_colored), diff --git a/src/wallet/test/new.rs b/src/wallet/test/new.rs index bd619abc..10d466dc 100644 --- a/src/wallet/test/new.rs +++ b/src/wallet/test/new.rs @@ -15,7 +15,8 @@ fn check_wallet(wallet: &Wallet, network: BitcoinNetwork, keychain_vanilla: Opti .unwrap() .to_string(); let coin_type = get_coin_type(&network, true); - let account_derivation_children = get_account_derivation_children(coin_type); + let account_derivation_children = + get_account_derivation_children(WitnessVersion::Taproot, coin_type); let expected_full_derivation_path = get_extended_derivation_path(account_derivation_children, KEYCHAIN_RGB); assert_eq!( @@ -33,7 +34,8 @@ fn check_wallet(wallet: &Wallet, network: BitcoinNetwork, keychain_vanilla: Opti .unwrap() .to_string(); let coin_type = get_coin_type(&network, false); - let account_derivation_children = get_account_derivation_children(coin_type); + let account_derivation_children = + get_account_derivation_children(WitnessVersion::Taproot, coin_type); let keychain_vanilla = keychain_vanilla.unwrap_or(KEYCHAIN_BTC); let expected_full_derivation_path = get_extended_derivation_path(account_derivation_children, keychain_vanilla); @@ -65,7 +67,7 @@ fn success() { // with custom vanilla keychain let bitcoin_network = BitcoinNetwork::Regtest; - let keys = generate_keys(bitcoin_network); + let keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); let vanilla_keychain = Some(u8::MAX); let wallet = Wallet::new( WalletData { @@ -133,7 +135,7 @@ fn mainnet_success_electrum() { create_test_data_dir(); let bitcoin_network = BitcoinNetwork::Mainnet; - let keys = generate_keys(bitcoin_network); + let keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); let mut wallet = Wallet::new( WalletData { data_dir: get_test_data_dir_string(), @@ -162,7 +164,7 @@ fn mainnet_success_esplora() { create_test_data_dir(); let bitcoin_network = BitcoinNetwork::Mainnet; - let keys = generate_keys(bitcoin_network); + let keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); let mut wallet = Wallet::new( WalletData { data_dir: get_test_data_dir_string(), @@ -218,7 +220,7 @@ fn fail() { // invalid bitcoin keys let mut keys_bad = keys.clone(); - let alt_keys = generate_keys(BitcoinNetwork::Regtest); + let alt_keys = generate_keys(BitcoinNetwork::Regtest, WitnessVersion::Taproot); keys_bad.account_xpub_colored = alt_keys.xpub; let result = Wallet::new(wallet_data.clone(), keys_bad); assert!(matches!(result, Err(Error::InvalidBitcoinKeys))); @@ -327,7 +329,7 @@ fn watch_only_success() { initialize(); let bitcoin_network = BitcoinNetwork::Regtest; - let keys = generate_keys(bitcoin_network); + let keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); // watch-only wallet let mut wallet_watch = Wallet::new( @@ -394,7 +396,7 @@ fn watch_only_fail() { initialize(); let bitcoin_network = BitcoinNetwork::Regtest; - let keys = generate_keys(bitcoin_network); + let keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); // watch-only wallet invalid fingerprint let mut keys_bad = keys.clone(); @@ -421,13 +423,24 @@ fn get_account_xpub_success() { let mnemonic = wallet.get_keys().mnemonic.clone().unwrap(); // get colored account xpub - let (_, account_xpub, _) = get_account_data(&BitcoinNetwork::Regtest, &mnemonic, true).unwrap(); + let (_, account_xpub, _) = get_account_data( + &BitcoinNetwork::Regtest, + &mnemonic, + true, + WitnessVersion::Taproot, + ) + .unwrap(); assert_eq!(account_xpub.network, NetworkKind::Test,); assert_eq!(account_xpub.depth, 3); // get vanilla account xpub - let (_, account_xpub, _) = - get_account_data(&BitcoinNetwork::Regtest, &mnemonic, false).unwrap(); + let (_, account_xpub, _) = get_account_data( + &BitcoinNetwork::Regtest, + &mnemonic, + false, + WitnessVersion::Taproot, + ) + .unwrap(); assert_eq!(account_xpub.network, NetworkKind::Test,); assert_eq!(account_xpub.depth, 3); } @@ -462,7 +475,7 @@ fn supported_schemas() { let bitcoin_network = BitcoinNetwork::Regtest; // wallet (NIA schema supported) - let keys = generate_keys(bitcoin_network); + let keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); let mut wallet_nia = Wallet::new( WalletData { data_dir: get_test_data_dir_string(), @@ -493,7 +506,7 @@ fn supported_schemas() { assert_matches!(result, Err(Error::UnsupportedSchema { asset_schema: _ })); // recipient wallet (UDA schema supported) - let keys_rcv = generate_keys(bitcoin_network); + let keys_rcv = generate_keys(bitcoin_network, WitnessVersion::Taproot); let mut rcv_wallet_uda = Wallet::new( WalletData { data_dir: get_test_data_dir_string(), @@ -585,7 +598,7 @@ fn supported_schemas() { // wallet (mainnet, IFA schema supported) let bitcoin_network = BitcoinNetwork::Mainnet; - let keys_mainnet = generate_keys(bitcoin_network); + let keys_mainnet = generate_keys(bitcoin_network, WitnessVersion::Taproot); let result = Wallet::new( WalletData { data_dir: get_test_data_dir_string(), diff --git a/src/wallet/test/send.rs b/src/wallet/test/send.rs index 8e5bee00..55e325ef 100644 --- a/src/wallet/test/send.rs +++ b/src/wallet/test/send.rs @@ -7822,3 +7822,128 @@ fn allocations() { ); } } + +// End-to-end RGB on P2WPKH: fund + create_utxos + issue NIA + receive + send. +// Exercises the OpretFirst commitment on a non-taproot output with both +// blinded and witness recipients. +#[cfg(feature = "electrum")] +#[test] +#[parallel] +fn p2wpkh_send_receive_nia() { + initialize(); + + let amount_blind: u64 = 40; + let amount_witness: u64 = 26; + let total = amount_blind + amount_witness; + + let (mut wallet, online) = get_funded_wallet_p2wpkh(); + let (mut rcv_wallet, rcv_online) = get_funded_wallet_p2wpkh(); + + let asset = test_issue_asset_nia(&mut wallet, online, None); + + // two recipients on the P2WPKH receiver: one blinded, one witness + let receive_blind = test_blind_receive(&mut rcv_wallet); + let receive_witness = test_witness_receive(&mut rcv_wallet); + let recipient_map = HashMap::from([( + asset.asset_id.clone(), + vec![ + Recipient { + assignment: Assignment::Fungible(amount_blind), + recipient_id: receive_blind.recipient_id.clone(), + witness_data: None, + transport_endpoints: TRANSPORT_ENDPOINTS.clone(), + }, + Recipient { + assignment: Assignment::Fungible(amount_witness), + recipient_id: receive_witness.recipient_id.clone(), + witness_data: Some(WitnessData { + amount_sat: 1000, + blinding: None, + }), + transport_endpoints: TRANSPORT_ENDPOINTS.clone(), + }, + ], + )]); + let txid = test_send(&mut wallet, online, &recipient_map); + assert!(!txid.is_empty()); + + // drive transfers from WaitingCounterparty to Settled + wait_for_refresh(&mut rcv_wallet, rcv_online, None, None); + wait_for_refresh(&mut wallet, online, Some(&asset.asset_id), None); + mine(false, false); + wait_for_refresh(&mut rcv_wallet, rcv_online, None, None); + wait_for_refresh(&mut wallet, online, Some(&asset.asset_id), None); + + // settled balance on both sides + let balance_sender = test_get_asset_balance(&wallet, &asset.asset_id); + assert_eq!(balance_sender.settled, AMOUNT - total); + let balance_receiver = test_get_asset_balance(&rcv_wallet, &asset.asset_id); + assert_eq!(balance_receiver.settled, total); +} + +// Cross-type RGB transfer (P2TR → P2WPKH) with a full send-back roundtrip. +#[cfg(feature = "electrum")] +#[test] +#[parallel] +fn cross_type_p2tr_to_p2wpkh() { + initialize(); + + let amount: u64 = 66; + let amount_back: u64 = 10; + + let (mut wallet, online) = get_funded_wallet!(); + let (mut rcv_wallet, rcv_online) = get_funded_wallet_p2wpkh(); + + let asset = test_issue_asset_nia(&mut wallet, online, None); + + // P2TR → P2WPKH + let receive_data = test_blind_receive(&mut rcv_wallet); + let recipient_map = HashMap::from([( + asset.asset_id.clone(), + vec![Recipient { + assignment: Assignment::Fungible(amount), + recipient_id: receive_data.recipient_id.clone(), + witness_data: None, + transport_endpoints: TRANSPORT_ENDPOINTS.clone(), + }], + )]); + let txid = test_send(&mut wallet, online, &recipient_map); + assert!(!txid.is_empty()); + + // drive transfers from WaitingCounterparty to Settled + wait_for_refresh(&mut rcv_wallet, rcv_online, None, None); + wait_for_refresh(&mut wallet, online, Some(&asset.asset_id), None); + mine(false, false); + wait_for_refresh(&mut rcv_wallet, rcv_online, None, None); + wait_for_refresh(&mut wallet, online, Some(&asset.asset_id), None); + + let balance_sender = test_get_asset_balance(&wallet, &asset.asset_id); + assert_eq!(balance_sender.settled, AMOUNT - amount); + let balance_receiver = test_get_asset_balance(&rcv_wallet, &asset.asset_id); + assert_eq!(balance_receiver.settled, amount); + + // P2WPKH → P2TR (send some back to complete the roundtrip) + let receive_back = test_blind_receive(&mut wallet); + let recipient_map_back = HashMap::from([( + asset.asset_id.clone(), + vec![Recipient { + assignment: Assignment::Fungible(amount_back), + recipient_id: receive_back.recipient_id.clone(), + witness_data: None, + transport_endpoints: TRANSPORT_ENDPOINTS.clone(), + }], + )]); + let txid_back = test_send(&mut rcv_wallet, rcv_online, &recipient_map_back); + assert!(!txid_back.is_empty()); + + wait_for_refresh(&mut wallet, online, None, None); + wait_for_refresh(&mut rcv_wallet, rcv_online, None, None); + mine(false, false); + wait_for_refresh(&mut wallet, online, None, None); + wait_for_refresh(&mut rcv_wallet, rcv_online, None, None); + + let balance_sender = test_get_asset_balance(&wallet, &asset.asset_id); + assert_eq!(balance_sender.settled, AMOUNT - amount + amount_back); + let balance_receiver = test_get_asset_balance(&rcv_wallet, &asset.asset_id); + assert_eq!(balance_receiver.settled, amount - amount_back); +} diff --git a/src/wallet/test/utils/helpers.rs b/src/wallet/test/utils/helpers.rs index 0177a22d..275d4271 100644 --- a/src/wallet/test/utils/helpers.rs +++ b/src/wallet/test/utils/helpers.rs @@ -73,7 +73,7 @@ pub(crate) fn get_test_wallet_with_net( max_allocations_per_utxo: Option, bitcoin_network: BitcoinNetwork, ) -> Wallet { - let keys = generate_keys(bitcoin_network); + let keys = generate_keys(bitcoin_network, WitnessVersion::Taproot); let wallet_keys = if private_keys { SinglesigKeys::from_keys(&keys, None) } else { @@ -114,6 +114,27 @@ pub(crate) fn get_test_wallet(private_keys: bool, max_allocations_per_utxo: Opti ) } +#[cfg(any(feature = "electrum", feature = "esplora"))] +pub(crate) fn get_funded_wallet_p2wpkh() -> (Wallet, Online) { + create_test_data_dir(); + let keys = generate_keys(BitcoinNetwork::Regtest, WitnessVersion::SegWitV0); + let mut wallet = Wallet::new( + WalletData { + data_dir: get_test_data_dir_string(), + bitcoin_network: BitcoinNetwork::Regtest, + database_type: DatabaseType::Sqlite, + max_allocations_per_utxo: MAX_ALLOCATIONS_PER_UTXO, + supported_schemas: AssetSchema::VALUES.to_vec(), + }, + SinglesigKeys::from_keys(&keys, None), + ) + .unwrap(); + let online = wallet.go_online(true, ELECTRUM_URL.to_string()).unwrap(); + fund_wallet(wallet.get_address().unwrap()); + test_create_utxos_default(&mut wallet, online); + (wallet, online) +} + #[cfg(any(feature = "electrum", feature = "esplora"))] pub(crate) fn get_empty_wallet( private_keys: bool,