Skip to content

Commit 808dc7c

Browse files
committed
miniscript: add a scaffold of parsing wsh(<miniscript>) policies
We add a policies.rs file with functions to parse and validate a policy containing miniscript. Policy specification: bitcoin/bips#1389 We only support `wsh(<miniscript>)` policies for now. Taproot or other policy fragments could be added in the future. More validation checks are coming in later commits, such as: - At least one key must be ours - No duplicate keys possible in the policy - No duplicate keys in the keys list - All keys in the keys list are used, and all key references (@0, ...) are valid. - ...? Also coming in later commits: - Derive a pkScript at a keypath, generate receive address from that - Policy registration (very similar to how multisig registration works today) - Signing transactions
1 parent b8c5dac commit 808dc7c

File tree

2 files changed

+249
-3
lines changed

2 files changed

+249
-3
lines changed

src/rust/bitbox02-rust/src/hww/api/bitcoin.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub mod common;
2121
pub mod keypath;
2222
mod multisig;
2323
pub mod params;
24+
mod policies;
2425
mod registration;
2526
mod script;
2627
pub mod signmsg;
@@ -38,8 +39,8 @@ use crate::keystore;
3839
use pb::btc_pub_request::{Output, XPubType};
3940
use pb::btc_request::Request;
4041
use pb::btc_script_config::multisig::ScriptType as MultisigScriptType;
41-
use pb::btc_script_config::Multisig;
4242
use pb::btc_script_config::{Config, SimpleType};
43+
use pb::btc_script_config::{Multisig, Policy};
4344
use pb::response::Response;
4445
use pb::BtcCoin;
4546
use pb::BtcScriptConfig;
@@ -144,7 +145,7 @@ pub fn derive_address_simple(
144145
.address(coin_params)?)
145146
}
146147

