Skip to content

refactor(wallet)!: remove signers and it's APIs, relying on rust-bitcoin's Psbt::sign instead. #235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/example_wallet_electrum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ version = "0.2.0"
edition = "2021"

[dependencies]
bdk_wallet = { path = "../../wallet", features = ["file_store"] }
bdk_wallet = { path = "../../wallet", features = ["file_store", "test-utils"] }
bdk_electrum = { version = "0.23.0" }
anyhow = "1"
16 changes: 15 additions & 1 deletion examples/example_wallet_electrum/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use bdk_wallet::bitcoin::key::Secp256k1;
use bdk_wallet::file_store::Store;
use bdk_wallet::miniscript::Descriptor;
use bdk_wallet::Wallet;
use std::io::Write;

Expand Down Expand Up @@ -85,7 +87,19 @@ fn main() -> Result<(), anyhow::Error> {
tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT);

let mut psbt = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;

let secp = Secp256k1::new();

let (_, external_keymap) = Descriptor::parse_descriptor(&secp, EXTERNAL_DESC)?;
let (_, internal_keymap) = Descriptor::parse_descriptor(&secp, INTERNAL_DESC)?;
let key_map = external_keymap.into_iter().chain(internal_keymap).collect();

// It's using the signer implementation from `test_utils`, you should implement your own signing
// implementation for `KeyMap`.
let signer = bdk_wallet::test_utils::SignerWrapper::new(key_map);
let _ = psbt.sign(&signer, &secp);

let finalized = wallet.finalize_psbt(&mut psbt, SignOptions::default())?;
assert!(finalized);

let tx = psbt.extract_tx()?;
Expand Down
2 changes: 1 addition & 1 deletion examples/example_wallet_esplora_async/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bdk_wallet = { path = "../../wallet", features = ["rusqlite"] }
bdk_wallet = { path = "../../wallet", features = ["rusqlite", "test-utils"] }
bdk_esplora = { version = "0.22.0", features = ["async-https", "tokio"] }
tokio = { version = "1.38.1", features = ["rt", "rt-multi-thread", "macros"] }
anyhow = "1"
15 changes: 14 additions & 1 deletion examples/example_wallet_esplora_async/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::Ok;
use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_wallet::{
bitcoin::{Amount, Network},
miniscript::Descriptor,
rusqlite::Connection,
KeychainKind, SignOptions, Wallet,
};
Expand Down Expand Up @@ -80,7 +81,19 @@ async fn main() -> Result<(), anyhow::Error> {
tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT);

let mut psbt = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;

let secp = bdk_wallet::bitcoin::key::Secp256k1::new();

let (_, external_keymap) = Descriptor::parse_descriptor(&secp, EXTERNAL_DESC)?;
let (_, internal_keymap) = Descriptor::parse_descriptor(&secp, INTERNAL_DESC)?;
let key_map = external_keymap.into_iter().chain(internal_keymap).collect();

// It's using the signer implementation from `test_utils`, you should implement your own signing
// implementation for `KeyMap`.
let signer = bdk_wallet::test_utils::SignerWrapper::new(key_map);
let _ = psbt.sign(&signer, &secp);

let finalized = wallet.finalize_psbt(&mut psbt, SignOptions::default())?;
assert!(finalized);

let tx = psbt.extract_tx()?;
Expand Down
2 changes: 1 addition & 1 deletion examples/example_wallet_esplora_blocking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bdk_wallet = { path = "../../wallet", features = ["file_store"] }
bdk_wallet = { path = "../../wallet", features = ["file_store", "test-utils"] }
bdk_esplora = { version = "0.22.0", features = ["blocking"] }
anyhow = "1"
15 changes: 14 additions & 1 deletion examples/example_wallet_esplora_blocking/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use bdk_esplora::{esplora_client, EsploraExt};
use bdk_wallet::{
bitcoin::{Amount, Network},
file_store::Store,
miniscript::Descriptor,
KeychainKind, SignOptions, Wallet,
};

