Skip to content

Commit

Permalink
bitcoin: add support for tr() wallet policies/descriptors
Browse files Browse the repository at this point in the history
Support for Taproot wallet policies:

`tr(INTERNALKEY,{TREE})` where `TREE=SCRIPT` or `TREE={TREE,TREE}`.

SCRIPT can be an arbitrary miniscript like we already suppot in
`wsh(<miniscript>)` policies, with slight adaptations for the tr()
context.

References:
- https://github.com/bitcoin/bips/blob/master/bip-0388.mediawiki
- https://github.com/bitcoin/bitcoin/blob/efbf4e71ce8e3cd49ccdfb5e55e14fa4b338453c/doc/descriptors.md

`tr(@0/**)` is equivalent to BIP-86 that we already support as a
SimpleType (single signature).

The Taproot output key (which is in the Taproot address) is
`internalkey + tweak` where tweak is either a standard tweak if no
tree is present, or the merkle root hash of the tree.

A tr() UTXO can be spent by signing with the private key of the output
key (i.e. the tweaked private key of the internal key), called a key path
spend, or by providing and satisfying a script that is a leaf of the
tree, called a script path spend.

When spending using the the output key, it is the same as BIP-86 and
the BIP341 sighash computation is the same. When spending using a leaf
script, the sighash algo is extended - see the changes to bip341.rs
and the documentation that is referenced there.

We change keystore_secp256k1_schnorr_bip86_sign to
keystore_secp256k1_schnorr_sign, taking the tweak as an argument
instead, which we feed from signtx.rs depending on whether we are
spending BIP-86, a policy with/without a tree.

This commit adds significant binary bloat (~47kB), in large part
because rust-miniscript uses generics, so Miniscript<Tr> duplicates a
lot of the code of Miniscript<Wsh>, even though it is nearly
identical. This can be solved over time in rust-miniscript to reduce
the binary size cost of this feature.

With these commits since the previous release, we also reduced space
by more than this feature pulls in:

daa745f
debb871
2fa257c
61a82ff

```
749548	build/bin/firmware.bin (based on this commit)
753272	firmware.v9.19.0.bin
```
  • Loading branch information
benma committed Jul 30, 2024
1 parent 11fac8a commit 27cae27
Show file tree
Hide file tree
Showing 11 changed files with 613 additions and 87 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
## Firmware

### [Unreleased]
- Support for Taproot wallet policies and Miniscript on Taproot (MiniTapscript)

