Merge rust-bitcoin/rust-bitcoin#4623: Improve `Xpriv::derive_xpriv` and `Xpub::derive_xpub` ergonomics slightly

352712257e psbt: Use new `derive_xpriv` flexibility in GetKey (Daniel Roberts)
bd3f4b6bf1 psbt: Add test for GetKey bip32 (Daniel Roberts)
c7bdec14fb Fix clippy lint and formatting for `Xpriv::derive_xpriv` and `Xpriv::derive_xpub` calls (Daniel Roberts)
9d4381c8fe Improve `Xpriv::derive_xpriv` and `Xpub::derive_xpub` ergonomics (Daniel Roberts)

Pull request description:

  This enables a couple more things to be passed to the bip32 derive methods, with (afaict) no downside (all existing call sites remain valid and work as before).
  
  Given
  
  ```
  let secp = Secp256k1::new();
  let path: DerivationPath = "42'/350'/0".parse().unwrap();
  let xpriv: Xpriv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi".parse().unwrap();
  ```
  
  The following *new* ways to call derive are enabled:
  
  ```
  /// Derive using only part of the path
  xpriv.derive_xpriv(&secp, &path[1..])
  ```
  
  ```
  /// Derive moving into the method
  xpriv.derive_xpriv(&secp, path)
  ```
  
  The second case is probably of questionable usefulness, but I've ended up writing something like
  ```
  let path: DerivationPath = path.into_iter().skip(n).cloned().collect()
  ```
  enough that the first case would be welcome, and can be done with a minimal change.
  
  I'm reasonably confident this doesn't break anything (and it indeed doesn't break any tests that I can see) but this is definitely on the edge of my comfort with the rust type system, which is why I've created this as a draft.


ACKs for top commit:
  tcharding:
    ACK 352712257e
  apoelstra:
    ACK 352712257e66c7959b80a9f6375961295a292306; successfully ran local tests; nice!


Tree-SHA512: 0babdd9589d3312d4df54d77202a71dbb021e216167514c1a87f1f992f254f2b298b874411f8c7a3d56d7f730b22d104c3a78f9145715ea8df6bedad35c1ffd5
This commit is contained in:
Andrew Poelstra 2025-06-24 02:32:19 +00:00
commit d84745fd92
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
7 changed files with 40 additions and 23 deletions

View File

@ -46,7 +46,7 @@ fn main() {
// generate first receiving address at m/0/0
// manually creating indexes this time
let zero = ChildNumber::ZERO_NORMAL;
let public_key = xpub.derive_xpub(&secp, &[zero, zero]).unwrap().public_key;
let public_key = xpub.derive_xpub(&secp, [zero, zero]).unwrap().public_key;
let address = Address::p2wpkh(CompressedPublicKey(public_key), KnownHrp::Mainnet);
println!("First receiving address: {address}");
}

View File

@ -67,7 +67,7 @@ fn get_external_address_xpriv<C: Signing>(
let external_index = ChildNumber::ZERO_NORMAL;
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv.derive_xpriv(secp, &[external_index, idx]).expect("only deriving two more steps")
child_xpriv.derive_xpriv(secp, [external_index, idx]).expect("only deriving two more steps")
}
// Derive the internal address xpriv.
@ -83,7 +83,7 @@ fn get_internal_address_xpriv<C: Signing>(
let internal_index = ChildNumber::ONE_NORMAL;
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv.derive_xpriv(secp, &[internal_index, idx]).expect("only deriving two more steps")
child_xpriv.derive_xpriv(secp, [internal_index, idx]).expect("only deriving two more steps")
}
// The address to send to.

View File

@ -260,7 +260,7 @@ impl WatchOnly {
secp: &Secp256k1<C>,
) -> Result<(CompressedPublicKey, Address, DerivationPath)> {
let path = [ChildNumber::ONE_NORMAL, ChildNumber::ZERO_NORMAL];
let derived = self.account_0_xpub.derive_xpub(secp, &path)?;
let derived = self.account_0_xpub.derive_xpub(secp, path)?;
let pk = derived.to_public_key();
let addr = Address::p2wpkh(pk, NETWORK);

View File

@ -65,7 +65,7 @@ fn get_external_address_xpriv<C: Signing>(
let external_index = ChildNumber::ZERO_NORMAL;
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv.derive_xpriv(secp, &[external_index, idx]).expect("only deriving two more steps")
child_xpriv.derive_xpriv(secp, [external_index, idx]).expect("only deriving two more steps")
}
// Derive the internal address xpriv.
@ -81,7 +81,7 @@ fn get_internal_address_xpriv<C: Signing>(
let internal_index = ChildNumber::ONE_NORMAL;
let idx = ChildNumber::from_normal_idx(index).expect("valid index number");
child_xpriv.derive_xpriv(secp, &[internal_index, idx]).expect("only deriving two more steps")
child_xpriv.derive_xpriv(secp, [internal_index, idx]).expect("only deriving two more steps")
}
// Get the Taproot Key Origin.

View File

@ -298,7 +298,7 @@ fn generate_bip86_key_spend_tx(
.ok_or("missing Taproot key origin")?;
let secret_key =
master_xpriv.derive_xpriv(secp, &derivation_path)?.to_private_key().inner;
master_xpriv.derive_xpriv(secp, derivation_path)?.to_private_key().inner;
sign_psbt_taproot(
secret_key,
input.tap_internal_key.unwrap(),
@ -540,7 +540,7 @@ impl BenefactorWallet {
.ok_or("missing Taproot key origin")?;
let secret_key = self
.master_xpriv
.derive_xpriv(&self.secp, &derivation_path)
.derive_xpriv(&self.secp, derivation_path)
.expect("derivation path is short")
.to_private_key()
.inner;
@ -664,11 +664,8 @@ impl BeneficiaryWallet {
for (x_only_pubkey, (leaf_hashes, (_, derivation_path))) in
&psbt.inputs[0].tap_key_origins.clone()
{
let secret_key = self
.master_xpriv
.derive_xpriv(&self.secp, &derivation_path)?
.to_private_key()
.inner;
let secret_key =
self.master_xpriv.derive_xpriv(&self.secp, derivation_path)?.to_private_key().inner;
for lh in leaf_hashes {
let sighash_type = TapSighashType::All;
let hash = SighashCache::new(&unsigned_tx).taproot_script_spend_signature_hash(

View File

@ -756,7 +756,7 @@ impl Xpriv {
pub fn derive_priv<C: secp256k1::Signing, P: AsRef<[ChildNumber]>>(
&self,
secp: &Secp256k1<C>,
path: &P,
path: P,
) -> Result<Xpriv, DerivationError> {
self.derive_xpriv(secp, path)
}
@ -767,7 +767,7 @@ impl Xpriv {
pub fn derive_xpriv<C: secp256k1::Signing, P: AsRef<[ChildNumber]>>(
&self,
secp: &Secp256k1<C>,
path: &P,
path: P,
) -> Result<Xpriv, DerivationError> {
let mut sk: Xpriv = *self;
for cnum in path.as_ref() {
@ -910,7 +910,7 @@ impl Xpub {
pub fn derive_pub<C: secp256k1::Verification, P: AsRef<[ChildNumber]>>(
&self,
secp: &Secp256k1<C>,
path: &P,
path: P,
) -> Result<Xpub, DerivationError> {
self.derive_xpub(secp, path)
}
@ -921,7 +921,7 @@ impl Xpub {
pub fn derive_xpub<C: secp256k1::Verification, P: AsRef<[ChildNumber]>>(
&self,
secp: &Secp256k1<C>,
path: &P,
path: P,
) -> Result<Xpub, DerivationError> {
let mut pk: Xpub = *self;
for cnum in path.as_ref() {

View File

@ -21,7 +21,7 @@ use std::collections::{HashMap, HashSet};
use internals::write_err;
use secp256k1::{Keypair, Message, Secp256k1, Signing, Verification};
use crate::bip32::{self, DerivationPath, KeySource, Xpriv, Xpub};
use crate::bip32::{self, KeySource, Xpriv, Xpub};
use crate::crypto::key::{PrivateKey, PublicKey};
use crate::crypto::{ecdsa, taproot};
use crate::key::{TapTweak, XOnlyPublicKey};
@ -814,14 +814,13 @@ impl GetKey for Xpriv {
KeyRequest::XOnlyPubkey(_) => Err(GetKeyError::NotSupported),
KeyRequest::Bip32((fingerprint, path)) => {
let key = if self.fingerprint(secp) == *fingerprint {
let k = self.derive_xpriv(secp, &path).map_err(GetKeyError::Bip32)?;
let k = self.derive_xpriv(secp, path).map_err(GetKeyError::Bip32)?;
Some(k.to_private_key())
} else if self.parent_fingerprint == *fingerprint
&& !path.is_empty()
&& path[0] == self.child_number
{
let path = DerivationPath::from_iter(path.into_iter().skip(1).copied());
let k = self.derive_xpriv(secp, &path).map_err(GetKeyError::Bip32)?;
let k = self.derive_xpriv(secp, &path[1..]).map_err(GetKeyError::Bip32)?;
Some(k.to_private_key())
} else {
None
@ -1330,7 +1329,7 @@ mod tests {
#[cfg(feature = "rand-std")]
use {
crate::address::script_pubkey::ScriptBufExt as _,
crate::bip32::{DerivationPath, Fingerprint},
crate::bip32::Fingerprint,
crate::locktime,
crate::witness_version::WitnessVersion,
crate::WitnessProgram,
@ -1339,7 +1338,7 @@ mod tests {
use super::*;
use crate::address::script_pubkey::ScriptExt as _;
use crate::bip32::ChildNumber;
use crate::bip32::{ChildNumber, DerivationPath};
use crate::locktime::absolute;
use crate::network::NetworkKind;
use crate::psbt::serialize::{Deserialize, Serialize};
@ -2397,6 +2396,27 @@ mod tests {
);
}
#[test]
fn get_key_xpriv_bip32_parent() {
let secp = Secp256k1::new();
let seed = hex!("000102030405060708090a0b0c0d0e0f");
let parent_xpriv: Xpriv = Xpriv::new_master(NetworkKind::Main, &seed);
let path: DerivationPath = "m/1/2/3".parse().unwrap();
let path_prefix: DerivationPath = "m/1".parse().unwrap();
let expected_private_key =
parent_xpriv.derive_xpriv(&secp, &path).unwrap().to_private_key();
let derived_xpriv = parent_xpriv.derive_xpriv(&secp, &path_prefix).unwrap();
let derived_key = derived_xpriv
.get_key(&KeyRequest::Bip32((parent_xpriv.fingerprint(&secp), path.clone())), &secp)
.unwrap();
assert_eq!(derived_key, Some(expected_private_key));
}
#[test]
fn fee() {
let output_0_val = Amount::from_sat_u32(99_999_699);