Expand Down Expand Up @@ -80,7 +81,19 @@ fn main() -> Result<(), anyhow::Error> {
tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT);

let mut psbt = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;

let secp = bdk_wallet::bitcoin::key::Secp256k1::new();

let (_, external_keymap) = Descriptor::parse_descriptor(&secp, EXTERNAL_DESC)?;
let (_, internal_keymap) = Descriptor::parse_descriptor(&secp, INTERNAL_DESC)?;
let key_map = external_keymap.into_iter().chain(internal_keymap).collect();

// It's using the signer implementation from `test_utils`, you should implement your own signing
// implementation for `KeyMap`.
let signer = bdk_wallet::test_utils::SignerWrapper::new(key_map);
let _ = psbt.sign(&signer, &secp);

let finalized = wallet.finalize_psbt(&mut psbt, SignOptions::default())?;
assert!(finalized);

let tx = psbt.extract_tx()?;
Expand Down
2 changes: 1 addition & 1 deletion wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ workspace = true
[dependencies]
rand_core = { version = "0.6.0" }
miniscript = { version = "12.3.1", features = [ "serde" ], default-features = false }
bitcoin = { version = "0.32.4", features = [ "serde", "base64" ], default-features = false }
bitcoin = { version = "0.32.6", features = [ "serde", "base64" ], default-features = false }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
bdk_chain = { version = "0.23.0", features = [ "miniscript", "serde" ], default-features = false }
Expand Down
163 changes: 160 additions & 3 deletions wallet/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,171 @@
use alloc::string::ToString;
use alloc::sync::Arc;
use core::str::FromStr;
use miniscript::descriptor::{DescriptorSecretKey, KeyMap};
use std::collections::BTreeMap;

use bdk_chain::{BlockId, ConfirmationBlockTime, TxUpdate};
use bitcoin::{
absolute, hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint,
Transaction, TxIn, TxOut, Txid,
absolute,
hashes::Hash,
key::Secp256k1,
psbt::{GetKey, GetKeyError, KeyRequest},
transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
Txid,
};

use crate::{KeychainKind, Update, Wallet};
use crate::{descriptor::check_wallet_descriptor, KeychainKind, Update, Wallet};

#[derive(Debug, Clone)]
/// A wrapper over the [`KeyMap`] type that has the `GetKey` trait implementation for signing.
pub struct SignerWrapper {
key_map: KeyMap,
}

impl SignerWrapper {
/// Creates a new [`SignerWrapper`] for the given [`KeyMap`].
pub fn new(key_map: KeyMap) -> Self {
Self { key_map }
}
}

impl GetKey for SignerWrapper {
type Error = GetKeyError;

fn get_key<C: bitcoin::secp256k1::Signing>(
&self,
key_request: KeyRequest,
secp: &bitcoin::key::Secp256k1<C>,
) -> Result<Option<bitcoin::PrivateKey>, Self::Error> {
for key_map in self.key_map.iter() {
let (_, desc_sk) = key_map;
let wrapper = DescriptorSecretKeyWrapper::new(desc_sk.clone());
match wrapper.get_key(key_request.clone(), secp) {
Ok(Some(private_key)) => return Ok(Some(private_key)),
Ok(None) => continue,
// TODO: (@leonardo) how should we handle this ?
// we can't error-out on this, because the valid signing key can be in the next
// iterations.
Err(_) => continue,
}
}
Ok(None)
}
}

/// .
pub struct DescriptorSecretKeyWrapper(DescriptorSecretKey);

impl DescriptorSecretKeyWrapper {
/// .
pub fn new(desc_sk: DescriptorSecretKey) -> Self {
Self(desc_sk)
}
}