147-
/// Processes a SimpleType (single-sig) adress api call.
148+
/// Processes a SimpleType (single-sig) address api call.
148149
async fn address_simple(
149150
coin: BtcCoin,
150151
simple_type: SimpleType,
@@ -164,7 +165,7 @@ async fn address_simple(
164165
Ok(Response::Pub(pb::PubResponse { r#pub: address }))
165166
}
166167

167-
/// Processes a multisig adress api call.
168+
/// Processes a multisig address api call.
168169
pub async fn address_multisig(
169170
coin: BtcCoin,
170171
multisig: &Multisig,
@@ -205,6 +206,25 @@ pub async fn address_multisig(
205206
Ok(Response::Pub(pb::PubResponse { r#pub: address }))
206207
}
207208

209+
/// Processes a policy address api call.
210+
async fn address_policy(
211+
coin: BtcCoin,
212+
policy: &Policy,
213+
_keypath: &[u32],
214+
_display: bool,
215+
) -> Result<Response, Error> {
216+
let parsed = policies::parse(policy)?;
217+
parsed.validate(coin)?;
218+
219+
// TODO: check that the policy was registered before.
220+
221+
// TODO: confirm policy registration
222+
223+
// TODO: create address at keypath and do user verification
224+
225+
todo!();
226+
}
227+
208228
/// Handle a Bitcoin xpub/address protobuf api call.
209229
pub async fn process_pub(request: &pb::BtcPubRequest) -> Result<Response, Error> {
210230
let coin = match BtcCoin::from_i32(request.coin) {
@@ -233,6 +253,9 @@ pub async fn process_pub(request: &pb::BtcPubRequest) -> Result<Response, Error>
233253
Some(Output::ScriptConfig(BtcScriptConfig {
234254
config: Some(Config::Multisig(ref multisig)),
235255
})) => address_multisig(coin, multisig, &request.keypath, request.display).await,
256+
Some(Output::ScriptConfig(BtcScriptConfig {
257+
config: Some(Config::Policy(ref policy)),
258+
})) => address_policy(coin, policy, &request.keypath, request.display).await,
236259
_ => Err(Error::InvalidInput),
237260
}
238261
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright 2023 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use super::pb;
16+
use super::Error;
17+
use pb::BtcCoin;
18+
19+
use pb::btc_script_config::Policy;
20+
21+
use alloc::string::String;
22+
23+
use core::str::FromStr;
24+
25+
// Arbitrary limit of keys that can be present in a policy.
26+
const MAX_KEYS: usize = 20;
27+
28+
// We only support Bitcoin testnet for now.
29+
fn check_enabled(coin: BtcCoin) -> Result<(), Error> {
30+
if !matches!(coin, BtcCoin::Tbtc) {
31+
return Err(Error::InvalidInput);
32+
}
33+
Ok(())
34+
}
35+
36+
/// See `ParsedPolicy`.
37+
#[derive(Debug)]
38+
pub struct Wsh<'a> {
39+
policy: &'a Policy,
40+
miniscript_expr: miniscript::Miniscript<String, miniscript::Segwitv0>,
41+
}
42+
43+
/// Result of `parse()`.
44+
#[derive(Debug)]
45+
pub enum ParsedPolicy<'a> {
46+
// `wsh(...)` policies
47+
Wsh(Wsh<'a>),
48+
// `tr(...)` Taproot etc. in the future.
49+
}
50+
51+
impl<'a> ParsedPolicy<'a> {
52+
fn get_policy(&self) -> &Policy {
53+
match self {
54+
Self::Wsh(Wsh { ref policy, .. }) => policy,
55+
}
56+
}
57+
58+
/// Validate a policy.
59+
/// - Coin is supported (only Bitcoin testnet for now)
60+
/// - Number of keys
61+
/// - TODO: many more checks.
62+
pub fn validate(&self, coin: BtcCoin) -> Result<(), Error> {
63+
check_enabled(coin)?;
64+
65+
let policy = self.get_policy();
66+
67+
if policy.keys.len() > MAX_KEYS {
68+
return Err(Error::InvalidInput);
69+
}
70+
71+
// TODO: more checks
72+
73+
Ok(())
74+
}
75+
}
76+
77+
/// Parses a policy as specified by 'Wallet policies': https://github.com/bitcoin/bips/pull/1389.
78+
/// Only `wsh(<miniscript expression>)` is supported for now.
79+
/// Example: `wsh(pk(@0/**))`.
80+
///
81+
/// The parsed output keeps the key strings as is (e.g. "@0/**"). They will be processed and
82+
/// replaced with actual pubkeys in a later step.
83+
pub fn parse(policy: &Policy) -> Result<ParsedPolicy, Error> {
84+
let desc = policy.policy.as_str();
85+
match desc.as_bytes() {
86+
// Match wsh(...).
87+
[b'w', b's', b'h', b'(', .., b')'] => {
88+
let miniscript_expr: miniscript::Miniscript<String, miniscript::Segwitv0> =
89+
miniscript::Miniscript::from_str(&desc[4..desc.len() - 1])
90+
.or(Err(Error::InvalidInput))?;
91+
92+
Ok(ParsedPolicy::Wsh(Wsh {
93+
policy,
94+
miniscript_expr,
95+
}))
96+
}
97+
_ => Err(Error::InvalidInput),
98+
}
99+
}
100+
101+
#[cfg(test)]
102+
mod tests {
103+
use super::*;
104+
105+
use alloc::vec::Vec;
106+
107+
use crate::bip32::parse_xpub;
108+
use bitbox02::testing::mock_unlocked;
109+
use util::bip32::HARDENED;
110+
111+
const SOME_XPUB_1: &str = "xpub6FMWuwbCA9KhoRzAMm63ZhLspk5S2DM5sePo8J8mQhcS1xyMbAqnc7Q7UescVEVFCS6qBMQLkEJWQ9Z3aDPgBov5nFUYxsJhwumsxM4npSo";
112+
113+
const KEYPATH_ACCOUNT: &[u32] = &[48 + HARDENED, 1 + HARDENED, 0 + HARDENED, 3 + HARDENED];
114+
115+
// Creates a policy key without fingerprint/keypath from an xpub string.
116+
fn make_key(xpub: &str) -> pb::KeyOriginInfo {
117+
pb::KeyOriginInfo {
118+
root_fingerprint: vec![],
119+
keypath: vec![],
120+
xpub: Some(parse_xpub(xpub).unwrap()),
121+
}
122+
}
123+
124+
// Creates a policy for one of our own keys at keypath.
125+
fn make_our_key(keypath: &[u32]) -> pb::KeyOriginInfo {
126+
let our_xpub = crate::keystore::get_xpub(keypath).unwrap();
127+
pb::KeyOriginInfo {
128+
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
129+
keypath: keypath.to_vec(),
130+
xpub: Some(our_xpub.into()),
131+
}
132+
}
133+
134+
fn make_policy(policy: &str, keys: &[pb::KeyOriginInfo]) -> Policy {
135+
Policy {
136+
policy: policy.into(),
137+
keys: keys.to_vec(),
138+
}
139+
}
140+
141+
#[test]
142+
fn test_parse_wsh_miniscript() {
143+
// Parse a valid example and check that the keys are collected as is as strings.
144+
let policy = make_policy("wsh(pk(@0/**))", &[]);
145+
match parse(&policy).unwrap() {
146+
ParsedPolicy::Wsh(Wsh {
147+
ref miniscript_expr,
148+
..
149+
}) => {
150+
assert_eq!(
151+
miniscript_expr.iter_pk().collect::<Vec<String>>(),
152+
vec!["@0/**"]
153+
);
154+
}
155+
}
156+
157+
// Parse another valid example and check that the keys are collected as is as strings.
158+
let policy = make_policy("wsh(or_b(pk(@0/**),s:pk(@1/**)))", &[]);
159+
match parse(&policy).unwrap() {
160+
ParsedPolicy::Wsh(Wsh {
161+
ref miniscript_expr,
162+
..
163+
}) => {
164+
assert_eq!(
165+
miniscript_expr.iter_pk().collect::<Vec<String>>(),
166+
vec!["@0/**", "@1/**"]
167+
);
168+
}
169+
}
170+
171+
// Unknown top-level fragment.
172+
assert_eq!(
173+
parse(&make_policy("unknown(pk(@0/**))", &[])).unwrap_err(),
174+
Error::InvalidInput,
175+
);
176+
177+
// Unknown script fragment.
178+
assert_eq!(
179+
parse(&make_policy("wsh(unknown(@0/**))", &[])).unwrap_err(),
180+
Error::InvalidInput,
181+
);
182+
183+
// Miniscript type-check fails (should be `or_b(pk(@0/**),s:pk(@1/**))`).
184+
assert_eq!(
185+
parse(&make_policy("wsh(or_b(pk(@0/**),pk(@1/**)))", &[])).unwrap_err(),
186+
Error::InvalidInput,
187+
);
188+
}
189+
190+
#[test]
191+
fn test_parse_validate() {
192+
mock_unlocked();
193+
194+
let our_key = make_our_key(KEYPATH_ACCOUNT);
195+
196+
// All good.
197+
assert!(parse(&make_policy("wsh(pk(@0/**))", &[our_key.clone()]))
198+
.unwrap()
199+
.validate(BtcCoin::Tbtc)
200+
.is_ok());
201+
202+
// Unsupported coins
203+
for coin in [BtcCoin::Btc, BtcCoin::Ltc, BtcCoin::Tltc] {
204+
assert_eq!(
205+
parse(&make_policy("wsh(pk(@0/**))", &[our_key.clone()]))
206+
.unwrap()
207+
.validate(coin),
208+
Err(Error::InvalidInput)
209+
);
210+
}
211+
212+
// Too many keys.
213+
let many_keys: Vec<pb::KeyOriginInfo> = (0..=20)
214+
.map(|i| make_our_key(&[48 + HARDENED, 1 + HARDENED, i + HARDENED, 3 + HARDENED]))
215+
.collect();
216+
assert_eq!(
217+
parse(&make_policy("wsh(pk(@0/**))", &many_keys))
218+
.unwrap()
219+
.validate(BtcCoin::Tbtc),
220+
Err(Error::InvalidInput)
221+
);
222+
}
223+
}

0 commit comments

Comments
 (0)