Skip to content

Commit 78671ae

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 98bee99 commit 78671ae

File tree

2 files changed

+244
-3
lines changed

2 files changed

+244
-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: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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+
pub fn parse(policy: &Policy) -> Result<ParsedPolicy, Error> {
81+
let desc = policy.policy.as_str();
82+
match desc.as_bytes() {
83+
// Match wsh(...).
84+
[b'w', b's', b'h', b'(', .., b')'] => {
85+
let miniscript_expr: miniscript::Miniscript<String, miniscript::Segwitv0> =
86+
miniscript::Miniscript::from_str(&desc[4..desc.len() - 1])
87+
.or(Err(Error::InvalidInput))?;
88+
89+
Ok(ParsedPolicy::Wsh(Wsh {
90+
policy,
91+
miniscript_expr,
92+
}))
93+
}
94+
_ => Err(Error::InvalidInput),
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use super::*;
101+
102+
use alloc::vec::Vec;
103+
104+
use crate::bip32::parse_xpub;
105+
use bitbox02::testing::mock_unlocked;
106+
use util::bip32::HARDENED;
107+
108+
const SOME_XPUB_1: &str = "xpub6FMWuwbCA9KhoRzAMm63ZhLspk5S2DM5sePo8J8mQhcS1xyMbAqnc7Q7UescVEVFCS6qBMQLkEJWQ9Z3aDPgBov5nFUYxsJhwumsxM4npSo";
109+
110+
const KEYPATH_ACCOUNT: &[u32] = &[48 + HARDENED, 1 + HARDENED, 0 + HARDENED, 3 + HARDENED];
111+
112+
// Creates a policy key without fingerprint/keypath from an xpub string.
113+
fn make_key(xpub: &str) -> pb::KeyOriginInfo {
114+
pb::KeyOriginInfo {
115+
root_fingerprint: vec![],
116+
keypath: vec![],
117+
xpub: Some(parse_xpub(xpub).unwrap()),
118+
}
119+
}
120+
121+
// Creates a policy for one of our own keys at keypath.
122+
fn make_our_key(keypath: &[u32]) -> pb::KeyOriginInfo {
123+
let our_xpub = crate::keystore::get_xpub(keypath).unwrap();
124+
pb::KeyOriginInfo {
125+
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
126+
keypath: keypath.to_vec(),
127+
xpub: Some(our_xpub.into()),
128+
}
129+
}
130+
131+
fn make_policy(policy: &str, keys: &[pb::KeyOriginInfo]) -> Policy {
132+
Policy {
133+
policy: policy.into(),
134+
keys: keys.to_vec(),
135+
}
136+
}
137+
138+
#[test]
139+
fn test_parse_wsh_miniscript() {
140+
// Parse a valid example and check that the keys are collected as is as strings.
141+
let policy = make_policy("wsh(pk(@0/**))", &[]);
142+
match parse(&policy).unwrap() {
143+
ParsedPolicy::Wsh(Wsh {
144+
ref miniscript_expr,
145+
..
146+
}) => {
147+
assert_eq!(
148+
miniscript_expr.iter_pk().collect::<Vec<String>>(),
149+
vec!["@0/**"]
150+
);
151+
}
152+
}
153+
154+
// Parse another valid example and check that the keys are collected as is as strings.
155+
let policy = make_policy("wsh(or_b(pk(@0/**),s:pk(@1/**)))", &[]);
156+
match parse(&policy).unwrap() {
157+
ParsedPolicy::Wsh(Wsh {
158+
ref miniscript_expr,
159+
..
160+
}) => {
161+
assert_eq!(
162+
miniscript_expr.iter_pk().collect::<Vec<String>>(),
163+
vec!["@0/**", "@1/**"]
164+
);
165+
}
166+
}
167+
168+
// Unknown top-level fragment.
169+
assert_eq!(
170+
parse(&make_policy("unknown(pk(@0/**))", &[])).unwrap_err(),
171+
Error::InvalidInput,
172+
);
173+
174+
// Unknown script fragment.
175+
assert_eq!(
176+
parse(&make_policy("wsh(unknown(@0/**))", &[])).unwrap_err(),
177+
Error::InvalidInput,
178+
);
179+
180+
// Miniscript type-check fails (should be `or_b(pk(@0/**),s:pk(@1/**))`).
181+
assert_eq!(
182+
parse(&make_policy("wsh(or_b(pk(@0/**),pk(@1/**)))", &[])).unwrap_err(),
183+
Error::InvalidInput,
184+
);
185+
}
186+
187+
#[test]
188+
fn test_parse_validate() {
189+
let our_key = make_our_key(KEYPATH_ACCOUNT);
190+
191+
// All good.
192+
assert!(parse(&make_policy("wsh(pk(@0/**))", &[our_key.clone()]))
193+
.unwrap()
194+
.validate(BtcCoin::Tbtc)
195+
.is_ok());
196+
197+
// Unsupported coins
198+
for coin in [BtcCoin::Btc, BtcCoin::Ltc, BtcCoin::Tltc] {
199+
assert_eq!(
200+
parse(&make_policy("wsh(pk(@0/**))", &[our_key.clone()]))
201+
.unwrap()
202+
.validate(coin),
203+
Err(Error::InvalidInput)
204+
);
205+
}
206+
207+
// Too many keys.
208+
let many_keys: Vec<pb::KeyOriginInfo> = (0..=20)
209+
.map(|i| make_our_key(&[48 + HARDENED, 1 + HARDENED, i + HARDENED, 3 + HARDENED]))
210+
.collect();
211+
assert_eq!(
212+
parse(&make_policy("wsh(pk(@0/**))", &many_keys))
213+
.unwrap()
214+
.validate(BtcCoin::Tbtc),
215+
Err(Error::InvalidInput)
216+
);
217+
}
218+
}

0 commit comments

Comments
 (0)