Skip to content
Merged
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
73 changes: 60 additions & 13 deletions src/descriptor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod builder;
mod script_type;

use std::str::FromStr as _;
use std::{borrow::Cow, str::FromStr as _};

use bitcoin::{
bip32::{DerivationPath, Fingerprint},
Expand Down Expand Up @@ -125,9 +125,7 @@ pub struct Descriptors {
impl Descriptors {
/// Parse a single multipath descriptor into external and internal descriptors
pub fn try_from_line(line: &str) -> Result<Self, Error> {
let secp = &secp256k1::Secp256k1::signing_only();
let (descriptor, _keymap) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, line)?;
let descriptor = parse_descriptor_line(line)?;

if !descriptor.is_multipath() {
return Err(Error::MissingKeys);
Expand Down Expand Up @@ -357,12 +355,8 @@ impl TryFrom<&str> for Descriptors {
let external = lines[0];
let internal = lines[1];

let secp = &secp256k1::Secp256k1::signing_only();
let (internal_desc, _keymap) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, internal)?;

let (external_desc, _keymap) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, external)?;
let internal_desc = parse_descriptor_line(internal)?;
let external_desc = parse_descriptor_line(external)?;

Ok(Descriptors {
external: external_desc,
Expand Down Expand Up @@ -392,14 +386,36 @@ fn deserialize_descriptor<'de, D>(
where
D: serde::Deserializer<'de>,
{
let secp = &secp256k1::Secp256k1::signing_only();
let desc = String::deserialize(deserializer)?;
let (descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, desc.as_str())
.map_err(serde::de::Error::custom)?;
let descriptor = parse_descriptor_line(&desc).map_err(serde::de::Error::custom)?;

Ok(descriptor)
}

fn parse_descriptor_line(line: &str) -> Result<Descriptor<DescriptorPublicKey>, Error> {
let secp = &secp256k1::Secp256k1::signing_only();
let line = normalized_descriptor_line(line)?;
let (descriptor, _keymap) =
Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, line.as_ref())?;

Ok(descriptor)
}

fn normalized_descriptor_line(line: &str) -> Result<Cow<'_, str>, Error> {
let normalized = xpub::normalize_slip132_public_keys(line)?;

match normalized {
Cow::Owned(normalized) => {
if let Some((descriptor, _checksum)) = normalized.rsplit_once('#') {
return Ok(Cow::Owned(descriptor.to_string()));
}

Ok(Cow::Owned(normalized))
}
Cow::Borrowed(line) => Ok(Cow::Borrowed(line)),
}
}
Comment thread
praveenperera marked this conversation as resolved.

fn split_multipath_descriptor(
descriptor: Descriptor<DescriptorPublicKey>,
) -> Result<Descriptors, Error> {
Expand Down Expand Up @@ -556,6 +572,37 @@ mod tests {
assert_eq!(desc.internal, internal);
}

#[test]
fn test_parse_combination_descriptor_with_zpub() {
let descriptor = "wpkh([817e7be0/84h/0h/0h]zpub6rNrPrFwgm4wMBSysetK5tpLBS2HYT8TDKQA6amxFHKJUnQq8rNtc4JDfGYPbvF9wJyagPpG1Faqnfe3BB8XzKon8LwW9KkMWyAQ4RQHzB1/<0;1>/*)";
let desc = Descriptors::try_from_line(descriptor).unwrap();

assert_eq!(desc.external, known_desc().external);
assert_eq!(desc.internal, known_desc().internal);
}

#[test]
fn test_parse_combination_descriptor_with_zpub_and_checksum() {
let descriptor = "wpkh([817e7be0/84h/0h/0h]zpub6rNrPrFwgm4wMBSysetK5tpLBS2HYT8TDKQA6amxFHKJUnQq8rNtc4JDfGYPbvF9wJyagPpG1Faqnfe3BB8XzKon8LwW9KkMWyAQ4RQHzB1/<0;1>/*)#00000000";
let desc = Descriptors::try_from_line(descriptor).unwrap();

assert_eq!(desc.external, known_desc().external);
assert_eq!(desc.internal, known_desc().internal);
}

#[test]
fn test_parse_two_line_descriptor_with_zpub() {
let desc = r#"
wpkh([817e7be0/84h/0h/0h]zpub6rNrPrFwgm4wMBSysetK5tpLBS2HYT8TDKQA6amxFHKJUnQq8rNtc4JDfGYPbvF9wJyagPpG1Faqnfe3BB8XzKon8LwW9KkMWyAQ4RQHzB1/0/*)
wpkh([817e7be0/84h/0h/0h]zpub6rNrPrFwgm4wMBSysetK5tpLBS2HYT8TDKQA6amxFHKJUnQq8rNtc4JDfGYPbvF9wJyagPpG1Faqnfe3BB8XzKon8LwW9KkMWyAQ4RQHzB1/1/*)
"#;

let desc = Descriptors::try_from(desc).unwrap();

assert_eq!(desc.external.to_string(), known_desc().external.to_string());
assert_eq!(desc.internal.to_string(), known_desc().internal.to_string());
}

#[test]
fn test_fingerprint_getter() {
let single_sig = r#"{
Expand Down
99 changes: 98 additions & 1 deletion src/xpub.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::str::FromStr as _;
use std::{borrow::Cow, str::FromStr as _};

use bitcoin::{
base58,
bip32::{Fingerprint, Xpub as Bip32Xpub},
};

const EXTENDED_KEY_LENGTH: usize = 78;
const MIN_ENCODED_EXTENDED_PUBLIC_KEY_LENGTH: usize = 100;

const XPUB_VERSION: [u8; 4] = [0x04, 0x88, 0xb2, 0x1e];
const YPUB_VERSION: [u8; 4] = [0x04, 0x9d, 0x7c, 0xb2];
Expand Down Expand Up @@ -195,6 +196,50 @@ pub fn to_standard_extended_public_key(xpub: &str) -> Result<String, Error> {
Ok(standard_xpub)
}

/// Normalize all SLIP-132 public keys in a descriptor-like string
pub fn normalize_slip132_public_keys(string: &str) -> Result<Cow<'_, str>, Error> {
let mut normalized = None;
let mut last_index = 0;
let mut index = 0;

while index < string.len() {
let rest = &string[index..];
let Some(next_char) = rest.chars().next() else {
break;
};

if starts_with_slip132_prefix(rest) && has_base58_boundary_before(string, index) {
let end_index = index + base58_token_len(rest);
let token = &string[index..end_index];

if token.len() < MIN_ENCODED_EXTENDED_PUBLIC_KEY_LENGTH {
index += next_char.len_utf8();
continue;
}

let replacement = to_standard_extended_public_key(token)?;
let normalized = normalized.get_or_insert_with(String::new);

normalized.push_str(&string[last_index..index]);
normalized.push_str(&replacement);

last_index = end_index;
index = end_index;
continue;
}

index += next_char.len_utf8();
}

match normalized {
Some(mut normalized) => {
normalized.push_str(&string[last_index..]);
Ok(Cow::Owned(normalized))
}
None => Ok(Cow::Borrowed(string)),
}
}

/// Convert a zpub to standard xpub form
#[deprecated(since = "0.6.0", note = "use to_standard_extended_public_key")]
pub fn zpub_to_xpub(zpub: &str) -> Result<String, Error> {
Expand Down Expand Up @@ -264,6 +309,38 @@ fn version_info(version: [u8; 4]) -> Result<VersionInfo, Error> {
Ok(info)
}

fn starts_with_slip132_prefix(string: &str) -> bool {
["ypub", "zpub", "upub", "vpub"]
.iter()
.any(|prefix| string.starts_with(prefix))
}

fn has_base58_boundary_before(string: &str, index: usize) -> bool {
if index == 0 {
return true;
}

string[..index]
.chars()
.next_back()
.map(|char| !is_base58_char(char))
.unwrap_or(true)
}

fn base58_token_len(string: &str) -> usize {
string
.char_indices()
.find_map(|(index, char)| (!is_base58_char(char)).then_some(index))
.unwrap_or(string.len())
}

fn is_base58_char(char: char) -> bool {
matches!(
char,
'1'..='9' | 'A'..='H' | 'J'..='N' | 'P'..='Z' | 'a'..='k' | 'm'..='z'
)
}

struct VersionInfo {
original_format: OriginalFormat,
standard_version: [u8; 4],
Expand Down Expand Up @@ -343,6 +420,26 @@ mod tests {
assert_eq!(result, BIP84_XPUB);
}

#[test]
fn test_normalize_slip132_public_keys_in_descriptor() {
let descriptor = format!("wpkh([73c5da0a/84h/0h/0h]{BIP84_ZPUB}/<0;1>/*)#ignored");
let expected = format!("wpkh([73c5da0a/84h/0h/0h]{BIP84_XPUB}/<0;1>/*)#ignored");

let result = normalize_slip132_public_keys(&descriptor).unwrap();

assert_eq!(result, expected);
}

#[test]
fn test_normalize_slip132_public_keys_skips_short_tokens() {
let descriptor = format!("zpubINVALID {BIP84_ZPUB}");
let expected = format!("zpubINVALID {BIP84_XPUB}");

let result = normalize_slip132_public_keys(&descriptor).unwrap();

assert_eq!(result, expected);
}

#[test]
fn test_invalid_slip132_key() {
let invalid = "zpubINVALID";
Expand Down
Loading