Skip to content

Commit 6387817

Browse files
committed
feat: add support for BIP-388 wallet policies
Implemented BIP-388 by introducing a new type, `WalletPolicy`, which can be used to create descriptor templates. The idea is pretty simple and only really required making a new type that implements `MiniscriptKey`, then the existing parser and `Translator` trait takes care of the rest. There are a few edge cases that require validation, which isn't so nice, but works for now. Test cases that require BIP-390 and BIP-387's sortedmulti_a are commented out and issues could arise with those test vectors when/if those BIPs are implemented. See `WalletPolicy`'s doc and the unit tests for usage.
1 parent 65d5555 commit 6387817

4 files changed

Lines changed: 536 additions & 0 deletions

File tree

src/descriptor/key.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use bitcoin::key::{PublicKey, XOnlyPublicKey};
1212
use bitcoin::secp256k1::{Secp256k1, Signing, Verification};
1313
use bitcoin::NetworkKind;
1414

15+
use super::WalletPolicyError;
1516
use crate::prelude::*;
1617
#[cfg(feature = "serde")]
1718
use crate::serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -497,6 +498,7 @@ impl error::Error for DescriptorKeyParseError {
497498
#[derive(Debug, PartialEq, Eq, Clone)]
498499
pub enum XKeyParseError {
499500
Bip32(bip32::Error),
501+
Bip388(WalletPolicyError),
500502
}
501503

502504
#[cfg(feature = "std")]
@@ -508,6 +510,7 @@ impl fmt::Display for XKeyParseError {
508510
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509511
match self {
510512
XKeyParseError::Bip32(err) => err.fmt(f),
513+
XKeyParseError::Bip388(err) => err.fmt(f),
511514
}
512515
}
513516
}

