Skip to content

Commit

Permalink
miniscript: add a scaffold of parsing wsh(<miniscript>) policies
Browse files Browse the repository at this point in the history
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
  • Loading branch information
benma committed Jun 29, 2023
1 parent a6971f0 commit 5625aec
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 3 deletions.
29 changes: 26 additions & 3 deletions src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod common;
pub mod keypath;
mod multisig;
pub mod params;
mod policies;
mod registration;
mod script;
pub mod signmsg;
Expand All @@ -38,8 +39,8 @@ use crate::keystore;
use pb::btc_pub_request::{Output, XPubType};
use pb::btc_request::Request;
use pb::btc_script_config::multisig::ScriptType as MultisigScriptType;
use pb::btc_script_config::Multisig;
use pb::btc_script_config::{Config, SimpleType};
use pb::btc_script_config::{Multisig, Policy};
use pb::response::Response;
use pb::BtcCoin;
use pb::BtcScriptConfig;
Expand Down Expand Up @@ -144,7 +145,7 @@ pub fn derive_address_simple(
.address(coin_params)?)
}

/// Processes a SimpleType (single-sig) adress api call.
/// Processes a SimpleType (single-sig) address api call.
async fn address_simple(
coin: BtcCoin,
simple_type: SimpleType,
Expand All @@ -164,7 +165,7 @@ async fn address_simple(
Ok(Response::Pub(pb::PubResponse { r#pub: address }))
}

/// Processes a multisig adress api call.
/// Processes a multisig address api call.
pub async fn address_multisig(
coin: BtcCoin,
multisig: &Multisig,
Expand Down Expand Up @@ -205,6 +206,25 @@ pub async fn address_multisig(
Ok(Response::Pub(pb::PubResponse { r#pub: address }))
}

/// Processes a policy address api call.
async fn address_policy(
coin: BtcCoin,
policy: &Policy,
_keypath: &[u32],
_display: bool,
) -> Result<Response, Error> {
let parsed = policies::parse(policy)?;
parsed.validate(coin)?;

// TODO: check that the policy was registered before.

// TODO: confirm policy registration

// TODO: create address at keypath and do user verification

todo!();
}

/// Handle a Bitcoin xpub/address protobuf api call.
pub async fn process_pub(request: &pb::BtcPubRequest) -> Result<Response, Error> {
let coin = match BtcCoin::from_i32(request.coin) {
Expand Down Expand Up @@ -233,6 +253,9 @@ pub async fn process_pub(request: &pb::BtcPubRequest) -> Result<Response, Error>
Some(Output::ScriptConfig(BtcScriptConfig {
config: Some(Config::Multisig(ref multisig)),
})) => address_multisig(coin, multisig, &request.keypath, request.display).await,
Some(Output::ScriptConfig(BtcScriptConfig {
config: Some(Config::Policy(ref policy)),
})) => address_policy(coin, policy, &request.keypath, request.display).await,
_ => Err(Error::InvalidInput),
}
}
Expand Down
223 changes: 223 additions & 0 deletions src/rust/bitbox02-rust/src/hww/api/bitcoin/policies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// Copyright 2023 Shift Crypto AG
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use super::pb;
use super::Error;
use pb::BtcCoin;

use pb::btc_script_config::Policy;

use alloc::string::String;

use core::str::FromStr;

// Arbitrary limit of keys that can be present in a policy.
const MAX_KEYS: usize = 20;

// We only support Bitcoin testnet for now.
fn check_enabled(coin: BtcCoin) -> Result<(), Error> {
if !matches!(coin, BtcCoin::Tbtc) {
return Err(Error::InvalidInput);
}
Ok(())
}

/// See `ParsedPolicy`.
#[derive(Debug)]
pub struct Wsh<'a> {
policy: &'a Policy,
miniscript_expr: miniscript::Miniscript<String, miniscript::Segwitv0>,
}

/// Result of `parse()`.
#[derive(Debug)]
pub enum ParsedPolicy<'a> {
// `wsh(...)` policies
Wsh(Wsh<'a>),
// `tr(...)` Taproot etc. in the future.
}

impl<'a> ParsedPolicy<'a> {
fn get_policy(&self) -> &Policy {
match self {
Self::Wsh(Wsh { ref policy, .. }) => policy,
}
}

/// Validate a policy.
/// - Coin is supported (only Bitcoin testnet for now)
/// - Number of keys
/// - TODO: many more checks.
pub fn validate(&self, coin: BtcCoin) -> Result<(), Error> {
check_enabled(coin)?;

let policy = self.get_policy();

if policy.keys.len() > MAX_KEYS {
return Err(Error::InvalidInput);
}

// TODO: more checks

Ok(())
}
}

/// Parses a policy as specified by 'Wallet policies': https://github.com/bitcoin/bips/pull/1389.
/// Only `wsh(<miniscript expression>)` is supported for now.
/// Example: `wsh(pk(@0/**))`.
///
/// The parsed output keeps the key strings as is (e.g. "@0/**"). They will be processed and
/// replaced with actual pubkeys in a later step.
pub fn parse(policy: &Policy) -> Result<ParsedPolicy, Error> {
let desc = policy.policy.as_str();
match desc.as_bytes() {
// Match wsh(...).
[b'w', b's', b'h', b'(', .., b')'] => {
let miniscript_expr: miniscript::Miniscript<String, miniscript::Segwitv0> =
miniscript::Miniscript::from_str(&desc[4..desc.len() - 1])
.or(Err(Error::InvalidInput))?;

Ok(ParsedPolicy::Wsh(Wsh {
policy,
miniscript_expr,
}))
}
_ => Err(Error::InvalidInput),
}
}

#[cfg(test)]
mod tests {
use super::*;

use alloc::vec::Vec;

use crate::bip32::parse_xpub;
use bitbox02::testing::mock_unlocked;
use util::bip32::HARDENED;

const SOME_XPUB_1: &str = "xpub6FMWuwbCA9KhoRzAMm63ZhLspk5S2DM5sePo8J8mQhcS1xyMbAqnc7Q7UescVEVFCS6qBMQLkEJWQ9Z3aDPgBov5nFUYxsJhwumsxM4npSo";

const KEYPATH_ACCOUNT: &[u32] = &[48 + HARDENED, 1 + HARDENED, 0 + HARDENED, 3 + HARDENED];

// Creates a policy key without fingerprint/keypath from an xpub string.
fn make_key(xpub: &str) -> pb::KeyOriginInfo {
pb::KeyOriginInfo {
root_fingerprint: vec![],
keypath: vec![],
xpub: Some(parse_xpub(xpub).unwrap()),
}
}

// Creates a policy for one of our own keys at keypath.
fn make_our_key(keypath: &[u32]) -> pb::KeyOriginInfo {
let our_xpub = crate::keystore::get_xpub(keypath).unwrap();
pb::KeyOriginInfo {
root_fingerprint: crate::keystore::root_fingerprint().unwrap(),
keypath: keypath.to_vec(),
xpub: Some(our_xpub.into()),
}
}

fn make_policy(policy: &str, keys: &[pb::KeyOriginInfo]) -> Policy {
Policy {
policy: policy.into(),
keys: keys.to_vec(),
}
}

#[test]
fn test_parse_wsh_miniscript() {
// Parse a valid example and check that the keys are collected as is as strings.
let policy = make_policy("wsh(pk(@0/**))", &[]);
match parse(&policy).unwrap() {
ParsedPolicy::Wsh(Wsh {
ref miniscript_expr,
..
}) => {
assert_eq!(
miniscript_expr.iter_pk().collect::<Vec<String>>(),
vec!["@0/**"]
);
}
}

// Parse another valid example and check that the keys are collected as is as strings.
let policy = make_policy("wsh(or_b(pk(@0/**),s:pk(@1/**)))", &[]);
match parse(&policy).unwrap() {
ParsedPolicy::Wsh(Wsh {
ref miniscript_expr,
..
}) => {
assert_eq!(
miniscript_expr.iter_pk().collect::<Vec<String>>(),
vec!["@0/**", "@1/**"]
);
}
}

// Unknown top-level fragment.
assert_eq!(
parse(&make_policy("unknown(pk(@0/**))", &[])).unwrap_err(),
Error::InvalidInput,
);

// Unknown script fragment.
assert_eq!(
parse(&make_policy("wsh(unknown(@0/**))", &[])).unwrap_err(),
Error::InvalidInput,
);

// Miniscript type-check fails (should be `or_b(pk(@0/**),s:pk(@1/**))`).
assert_eq!(
parse(&make_policy("wsh(or_b(pk(@0/**),pk(@1/**)))", &[])).unwrap_err(),
Error::InvalidInput,
);
}

#[test]
fn test_parse_validate() {
mock_unlocked();

let our_key = make_our_key(KEYPATH_ACCOUNT);

// All good.
assert!(parse(&make_policy("wsh(pk(@0/**))", &[our_key.clone()]))
.unwrap()
.validate(BtcCoin::Tbtc)
.is_ok());

// Unsupported coins
for coin in [BtcCoin::Btc, BtcCoin::Ltc, BtcCoin::Tltc] {
assert_eq!(
parse(&make_policy("wsh(pk(@0/**))", &[our_key.clone()]))
.unwrap()
.validate(coin),
Err(Error::InvalidInput)
);
}

// Too many keys.
let many_keys: Vec<pb::KeyOriginInfo> = (0..=20)
.map(|i| make_our_key(&[48 + HARDENED, 1 + HARDENED, i + HARDENED, 3 + HARDENED]))
.collect();
assert_eq!(
parse(&make_policy("wsh(pk(@0/**))", &many_keys))
.unwrap()
.validate(BtcCoin::Tbtc),
Err(Error::InvalidInput)
);
}
}

0 comments on commit 5625aec

Please sign in to comment.