|
| 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