src/descriptor/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ pub use self::tr::{
5353
pub mod checksum;
5454
mod key;
5555
mod key_map;
56+
mod wallet_policy;
5657

5758
pub use self::key::{
5859
DefiniteDescriptorKey, DerivPaths, DescriptorKeyParseError, DescriptorMultiXKey,
5960
DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, InnerXKey, MalformedKeyDataKind,
6061
NonDefiniteKeyError, SinglePriv, SinglePub, SinglePubKey, Wildcard, XKeyNetwork,
6162
};
6263
pub use self::key_map::KeyMap;
64+
pub use self::wallet_policy::{WalletPolicy, WalletPolicyError};
6365

6466
/// Script descriptor
6567
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
use core::fmt::{self, Display, Write};
4+
use core::str::FromStr;
5+
6+
use super::{DerivPaths, DescriptorKeyParseError, Wildcard};
7+
use crate::descriptor::key::{fmt_derivation_paths, parse_xkey_deriv};
8+
use crate::descriptor::WalletPolicyError;
9+
use crate::{BTreeSet, MiniscriptKey, String};
10+
11+
const RECEIVE_CHANGE_SHORTHAND: &str = "**";
12+
const RECEIVE_CHANGE_PATH: &str = "<0;1>/*";
13+
14+
/// A key expression type based off of the description of KEY and KP in BIP-388.
15+
/// Used as a `Pk` in `Descriptor<Pk>`
16+
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
17+
pub struct KeyExpression {
18+
/// The numeric part of key index (KI)
19+
pub index: KeyIndex,
20+
/// The derivation paths of this key
21+
pub derivation_paths: DerivPaths,
22+
/// The wildcard value
23+
pub wildcard: Wildcard,
24+
}
25+
26+
#[derive(Debug, Clone, Copy, Hash, PartialOrd, Ord, PartialEq, Eq)]
27+
pub struct KeyIndex(pub u32);
28+
29+
impl KeyExpression {
30+
pub fn is_disjoint(&self, other: &KeyExpression) -> bool {
31+
let lhs: BTreeSet<_> = self
32+
.derivation_paths
33+
.paths()
34+
.iter()
35+
.flat_map(|p| p.into_iter().copied())
36+
.collect();
37+
38+
!other
39+
.derivation_paths
40+
.paths()
41+
.iter()
42+
.flat_map(|p| p.into_iter())
43+
.any(|cn| lhs.contains(cn))
44+
}
45+
}
46+
47+
impl TryFrom<&str> for KeyExpression {
48+
type Error = DescriptorKeyParseError;
49+
50+
fn try_from(s: &str) -> Result<Self, Self::Error> {
51+
let path = match s.split_once('/') {
52+
Some((_placeholder, path)) => path,
53+
None => return Err(WalletPolicyError::KeyExpressionParseMustHaveDerivPath.into()),
54+
};
55+
if path != RECEIVE_CHANGE_SHORTHAND && !valid_unhardened_derivation_path(path) {
56+
return Err(WalletPolicyError::KeyExpressionParseInvalidDerivPath.into());
57+
}
58+
let (ki, derivation_paths, wildcard) =
59+
parse_xkey_deriv(&s.replace(RECEIVE_CHANGE_SHORTHAND, RECEIVE_CHANGE_PATH))?;
60+
Ok(KeyExpression {
61+
index: ki,
62+
derivation_paths: DerivPaths::new(derivation_paths)
63+
.ok_or(WalletPolicyError::KeyExpressionParseMustHaveDerivPath)?,
64+
wildcard,
65+
})
66+
}
67+
}
68+
69+
// Returns true if `path` is a string of the form /<NUM;NUM>/*, for two distinct
70+
// decimal numbers NUM representing unhardened derivations
71+
// NOTE: the prefix '/' should be stripped in the caller
72+
fn valid_unhardened_derivation_path(path: &str) -> bool {
73+
let (left, right) = match path.split_once(';') {
74+
Some(pair) => pair,
75+
None => return false,
76+
};
77+
let left_num = match left.strip_prefix("<") {
78+
Some(num) => num,
79+
None => return false,
80+
};
81+
let right_num = match right.strip_suffix(">/*") {
82+
Some(num) => num,
83+
None => return false,
84+
};
85+
matches!(
86+
(left_num.parse::<u32>(), right_num.parse::<u32>()),
87+
(Ok(a), Ok(b)) if a < b
88+
)
89+
}
90+
91+
impl FromStr for KeyExpression {
92+
type Err = DescriptorKeyParseError;
93+
94+
fn from_str(s: &str) -> Result<Self, Self::Err> { s.try_into() }
95+
}
96+
97+
impl Display for KeyExpression {
98+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99+
self.index.fmt(f)?;
100+
let mut path = String::new();
101+
fmt_derivation_paths(&mut path, self.derivation_paths.paths())?;
102+
write!(&mut path, "{}", self.wildcard)?;
103+
write!(f, "{}", path.replace(RECEIVE_CHANGE_PATH, RECEIVE_CHANGE_SHORTHAND))
104+
}
105+
}
106+
107+
impl MiniscriptKey for KeyExpression {
108+
type Sha256 = String;
109+
type Hash256 = String;
110+
type Ripemd160 = String;
111+
type Hash160 = String;
112+
113+
fn is_x_only_key(&self) -> bool { false }
114+
fn num_der_paths(&self) -> usize { self.derivation_paths.paths().len() }
115+
}
116+
117+
impl FromStr for KeyIndex {
118+
type Err = WalletPolicyError;
119+
120+
fn from_str(s: &str) -> Result<Self, Self::Err> {
121+
let mut chars = s.chars();
122+
match chars.next() {
123+
Some('@') => {
124+
let index_str = chars.take_while(char::is_ascii_digit).collect::<String>();
125+
let index = index_str
126+
.parse()
127+
.map_err(|_| WalletPolicyError::KeyIndexParseInvalidIndex(index_str))?;
128+
Ok(KeyIndex(index))
129+
}
130+
Some(ch) => Err(WalletPolicyError::KeyIndexParseExpectedAtSign(ch)),
131+
None => Err(WalletPolicyError::KeyIndexParseInvalidIndex(s.into())),
132+
}
133+
}
134+
}
135+
136+
impl Display for KeyIndex {
137+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "@{}", self.0) }
138+
}
139+
140+
#[cfg(test)]
141+
mod tests {
142+
use super::*;
143+
144+
#[test]
145+
fn can_test_disjoin_deriv_paths() {
146+
assert!(!KeyExpression::from_str("@0/<0;1>/*")
147+
.unwrap()
148+
.is_disjoint(&KeyExpression::from_str("@0/<1;2>/*").unwrap()));
149+
}
150+
}

0 commit comments

Comments
 (0)