### 9.19.0
- Display device name on screen before unlock
Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ add_custom_target(rust-bindgen
--allowlist-function keystore_secp256k1_compressed_to_uncompressed
--allowlist-function keystore_secp256k1_nonce_commit
--allowlist-function keystore_secp256k1_sign
--allowlist-function keystore_secp256k1_schnorr_bip86_sign
--allowlist-function keystore_secp256k1_schnorr_sign
--allowlist-function keystore_bip39_mnemonic_to_seed
--allowlist-function keystore_mock_unlocked
--allowlist-var EC_PUBLIC_KEY_UNCOMPRESSED_LEN
Expand Down
26 changes: 13 additions & 13 deletions src/keystore.c
Original file line number Diff line number Diff line change
Expand Up @@ -941,9 +941,10 @@ bool keystore_secp256k1_schnorr_bip86_pubkey(const uint8_t* pubkey33, uint8_t* p
return secp256k1_xonly_pubkey_serialize(ctx, pubkey_out, &tweaked_xonly_pubkey) == 1;
}

static bool _schnorr_bip86_keypair(
static bool _schnorr_keypair(
const uint32_t* keypath,
size_t keypath_len,
const uint8_t* tweak,
secp256k1_keypair* keypair_out,
secp256k1_xonly_pubkey* pubkey_out)
{
Expand All @@ -962,33 +963,32 @@ static bool _schnorr_bip86_keypair(
if (!secp256k1_keypair_xonly_pub(ctx, pubkey_out, NULL, keypair_out)) {
return false;
}
uint8_t pubkey_serialized[32] = {0};
if (!secp256k1_xonly_pubkey_serialize(ctx, pubkey_serialized, pubkey_out)) {
return false;
}
uint8_t hash[32] = {0};
_tagged_hash("TapTweak", pubkey_serialized, sizeof(pubkey_serialized), hash);

if (secp256k1_keypair_xonly_tweak_add(ctx, keypair_out, hash) != 1) {
return false;
if (tweak != NULL) {
if (secp256k1_keypair_xonly_tweak_add(ctx, keypair_out, tweak) != 1) {
return false;
}
if (!secp256k1_keypair_xonly_pub(ctx, pubkey_out, NULL, keypair_out)) {
return false;
}
}
return secp256k1_keypair_xonly_pub(ctx, pubkey_out, NULL, keypair_out) == 1;
return true;
}

static void _cleanup_keypair(secp256k1_keypair* keypair)
{
util_zero(keypair, sizeof(secp256k1_keypair));
}

bool keystore_secp256k1_schnorr_bip86_sign(
bool keystore_secp256k1_schnorr_sign(
const uint32_t* keypath,
size_t keypath_len,
const uint8_t* msg32,
const uint8_t* tweak,
uint8_t* sig64_out)
{
secp256k1_keypair __attribute__((__cleanup__(_cleanup_keypair))) keypair = {0};
secp256k1_xonly_pubkey pubkey = {0};
if (!_schnorr_bip86_keypair(keypath, keypath_len, &keypair, &pubkey)) {
if (!_schnorr_keypair(keypath, keypath_len, tweak, &keypair, &pubkey)) {
return false;
}
const secp256k1_context* ctx = wally_get_secp_context();
Expand Down
5 changes: 4 additions & 1 deletion src/keystore.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,15 @@ USE_RESULT bool keystore_secp256k1_schnorr_bip86_pubkey(
* @param[in] keypath derivation keypath
* @param[in] keypath_len number of elements in keypath
* @param[in] msg32 32 byte message to sign
* @param[in] tweak 32 bytes, tweak private key before signing with this tweak. Use NULL to not
* tweak.
* @param[out] sig64_out resulting 64 byte signature
*/
USE_RESULT bool keystore_secp256k1_schnorr_bip86_sign(
USE_RESULT bool keystore_secp256k1_schnorr_sign(
const uint32_t* keypath,
size_t keypath_len,
const uint8_t* msg32,
const uint8_t* tweak,
uint8_t* sig64_out);

#ifdef TESTING
Expand Down
3 changes: 2 additions & 1 deletion src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ async fn address_policy(
.await?;
}

let address = common::Payload::from_policy(&parsed, keypath)?.address(coin_params)?;
let address =
common::Payload::from_policy(coin_params, &parsed, keypath)?.address(coin_params)?;
if display {
confirm::confirm(&confirm::Params {
title,
Expand Down
44 changes: 41 additions & 3 deletions src/rust/bitbox02-rust/src/hww/api/bitcoin/bip341.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ pub struct Args {
pub hash_outputs: [u8; 32],
// Data about this input:
pub input_index: u32,
// tapleaf_hash as described in https://github.com/bitcoin/bips/blob/85cda4e225b4d5fd7aff403f69d827f23f6afbbc/bip-0342.mediawiki#common-signature-message-extension
// Providing this means we use the above tapscript message extension.
pub tapleaf_hash: Option<[u8; 32]>,
}

/// Compute the BIP341 signature hash.
///
/// https://github.com/bitcoin/bips/blob/bb8dc57da9b3c6539b88378348728a2ff43f7e9c/bip-0341.mediawiki#common-signature-message
///
/// The hash_type is assumed 0 (`SIGHASH_DEFAULT`). The `ext_flag` is
/// assumed 0 and `annex` is assumed to be not present.
/// The hash_type is assumed 0 (`SIGHASH_DEFAULT`). `annex` is assumed to be not present.
pub fn sighash(args: &Args) -> [u8; 32] {
let tag = Sha256::digest(b"TapSighash");
let mut ctx = Sha256::new();
Expand All @@ -53,10 +55,28 @@ pub fn sighash(args: &Args) -> [u8; 32] {
ctx.update(args.hash_sequences);
ctx.update(args.hash_outputs);
// spend_type is 0 because ext_flag is 0 and annex is absent.
ctx.update(0u8.to_le_bytes());
let ext_flag = if args.tapleaf_hash.is_some() {
// ext_flag = 1 for Taproot leaf scripts
// See https://github.com/bitcoin/bips/blob/85cda4e225b4d5fd7aff403f69d827f23f6afbbc/bip-0342.mediawiki#common-signature-message-extension
1
} else {
0
};
let spend_type: u8 = 2 * ext_flag;
ctx.update(spend_type.to_le_bytes());
// Data about this input:
ctx.update(args.input_index.to_le_bytes());

if let Some(hash) = args.tapleaf_hash.as_ref() {
// See https://github.com/bitcoin/bips/blob/85cda4e225b4d5fd7aff403f69d827f23f6afbbc/bip-0342.mediawiki#common-signature-message-extension
// tapleaf_hash
ctx.update(hash);
// keyversion
ctx.update(0u8.to_le_bytes());
// codesep_pos - we do not use any OP_CODESEPARATORs.
let codesep_pos: u32 = 0xFFFFFFFF;
ctx.update(codesep_pos.to_le_bytes());
}
ctx.finalize().into()
}

Expand All @@ -79,7 +99,25 @@ mod tests {
hash_sequences: *b"\x18\x95\x9c\x72\x21\xab\x5c\xe9\xe2\x6c\x3c\xd6\x7b\x22\xc2\x4f\x8b\xaa\x54\xba\xc2\x81\xd8\xe6\xb0\x5e\x40\x0e\x6c\x3a\x95\x7e",
hash_outputs: *b"\xa2\xe6\xda\xb7\xc1\xf0\xdc\xd2\x97\xc8\xd6\x16\x47\xfd\x17\xd8\x21\x54\x1e\xa6\x9c\x3c\xc3\x7d\xcb\xad\x7f\x90\xd4\xeb\x4b\xc5",
input_index: 4,
tapleaf_hash: None,
}),
*b"\x4f\x90\x0a\x0b\xae\x3f\x14\x46\xfd\x48\x49\x0c\x29\x58\xb5\xa0\x23\x22\x8f\x01\x66\x1c\xda\x34\x96\xa1\x1d\xa5\x02\xa7\xf7\xef");
}

#[test]
fn test_sighash_tapleaf() {
assert_eq!(
sighash(&Args {
version: 2,
locktime: 500000000,
hash_prevouts: *b"\xe3\xb3\x3b\xb4\xef\x3a\x52\xad\x1f\xff\xb5\x55\xc0\xd8\x28\x28\xeb\x22\x73\x70\x36\xea\xeb\x02\xa2\x35\xd8\x2b\x90\x9c\x4c\x3f",
hash_amounts: *b"\x58\xa6\x96\x4a\x4f\x5f\x8f\x0b\x64\x2d\xed\x0a\x8a\x55\x3b\xe7\x62\x2a\x71\x9d\xa7\x1d\x1f\x5b\xef\xce\xfc\xde\xe8\xe0\xfd\xe6",
hash_scriptpubkeys: *b"\x23\xad\x0f\x61\xad\x2b\xca\x5b\xa6\xa7\x69\x3f\x50\xfc\xe9\x88\xe1\x7c\x37\x80\xbf\x2b\x1e\x72\x0c\xfb\xb3\x8f\xbd\xd5\x2e\x21",
hash_sequences: *b"\x18\x95\x9c\x72\x21\xab\x5c\xe9\xe2\x6c\x3c\xd6\x7b\x22\xc2\x4f\x8b\xaa\x54\xba\xc2\x81\xd8\xe6\xb0\x5e\x40\x0e\x6c\x3a\x95\x7e",
hash_outputs: *b"\xa2\xe6\xda\xb7\xc1\xf0\xdc\xd2\x97\xc8\xd6\x16\x47\xfd\x17\xd8\x21\x54\x1e\xa6\x9c\x3c\xc3\x7d\xcb\xad\x7f\x90\xd4\xeb\x4b\xc5",
input_index: 4,
tapleaf_hash: Some(*b"\x34\xe7\x21\x15\xc0\x9c\x91\x3c\x8b\xe1\x2e\x46\xfc\x14\x5f\xcf\x7c\x53\xca\xd9\xca\x2a\x05\xf9\x3a\x7c\xa2\xe0\xca\x88\xd0\x07"),
}),
*b"\xba\xe0\xaa\xcb\xa5\xae\xa9\xee\xbe\x19\xe1\x57\xa9\x8f\x1e\xe7\x0d\x7d\x28\x8c\x28\x0f\x27\x3e\x63\xbb\x8a\x85\xd1\xee\xf3\xc2");
}
}
15 changes: 13 additions & 2 deletions src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2022 Shift Crypto AG
// Copyright 2022-2024 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.
Expand Down Expand Up @@ -152,6 +152,7 @@ impl Payload {
/// derived using keypath m/48'/1'/0'/3'/11/5 derives the payload for
/// wsh(and_v(v:pk(@0/11/5),pk(@1/21/5))).
pub fn from_policy(
params: &Params,
policy: &super::policies::ParsedPolicy,
keypath: &[u32],
) -> Result<Self, Error> {
Expand All @@ -161,6 +162,16 @@ impl Payload {
data: Sha256::digest(wsh.witness_script()).to_vec(),
output_type: BtcOutputType::P2wsh,
}),
super::policies::Descriptor::Tr(tr) => {
if params.taproot_support {
Ok(Payload {
data: tr.output_key().to_vec(),
output_type: BtcOutputType::P2tr,
})
} else {
Err(Error::InvalidInput)
}
}
}
}

Expand All @@ -182,7 +193,7 @@ impl Payload {
keypath[keypath.len() - 2],
keypath[keypath.len() - 1],
),
ValidatedScriptConfig::Policy(policy) => Self::from_policy(policy, keypath),
ValidatedScriptConfig::Policy(policy) => Self::from_policy(params, policy, keypath),
}
}

Expand Down
Loading

0 comments on commit 27cae27

Please sign in to comment.