use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use bitcoin::util::bip32::{
self, ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, KeySource,
};
use bitcoin::{OutPoint, XpubIdentifier};
use secp256k1::{Secp256k1, Signing, Verification};
use slip132::FromSlip132;
use crate::{
AccountStep, Bip43, DerivationStandard, DerivationSubpath, DerivePatternError, HardenedIndex,
SegmentIndexes, TerminalStep, UnhardenedIndex, XpubRef,
};
#[derive(
Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Display, Error, From
)]
#[display(doc_comments)]
pub enum ParseError {
#[display(inner)]
#[from]
Bip32(bip32::Error),
#[display(inner)]
#[from]
Slip132(slip132::Error),
InvalidDerivationPathFormat(String),
AccountXpubAbsent(String),
RevocationSeal(String),
}
pub trait DerivePublicKey {
fn derive_public_key<C: Verification>(
&self,
ctx: &Secp256k1<C>,
pat: impl IntoIterator<Item = impl Into<UnhardenedIndex>>,
) -> Result<secp256k1::PublicKey, DerivePatternError>;
}
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
#[derive(StrictEncode, StrictDecode)]
pub struct DerivationAccount {
pub master: XpubRef,
pub account_path: DerivationSubpath<AccountStep>,
pub account_xpub: ExtendedPubKey,
pub revocation_seal: Option<OutPoint>,
pub terminal_path: DerivationSubpath<TerminalStep>,
}
impl DerivePublicKey for DerivationAccount {
fn derive_public_key<C: Verification>(
&self,
ctx: &Secp256k1<C>,
pat: impl IntoIterator<Item = impl Into<UnhardenedIndex>>,
) -> Result<secp256k1::PublicKey, DerivePatternError> {
Ok(self
.account_xpub
.derive_pub(ctx, &self.to_terminal_derivation_path(pat)?)
.expect("unhardened derivation failure")
.public_key)
}
}
impl DerivationAccount {
pub fn with<C: Signing>(
secp: &Secp256k1<C>,
master_id: XpubIdentifier,
account_xpriv: ExtendedPrivKey,
account_path: &[u16],
terminal_path: impl IntoIterator<Item = TerminalStep>,
) -> DerivationAccount {
let account_xpub = ExtendedPubKey::from_priv(secp, &account_xpriv);
DerivationAccount {
master: XpubRef::XpubIdentifier(master_id),
account_path: account_path
.iter()
.copied()
.map(AccountStep::hardened_index)
.collect(),
account_xpub,
revocation_seal: None,
terminal_path: terminal_path.into_iter().collect(),
}
}
pub fn seed_based(&self) -> bool { self.master != XpubRef::Unknown }
pub fn keyspace_size(&self) -> usize {
self.terminal_path
.iter()
.fold(1usize, |size, step| size * step.count())
}
#[inline]
pub fn master_fingerprint(&self) -> Option<Fingerprint> { self.master.fingerprint() }
#[inline]
pub fn account_fingerprint(&self) -> Fingerprint { self.account_xpub.fingerprint() }
pub fn account_no(&self) -> Option<HardenedIndex> {
self.to_full_derivation_path([0u8, 0u8])
.ok()
.as_ref()
.and_then(Bip43::deduce)
.as_ref()
.and_then(Bip43::account_depth)
.and_then(|depth| self.account_path.get(depth as usize))
.and_then(AccountStep::to_hardened)
}
#[inline]
pub fn to_account_derivation_path(&self) -> DerivationPath {
self.account_path.iter().map(ChildNumber::from).collect()
}
#[inline]
pub fn account_key_source(&self) -> Option<KeySource> {
self.master_fingerprint()
.map(|fp| (fp, self.to_account_derivation_path()))
}
pub fn to_terminal_derivation_path(
&self,
pat: impl IntoIterator<Item = impl Into<UnhardenedIndex>>,
) -> Result<DerivationPath, DerivePatternError> {
let mut iter = pat.into_iter();
self.terminal_path
.iter()
.map(|step| {
if step.count() == 1 {
Ok(ChildNumber::Normal {
index: step.first_index(),
})
} else if let Some(index) = iter.next() {
let index = index.into();
if !step.contains(index.first_index()) {
Err(DerivePatternError)
} else {
Ok(ChildNumber::from(index))
}
} else {
Err(DerivePatternError)
}
})
.collect()
}
pub fn to_full_derivation_path(
&self,
pat: impl IntoIterator<Item = impl Into<UnhardenedIndex>>,
) -> Result<DerivationPath, DerivePatternError> {
let mut derivation_path =
Vec::with_capacity(self.account_path.len() + self.terminal_path.len() + 1);
derivation_path.extend(self.account_path.iter().map(ChildNumber::from));
derivation_path.extend(&self.to_terminal_derivation_path(pat)?);
Ok(derivation_path.into())
}
pub fn bip32_derivation<C: Verification>(
&self,
ctx: &Secp256k1<C>,
pat: impl IntoIterator<Item = impl Into<UnhardenedIndex>> + Clone,
) -> Result<(secp256k1::PublicKey, KeySource), DerivePatternError> {
Ok((
self.derive_public_key(ctx, pat.clone())?,
(
self.master_fingerprint().unwrap_or_default(),
self.to_full_derivation_path(pat)?,
),
))
}
}
impl DerivationAccount {
fn fmt_account_path(&self, f: &mut Formatter<'_>) -> fmt::Result {
if !self.account_path.is_empty() {
f.write_str("/")?;
}
f.write_str(
&self
.account_path
.iter()
.map(AccountStep::to_string)
.collect::<Vec<_>>()
.join("/"),
)
}
fn fmt_terminal_path(&self, f: &mut Formatter<'_>) -> fmt::Result {
for segment in self.terminal_path.iter() {
f.write_str("/")?;
Display::fmt(segment, f)?;
}
Ok(())
}
fn fmt_bitcoin_core(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(fp) = self.master.fingerprint() {
if self.account_xpub.fingerprint() != fp {
write!(f, "[{:08x}", fp)?;
}
} else if !self.account_path.is_empty() {
f.write_str("[00000000")?;
}
self.fmt_account_path(f)?;
if !self.account_path.is_empty() {
f.write_str("]")?;
}
write!(f, "{}", self.account_xpub)?;
self.fmt_terminal_path(f)
}
fn fmt_lnpbp(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.seed_based() {
f.write_str("m=")?;
if !self.account_path.is_empty() {
write!(f, "{}", self.master)?;
}
}
self.fmt_account_path(f)?;
if !self.account_path.is_empty() {
f.write_str("=")?;
}
write!(f, "[{}]", self.account_xpub)?;
if let Some(seal) = self.revocation_seal {
write!(f, "?{}", seal)?;
}
self.fmt_terminal_path(f)
}
pub fn from_str_bitcoin_core(s: &str) -> Result<DerivationAccount, ParseError> {
let mut split = s.split('/');
let mut account = DerivationAccount {
master: XpubRef::Unknown,
account_path: empty!(),
account_xpub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ\
29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
.parse()
.expect("hardcoded dumb xpub"),
revocation_seal: None,
terminal_path: empty!(),
};
let mut xpub = None;
if let Some(first) = split.next() {
if first.starts_with('[') {
let fp = first.trim_start_matches('[');
if fp == "00000000" || fp == "m" {
account.master = XpubRef::Unknown;
} else {
account.master = XpubRef::from_str(fp)?;
}
for next in split.by_ref() {
if let Some((index, xpub_str)) = next.split_once(']') {
account.account_path.push(AccountStep::from_str(index)?);
xpub = Some(ExtendedPubKey::from_str(xpub_str)?);
break;
}
account.account_path.push(AccountStep::from_str(next)?);
}
} else {
xpub = Some(ExtendedPubKey::from_str(first)?);
}
}
if let Some(xpub) = xpub {
account.account_xpub = xpub;
} else {
return Err(ParseError::AccountXpubAbsent(s.to_owned()));
}
for next in split {
account.terminal_path.push(TerminalStep::from_str(next)?);
}
Ok(account)
}
pub fn from_str_lnpbp(s: &str) -> Result<DerivationAccount, ParseError> {
let mut split = s.split('/');
let mut first = split
.next()
.expect("split always must return at least one element");
let removed = first.strip_prefix("m=");
let seed_based = removed.is_some();
first = removed.unwrap_or(first);
let master = if !seed_based {
XpubRef::Unknown
} else {
XpubRef::from_str(first)?
};
let mut source_path = DerivationSubpath::new();
if !seed_based && !first.is_empty() {
source_path.push(AccountStep::from_str(first)?);
}
let mut split = split.rev();
let mut terminal_path = DerivationSubpath::new();
let (branch_index, branch_xpub, revocation_seal) = loop {
let step = if let Some(step) = split.next() {
step
} else if let XpubRef::Xpub(branch_xpub) = master {
break (None, branch_xpub, None);
} else {
return Err(ParseError::InvalidDerivationPathFormat(s.to_owned()));
};
if TerminalStep::from_str(step)
.map(|t| terminal_path.insert(0, t))
.is_err()
{
let mut branch_segment = step.split('?');
let mut derivation_part = branch_segment
.next()
.expect("split always has at least one item")
.split('=');
match (
derivation_part.next(),
derivation_part.next(),
derivation_part.next(),
branch_segment.next(),
branch_segment.next(),
) {
(index, Some(xpub), None, seal, None) => {
let branch_index = index.map(HardenedIndex::from_str).transpose()?;
let xpub = &xpub[1..xpub.len() - 1]; let branch_xpub = ExtendedPubKey::from_slip132_str(xpub)?;
let revocation_seal = seal
.map(|seal| {
OutPoint::from_str(seal)
.map_err(|_| ParseError::RevocationSeal(seal.to_owned()))
})
.transpose()?;
break (branch_index, branch_xpub, revocation_seal);
}
_ => return Err(ParseError::InvalidDerivationPathFormat(s.to_owned())),
}
}
};
for step in split.rev() {
source_path.push(AccountStep::from_str(step)?);
}
if let Some(branch_index) = branch_index {
source_path.push(AccountStep::from(branch_index));
}
Ok(DerivationAccount {
master,
account_path: source_path,
account_xpub: branch_xpub,
revocation_seal,
terminal_path,
})
}
}
impl Display for DerivationAccount {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if f.alternate() {
self.fmt_lnpbp(f)
} else {
self.fmt_bitcoin_core(f)
}
}
}
impl FromStr for DerivationAccount {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
DerivationAccount::from_str_bitcoin_core(s)
.or_else(|err| DerivationAccount::from_str_lnpbp(s).map_err(|_| err))
}
}
#[cfg(feature = "miniscript")]
impl miniscript::MiniscriptKey for DerivationAccount {
type Sha256 = Self;
type Hash256 = Self;
type Ripemd160 = Self;
type Hash160 = Self;
}
#[cfg(test)]
mod test {
use super::*;
fn xpubs() -> [ExtendedPubKey; 5] {
[
ExtendedPubKey::from_str("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8").unwrap(),
ExtendedPubKey::from_str("xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw").unwrap(),
ExtendedPubKey::from_str("xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ").unwrap(),
ExtendedPubKey::from_str("xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5").unwrap(),
ExtendedPubKey::from_str("xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV").unwrap(),
]
}
#[test]
fn trivial_paths_lnpbp() {
let xpubs = xpubs();
for path in vec![
s!("m=[tpubD8P81yEGkUEs1Hk3kdpSuwLBFZYwMCaVBLckeWVneqkJPivLe6uHAmtXt9RGUSRh5EqMecxinhAybyvgBzwKX3sLGGsuuJgnfzQ47arxTCp]/0/*"),
format!("/0h/5h/8h=[{}]/1/0/*", xpubs[0]),
format!(
"/7h=[{}]/0h/5h/8h=[{}]/1/0/*",
xpubs[2].identifier(),
xpubs[3]
),
format!(
"m=[{}]/0h/5h/8h=[{}]/1/*/*",
xpubs[4].identifier(),
xpubs[1]
),
format!(
"/6h=[{}]/0h/5h/8h=[{}]/1/{{0,1}}/*",
xpubs[2].fingerprint(),
xpubs[3]
),
format!(
"m=[{}]/0h/5h/8h=[{}]/1/0/*",
xpubs[4].fingerprint(),
xpubs[0]
),
format!(
"m=[{}]/0/*",
xpubs[0]
),
format!(
"/9h=[{}]/0/*",
xpubs[1]
),
format!("/1h=[{}]/0h/5h/8h=[{}]/1/0/*", xpubs[2], xpubs[3]),
format!("m=[{}]/0h/5h/8h=[{}]/1/0/*", xpubs[4], xpubs[3]),
] {
let account = DerivationAccount::from_str_lnpbp(&path).unwrap();
assert_eq!(format!("{:#}", account), path);
}
}
#[test]
fn trivial_paths_bitcoincore() {
let xpubs = xpubs();
for path in vec![
s!("[00000000/48h/0h/0h/2h]xpub69PnGxAGwEBNtGPnxd71p2QbHRZvjDG1BEza1sZdRbd7uWkjHqfGxMburhdEocC5ud2NpkbhwnM29c2zdqWS36wJue1BuJgMnLTpxpxzJe1/<0;1>/*"),
s!("tpubD8P81yEGkUEs1Hk3kdpSuwLBFZYwMCaVBLckeWVneqkJPivLe6uHAmtXt9RGUSRh5EqMecxinhAybyvgBzwKX3sLGGsuuJgnfzQ47arxTCp/0/*"),
format!("[00000000/0h/5h/8h]{}/1/0/*", xpubs[0]),
format!(
"[{}/0h/5h/8h]{}/1/0/*",
xpubs[2].fingerprint(),
xpubs[3]
),
format!(
"{}/0/*/*",
xpubs[0]
),
] {
let account = DerivationAccount::from_str_bitcoin_core(&path).unwrap();
assert_eq!(format!("{}", account), path);
}
}
}