#[cfg(feature = "derive")]
use crate::encode::Descriptor;
#[cfg(feature = "derive")]
use crate::error::Error;
#[cfg(feature = "derive")]
use bitcoin::NetworkKind;
#[cfg(feature = "derive")]
use bitcoin::address::NetworkUnchecked;
#[cfg(feature = "derive")]
use bitcoin::bip32::{ChainCode, ChildNumber, Fingerprint, Xpub};
#[cfg(feature = "derive")]
use bitcoin::secp256k1::PublicKey;
#[cfg(feature = "derive")]
use bitcoin::{Address, Network};
#[cfg(feature = "derive")]
pub(crate) fn xpub_from_tlv_bytes(idx: u8, bytes: &[u8; 65]) -> Result<Xpub, Error> {
let chain_code_bytes: [u8; 32] = bytes[0..32]
.try_into()
.expect("32-byte slice is statically sized");
let chain_code = ChainCode::from(chain_code_bytes);
let public_key =
PublicKey::from_slice(&bytes[32..65]).map_err(|_| Error::InvalidXpubBytes { idx })?;
Ok(Xpub {
network: NetworkKind::Main,
depth: 0,
parent_fingerprint: Fingerprint::default(),
child_number: ChildNumber::Normal { index: 0 },
public_key,
chain_code,
})
}
#[cfg(feature = "derive")]
impl Descriptor {
pub fn derive_address(
&self,
chain: u32,
index: u32,
network: Network,
) -> Result<Address<NetworkUnchecked>, Error> {
if self.use_site_path.wildcard_hardened {
return Err(Error::HardenedPublicDerivation);
}
if let Some(alts) = &self.use_site_path.multipath {
if (chain as usize) >= alts.len() {
return Err(Error::ChainIndexOutOfRange {
chain,
alt_count: alts.len(),
});
}
if alts[chain as usize].hardened {
return Err(Error::HardenedPublicDerivation);
}
} else if chain != 0 {
return Err(Error::ChainIndexOutOfRange {
chain,
alt_count: 0,
});
}
let desc = crate::to_miniscript::to_miniscript_descriptor(self, chain)?;
let definite =
desc.at_derivation_index(index)
.map_err(|e| Error::AddressDerivationFailed {
detail: e.to_string(),
})?;
let addr = definite
.address(network)
.map_err(|e| Error::AddressDerivationFailed {
detail: e.to_string(),
})?;
Ok(addr.into_unchecked())
}
}
#[cfg(all(test, feature = "derive"))]
mod tests {
use super::*;
use crate::origin_path::{OriginPath, PathComponent, PathDecl, PathDeclPaths};
use crate::tag::Tag;
use crate::tlv::TlvSection;
use crate::tree::{Body, Node};
use crate::use_site_path::{Alternative, UseSitePath};
#[test]
fn xpub_from_tlv_bytes_rejects_invalid_pubkey() {
let bytes = [0u8; 65];
assert!(matches!(
xpub_from_tlv_bytes(7, &bytes),
Err(Error::InvalidXpubBytes { idx: 7 })
));
}
fn bip84_origin() -> OriginPath {
OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 84,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
],
}
}
fn one_test_xpub_bytes() -> [u8; 65] {
let mut bytes = [0u8; 65];
bytes[0..32].copy_from_slice(&[0x42; 32]);
bytes[32] = 0x02;
bytes[33..].copy_from_slice(&[
0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87,
0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B,
0x16, 0xF8, 0x17, 0x98,
]);
bytes
}
#[test]
fn derive_address_missing_pubkey_for_partial_keys() {
let d = Descriptor {
n: 2,
path_decl: PathDecl {
n: 2,
paths: PathDeclPaths::Shared(OriginPath {
components: vec![
PathComponent {
hardened: true,
value: 48,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 0,
},
PathComponent {
hardened: true,
value: 2,
},
],
}),
},
use_site_path: UseSitePath::standard_multipath(),
tree: Node {
tag: Tag::Wsh,
body: Body::Children(vec![Node {
tag: Tag::SortedMulti,
body: Body::MultiKeys {
k: 2,
indices: vec![0, 1],
},
}]),
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, one_test_xpub_bytes())]);
t
},
};
let err = d.derive_address(0, 0, Network::Bitcoin).unwrap_err();
assert!(matches!(err, Error::MissingPubkey { idx: 1 }));
}
#[test]
fn derive_address_chain_out_of_range() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84_origin()),
},
use_site_path: UseSitePath::standard_multipath(), tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, one_test_xpub_bytes())]);
t
},
};
let err = d.derive_address(5, 0, Network::Bitcoin).unwrap_err();
assert!(matches!(
err,
Error::ChainIndexOutOfRange {
chain: 5,
alt_count: 2
}
));
}
#[test]
fn derive_address_hardened_wildcard_rejected() {
let d = Descriptor {
n: 1,
path_decl: PathDecl {
n: 1,
paths: PathDeclPaths::Shared(bip84_origin()),
},
use_site_path: UseSitePath {
multipath: Some(vec![
Alternative {
hardened: false,
value: 0,
},
Alternative {
hardened: false,
value: 1,
},
]),
wildcard_hardened: true,
},
tree: Node {
tag: Tag::Wpkh,
body: Body::KeyArg { index: 0 },
},
tlv: {
let mut t = TlvSection::new_empty();
t.pubkeys = Some(vec![(0u8, one_test_xpub_bytes())]);
t
},
};
let err = d.derive_address(0, 0, Network::Bitcoin).unwrap_err();
assert!(matches!(err, Error::HardenedPublicDerivation));
}
}