impl GetKey for DescriptorSecretKeyWrapper {
type Error = GetKeyError;

fn get_key<C: bitcoin::secp256k1::Signing>(
&self,
key_request: KeyRequest,
secp: &Secp256k1<C>,
) -> Result<Option<bitcoin::PrivateKey>, Self::Error> {
match (&self.0, key_request) {
(DescriptorSecretKey::Single(single_priv), key_request) => {
let private_key = single_priv.key;
let public_key = private_key.public_key(secp);
let pubkey_map = BTreeMap::from([(public_key, private_key)]);
return pubkey_map.get_key(key_request, secp);
}
(DescriptorSecretKey::XPrv(descriptor_xkey), KeyRequest::Pubkey(public_key)) => {
let private_key = descriptor_xkey.xkey.private_key;
let pk = private_key.public_key(secp);
if public_key.inner.eq(&pk) {
return Ok(Some(
descriptor_xkey
.xkey
.derive_priv(secp, &descriptor_xkey.derivation_path)
.map_err(GetKeyError::Bip32)?
.to_priv(),
));
}
}
(
DescriptorSecretKey::XPrv(descriptor_xkey),
ref key_request @ KeyRequest::Bip32(ref key_source),
) => {
if let Some(key) = descriptor_xkey.xkey.get_key(key_request.clone(), secp)? {
return Ok(Some(key));
}

if let Some(_derivation_path) = descriptor_xkey.matches(key_source, secp) {
let (_fp, derivation_path) = key_source;

if let Some((_fp, origin_derivation_path)) = &descriptor_xkey.origin {
let derivation_path = &derivation_path[origin_derivation_path.len()..];
return Ok(Some(
descriptor_xkey
.xkey
.derive_priv(secp, &derivation_path)
.map_err(GetKeyError::Bip32)?
.to_priv(),
));
} else {
return Ok(Some(
descriptor_xkey
.xkey
.derive_priv(secp, derivation_path)
.map_err(GetKeyError::Bip32)?
.to_priv(),
));
};
}
}
(DescriptorSecretKey::XPrv(_), KeyRequest::XOnlyPubkey(_)) => {
return Err(GetKeyError::NotSupported)
}
(DescriptorSecretKey::MultiXPrv(_), _) => unimplemented!(),
_ => unreachable!(),
}
Ok(None)
}
}

/// Create the [`CreateParams`] for the provided testing `descriptor` and `change_descriptor`.
pub fn get_wallet_params(descriptor: &str, change_descriptor: Option<&str>) -> crate::CreateParams {
if let Some(change_desc) = change_descriptor {
Wallet::create(descriptor.to_string(), change_desc.to_string())
} else {
Wallet::create_single(descriptor.to_string())
}
}

/// Create a new [`SignerWrapper`] for the provided testing `descriptor` and `change_descriptor`.
pub fn get_wallet_signer(descriptor: &str, change_descriptor: Option<&str>) -> SignerWrapper {
let secp = Secp256k1::new();
let params = get_wallet_params(descriptor, change_descriptor).network(Network::Regtest);

let network = params.network;

let (descriptor, mut descriptor_keymap) = (params.descriptor)(&secp, network).unwrap();
check_wallet_descriptor(&descriptor).unwrap();
descriptor_keymap.extend(params.descriptor_keymap);

if let Some(change_descriptor) = params.change_descriptor {
let (change_descriptor, mut change_keymap) = change_descriptor(&secp, network).unwrap();
check_wallet_descriptor(&change_descriptor).unwrap();
change_keymap.extend(params.change_descriptor_keymap);
descriptor_keymap.extend(change_keymap)
}

SignerWrapper::new(descriptor_keymap)
}

/// Create a new [`SignerWrapper`] for the provided testing `descriptor`.
pub fn get_wallet_signer_single(descriptor: &str) -> SignerWrapper {
get_wallet_signer(descriptor, None)
}

/// Return a fake wallet that appears to be funded for testing.
///
Expand Down
41 changes: 25 additions & 16 deletions wallet/src/wallet/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ use core::fmt;
use core::str::FromStr;
use serde::{Deserialize, Serialize};

use miniscript::descriptor::{ShInner, WshInner};
use miniscript::descriptor::{KeyMap, ShInner, WshInner};
use miniscript::{Descriptor, ScriptContext, Terminal};

