Skip to content

Commit 9e14c69

Browse files
committed
btc: support Taproot policies
This mostly involves fixing PSBT signing to be able to handle Taproot leaf script spends. Integration tests with the simulator are added. The signed PSBTs are checked to be correctly signed by rust-miniscript and rust-bitcoinconsensus.
1 parent 14d4468 commit 9e14c69

File tree

5 files changed

+387
-25
lines changed

5 files changed

+387
-25
lines changed

CHANGELOG-npm.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
## 0.7.0
66
- btc: handle error when an input's previous transaction is required but missing
77
- btc: add support for regtest
8+
- btc: add support for Taproot wallet policies
89

910
## 0.6.0
1011

CHANGELOG-rust.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
## 0.6.0
66
- btc: handle error when an input's previous transaction is required but missing
77
- btc: add support for regtest
8+
- btc: add support for Taproot wallet policies
89
- cardano: added support for vote delegation
910

1011
## 0.5.0

src/btc.rs

+62-25
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,9 @@ pub enum PsbtError {
242242
#[error("{0}")]
243243
#[cfg_attr(feature = "wasm", assoc(js_code = "sign-error"))]
244244
SignError(#[from] bitcoin::psbt::SignError),
245-
#[error("The BitBox does not support Taproot script path spending.")]
246-
#[cfg_attr(feature = "wasm", assoc(js_code = "unsupported-tap-script"))]
247-
UnsupportedTapScript,
245+
#[error("Taproot pubkeys must be unique across the internal key and all leaf scripts.")]
246+
#[cfg_attr(feature = "wasm", assoc(js_code = "key-not-unique"))]
247+
KeyNotUnique,
248248
#[error("Could not find our key in an input.")]
249249
#[cfg_attr(feature = "wasm", assoc(js_code = "key-not-found"))]
250250
KeyNotFound,
@@ -256,13 +256,19 @@ pub enum PsbtError {
256256
enum OurKey {
257257
Segwit(bitcoin::secp256k1::PublicKey, Keypath),
258258
TaprootInternal(Keypath),
259+
TaprootScript(
260+
bitcoin::secp256k1::XOnlyPublicKey,
261+
bitcoin::taproot::TapLeafHash,
262+
Keypath,
263+
),
259264
}
260265

261266
impl OurKey {
262267
fn keypath(&self) -> Keypath {
263268
match self {
264269
OurKey::Segwit(_, kp) => kp.clone(),
265270
OurKey::TaprootInternal(kp) => kp.clone(),
271+
OurKey::TaprootScript(_, _, kp) => kp.clone(),
266272
}
267273
}
268274
}
@@ -336,19 +342,34 @@ fn find_our_key<T: PsbtOutputInfo>(
336342
our_root_fingerprint: &[u8],
337343
output_info: T,
338344
) -> Result<OurKey, PsbtError> {
339-
if let Some(tap_internal_key) = output_info.get_tap_internal_key() {
340-
let (leaf_hashes, (fingerprint, derivation_path)) = output_info
341-
.get_tap_key_origins()
342-
.get(tap_internal_key)
343-
.ok_or(PsbtError::KeyNotFound)?;
344-
if !leaf_hashes.is_empty() {
345-
return Err(PsbtError::UnsupportedTapScript);
346-
}
345+
for (xonly, (leaf_hashes, (fingerprint, derivation_path))) in
346+
output_info.get_tap_key_origins().iter()
347+
{
347348
if &fingerprint[..] == our_root_fingerprint {
348349
// TODO: check for fingerprint collision
349-
return Ok(OurKey::TaprootInternal(derivation_path.into()));
350+
351+
if let Some(tap_internal_key) = output_info.get_tap_internal_key() {
352+
if tap_internal_key == xonly {
353+
if !leaf_hashes.is_empty() {
354+
// TODO change err msg, we don't support the
355+
// same key as internal key and also in a leaf
356+
// script.
357+
return Err(PsbtError::KeyNotUnique);
358+
}
359+
return Ok(OurKey::TaprootInternal(derivation_path.into()));
360+
}
361+
}
362+
if leaf_hashes.len() != 1 {
363+
// TODO change err msg, per BIP-388 all pubkeys are
364+
// unique, so it can't be in multiple leafs.
365+
return Err(PsbtError::KeyNotUnique);
366+
}
367+
return Ok(OurKey::TaprootScript(
368+
*xonly,
369+
leaf_hashes[0],
370+
derivation_path.into(),
371+
));
350372
}
351-
return Err(PsbtError::KeyNotFound);
352373
}
353374
for (pubkey, (fingerprint, derivation_path)) in output_info.get_bip32_derivation().iter() {
354375
if &fingerprint[..] == our_root_fingerprint {
@@ -576,15 +597,26 @@ pub fn make_script_config_policy(policy: &str, keys: &[KeyOriginInfo]) -> pb::Bt
576597
}
577598
}
578599

579-
fn is_taproot(script_config: &pb::BtcScriptConfigWithKeypath) -> bool {
580-
matches!(script_config,
581-
pb::BtcScriptConfigWithKeypath {
582-
script_config:
583-
Some(pb::BtcScriptConfig {
584-
config: Some(pb::btc_script_config::Config::SimpleType(simple_type)),
585-
}),
586-
..
587-
} if *simple_type == pb::btc_script_config::SimpleType::P2tr as i32)
600+
fn is_taproot_simple(script_config: &pb::BtcScriptConfigWithKeypath) -> bool {
601+
matches!(
602+
script_config.script_config.as_ref(),
603+
Some(pb::BtcScriptConfig {
604+
config: Some(pb::btc_script_config::Config::SimpleType(simple_type)),
605+
}) if *simple_type == pb::btc_script_config::SimpleType::P2tr as i32
606+
)
607+
}
608+
609+
fn is_taproot_policy(script_config: &pb::BtcScriptConfigWithKeypath) -> bool {
610+
matches!(
611+
script_config.script_config.as_ref(),
612+
Some(pb::BtcScriptConfig {
613+
config: Some(pb::btc_script_config::Config::Policy(policy)),
614+
}) if policy.policy.as_str().starts_with("tr("),
615+
)
616+
}
617+
618+
fn is_schnorr(script_config: &pb::BtcScriptConfigWithKeypath) -> bool {
619+
is_taproot_simple(script_config) | is_taproot_policy(script_config)
588620
}
589621

590622
impl<R: Runtime> PairedBitBox<R> {
@@ -681,7 +713,7 @@ impl<R: Runtime> PairedBitBox<R> {
681713
format_unit: pb::btc_sign_init_request::FormatUnit,
682714
) -> Result<Vec<Vec<u8>>, Error> {
683715
self.validate_version(">=9.4.0")?; // anti-klepto since 9.4.0
684-
if transaction.script_configs.iter().any(is_taproot) {
716+
if transaction.script_configs.iter().any(is_taproot_simple) {
685717
self.validate_version(">=9.10.0")?; // taproot since 9.10.0
686718
}
687719

@@ -709,7 +741,7 @@ impl<R: Runtime> PairedBitBox<R> {
709741
let input_index: usize = next_response.index as _;
710742
let tx_input: &TxInput = &transaction.inputs[input_index];
711743

712-
let input_is_schnorr = is_taproot(
744+
let input_is_schnorr = is_schnorr(
713745
&transaction.script_configs[tx_input.script_config_index as usize],
714746
);
715747
let perform_antiklepto = is_inputs_pass2 && !input_is_schnorr;
@@ -897,7 +929,12 @@ impl<R: Runtime> PairedBitBox<R> {
897929
psbt_input.tap_key_sig = Some(
898930
bitcoin::taproot::Signature::from_slice(signature)
899931
.map_err(|_| Error::InvalidSignature)?,
900-
)
932+
);
933+
}
934+
OurKey::TaprootScript(xonly, leaf_hash, _) => {
935+
let sig = bitcoin::taproot::Signature::from_slice(signature)
936+
.map_err(|_| Error::InvalidSignature)?;
937+
psbt_input.tap_script_sigs.insert((xonly, leaf_hash), sig);
901938
}
902939
}
903940
}

tests/simulators.json

+8
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,13 @@
22
{
33
"url": "https://github.com/BitBoxSwiss/bitbox02-firmware/releases/download/firmware%2Fv9.19.0/bitbox02-multi-v9.19.0-simulator1.0.0-linux-amd64",
44
"sha256": "e28be3fd6c7777624ad2574546ba125b7f134f095fa951acc8fb7295f3d33931"
5+
},
6+
{
7+
"url": "https://github.com/BitBoxSwiss/bitbox02-firmware/releases/download/firmware%2Fv9.20.0/bitbox02-multi-v9.20.0-simulator1.0.0-linux-amd64",
8+
"sha256": "ac32c1a71bd0a3a934bc7b94268f651c655f2e3afbb954811a256e551a420b3d"
9+
},
10+
{
11+
"url": "https://github.com/BitBoxSwiss/bitbox02-firmware/releases/download/firmware%2Fv9.21.0/bitbox02-multi-v9.21.0-simulator1.0.0-linux-amd64",
12+
"sha256": "72031b226ea344970a6a1506893838a63b075e0bad726557ab9d941b42c534f5"
513
}
614
]

0 commit comments

Comments
 (0)