use crate::types::KeychainKind;
Expand Down Expand Up @@ -119,11 +119,8 @@ impl FullyNodedExport {
) -> Result<Self, &'static str> {
let descriptor = wallet
.public_descriptor(KeychainKind::External)
.to_string_with_secret(
&wallet
.get_signers(KeychainKind::External)
.as_key_map(wallet.secp_ctx()),
);
.to_string_with_secret(&KeyMap::new());

let descriptor = remove_checksum(descriptor);
Self::is_compatible_with_core(&descriptor)?;

Expand All @@ -147,11 +144,7 @@ impl FullyNodedExport {
let change_descriptor = {
let descriptor = wallet
.public_descriptor(KeychainKind::Internal)
.to_string_with_secret(
&wallet
.get_signers(KeychainKind::Internal)
.as_key_map(wallet.secp_ctx()),
);
.to_string_with_secret(&KeyMap::new());
Some(remove_checksum(descriptor))
};

Expand Down Expand Up @@ -239,16 +232,23 @@ mod test {
wallet
}

// TODO: (@leonardo) what's the best way to update these tests ?
#[test]
fn test_export_bip44() {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let public_descriptor = "wpkh([a12b02f4/44'/0'/0']xpub6BzhLAQUDcBUfHRQHZxDF2AbcJqp4Kaeq6bzJpXrjrWuK26ymTFwkEFbxPra2bJ7yeZKbDjfDeFwxe93JMqpo5SsPJH6dZdvV9kMzJkAZ69/0/*)";

let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let public_change_descriptor = "wpkh([a12b02f4/44'/0'/0']xpub6BzhLAQUDcBUfHRQHZxDF2AbcJqp4Kaeq6bzJpXrjrWuK26ymTFwkEFbxPra2bJ7yeZKbDjfDeFwxe93JMqpo5SsPJH6dZdvV9kMzJkAZ69/1/*)";

let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();

assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
assert_eq!(export.descriptor(), public_descriptor);
assert_eq!(
export.change_descriptor(),
Some(public_change_descriptor.into())
);
assert_eq!(export.blockheight, 5000);
assert_eq!(export.label, "Test Label");
}
Expand Down Expand Up @@ -305,24 +305,33 @@ mod test {
#[test]
fn test_export_tr() {
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let public_descriptor = "tr([73c5da0a/86'/0'/0']tpubDC3pD7UZXnsgh3EBjbtBQiB1FnLask7UHBSunZ1DPK4dCFFZoFRkgxHB8gt42FvLzx1DpxfHWxAsYaY6b643RVcGjDxXxns7wKKYnnfEcbB/0/*)";

let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
let public_change_descriptor = "tr([73c5da0a/86'/0'/0']tpubDC3pD7UZXnsgh3EBjbtBQiB1FnLask7UHBSunZ1DPK4dCFFZoFRkgxHB8gt42FvLzx1DpxfHWxAsYaY6b643RVcGjDxXxns7wKKYnnfEcbB/1/*)";

let wallet = get_test_wallet(descriptor, change_descriptor, Network::Testnet);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
assert_eq!(export.descriptor(), descriptor);
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
assert_eq!(export.descriptor(), public_descriptor);
assert_eq!(
export.change_descriptor(),
Some(public_change_descriptor.into())
);
assert_eq!(export.blockheight, 5000);
assert_eq!(export.label, "Test Label");
}

#[test]
fn test_export_to_json() {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let public_descriptor = "wpkh([a12b02f4/44'/0'/0']xpub6BzhLAQUDcBUfHRQHZxDF2AbcJqp4Kaeq6bzJpXrjrWuK26ymTFwkEFbxPra2bJ7yeZKbDjfDeFwxe93JMqpo5SsPJH6dZdvV9kMzJkAZ69/0/*)";

let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";

let wallet = get_test_wallet(descriptor, change_descriptor, Network::Bitcoin);
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();

assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
assert_eq!(export.to_string(), format!("{{\"descriptor\":\"{public_descriptor}\",\"blockheight\":5000,\"label\":\"Test Label\"}}"));
}

#[test]
Expand Down
Loading
Loading