From 1291a27e12a30ae310bdc18f2e8bef2d2a0c8f46 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Mon, 20 May 2024 09:22:39 +0200 Subject: [PATCH] bitcoin: support sending to silent payment addresses --- messages/btc.proto | 8 + src/CMakeLists.txt | 1 + src/keystore.c | 23 ++ src/keystore.h | 6 + src/rust/Cargo.lock | 4 +- src/rust/bitbox02-rust/Cargo.toml | 1 + .../src/hww/api/bitcoin/signtx.rs | 197 ++++++++++++++++-- .../bitbox02-rust/src/shiftcrypto.bitbox02.rs | 16 ++ src/rust/bitbox02/Cargo.toml | 3 + src/rust/bitbox02/src/keystore.rs | 55 ++++- 10 files changed, 291 insertions(+), 23 deletions(-) diff --git a/messages/btc.proto b/messages/btc.proto index 0e8d29b80d..46fe0f6249 100644 --- a/messages/btc.proto +++ b/messages/btc.proto @@ -112,6 +112,7 @@ message BTCSignInitRequest { SAT = 1; } FormatUnit format_unit = 8; + bool contains_silent_payment_outputs = 9; } message BTCSignNextResponse { @@ -157,6 +158,10 @@ enum BTCOutputType { } message BTCSignOutputRequest { + // https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki + message SilentPayment { + string address = 1; + } bool ours = 1; BTCOutputType type = 2; // if ours is false // 20 bytes for p2pkh, p2sh, pw2wpkh. 32 bytes for p2wsh. @@ -165,6 +170,9 @@ message BTCSignOutputRequest { repeated uint32 keypath = 5; // if ours is true // If ours is true. References a script config from BTCSignInitRequest uint32 script_config_index = 6; + // If provided, `type` must be `P2TR` and the payload must match the + // output derived from the silent payment address. + SilentPayment silent_payment = 7; } message BTCScriptConfigRegistration { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 32e1f8cb9c..9bb4e0e4fd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -339,6 +339,7 @@ add_custom_target(rust-bindgen --allowlist-function keystore_lock --allowlist-function keystore_create_and_store_seed --allowlist-function keystore_copy_seed + --allowlist-function keystore_secp256k1_get_private_key --allowlist-function keystore_get_bip39_mnemonic --allowlist-function keystore_get_bip39_word --allowlist-function keystore_get_ed25519_seed diff --git a/src/keystore.c b/src/keystore.c index e09420e15e..8d695fcc25 100644 --- a/src/keystore.c +++ b/src/keystore.c @@ -1000,6 +1000,29 @@ bool keystore_secp256k1_schnorr_bip86_sign( return secp256k1_schnorrsig_verify(ctx, sig64_out, msg32, 32, &pubkey) == 1; } +bool keystore_secp256k1_get_private_key( + const uint32_t* keypath, + const size_t keypath_len, + bool tweak_bip86, + uint8_t* key_out) +{ + if (tweak_bip86) { + secp256k1_keypair __attribute__((__cleanup__(_cleanup_keypair))) keypair = {0}; + secp256k1_xonly_pubkey pubkey = {0}; + if (!_schnorr_bip86_keypair(keypath, keypath_len, &keypair, &pubkey)) { + return false; + } + const secp256k1_context* ctx = wally_get_secp_context(); + return secp256k1_keypair_sec(ctx, key_out, &keypair) == 1; + } + struct ext_key xprv __attribute__((__cleanup__(keystore_zero_xkey))) = {0}; + if (!_get_xprv_twice(keypath, keypath_len, &xprv)) { + return false; + } + memcpy(key_out, xprv.priv_key + 1, 32); + return true; +} + #ifdef TESTING void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed) { diff --git a/src/keystore.h b/src/keystore.h index e3624ee080..b8150a33de 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -303,6 +303,12 @@ USE_RESULT bool keystore_secp256k1_schnorr_bip86_sign( const uint8_t* msg32, uint8_t* sig64_out); +USE_RESULT bool keystore_secp256k1_get_private_key( + const uint32_t* keypath, + size_t keypath_len, + bool tweak_bip86, + uint8_t* key_out); + #ifdef TESTING /** * convenience to mock the keystore state (locked, seed) in tests. diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 8e8b0810c3..fd74148347 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -64,6 +64,7 @@ name = "bitbox02" version = "0.1.0" dependencies = [ "bitbox02-sys", + "hex", "lazy_static", "util", "zeroize", @@ -104,6 +105,7 @@ dependencies = [ "prost", "sha2", "sha3", + "streaming-silent-payments", "util", "zeroize", ] @@ -687,7 +689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" [[package]] -name = "streaming-slient-payments" +name = "streaming-silent-payments" version = "0.1.0" dependencies = [ "bech32 0.11.0", diff --git a/src/rust/bitbox02-rust/Cargo.toml b/src/rust/bitbox02-rust/Cargo.toml index 5fb5d02060..8dd50a2465 100644 --- a/src/rust/bitbox02-rust/Cargo.toml +++ b/src/rust/bitbox02-rust/Cargo.toml @@ -31,6 +31,7 @@ bitbox02 = {path = "../bitbox02"} util = {path = "../util"} binascii = { version = "0.1.4", default-features = false, features = ["encode"] } bitbox02-noise = {path = "../bitbox02-noise"} +streaming-silent-payments = {path = "../streaming-silent-payments"} hex = { workspace = true } sha2 = { workspace = true } sha3 = { workspace = true, optional = true } diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs index bf0e3135a0..a34058c89e 100644 --- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs +++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs @@ -31,6 +31,8 @@ use pb::btc_sign_init_request::FormatUnit; use pb::btc_sign_next_response::Type as NextType; use sha2::{Digest, Sha256}; +use bitcoin::hashes::Hash; + /// After each request from the host, we send a `BtcSignNextResponse` response back to the host, /// containing information which request we want next, and containing additional metadata if /// available (e.g. a signature after signing an input). @@ -238,7 +240,7 @@ fn validate_input( validate_keypath(params, script_config_account, &input.keypath) } -fn is_taproot(script_config_account: &pb::BtcScriptConfigWithKeypath) -> bool { +fn is_taproot_bip86(script_config_account: &pb::BtcScriptConfigWithKeypath) -> bool { match script_config_account { pb::BtcScriptConfigWithKeypath { script_config: @@ -251,6 +253,10 @@ fn is_taproot(script_config_account: &pb::BtcScriptConfigWithKeypath) -> bool { } } +fn is_taproot(script_config_account: &pb::BtcScriptConfigWithKeypath) -> bool { + is_taproot_bip86(script_config_account) +} + /// Generates the subscript (scriptCode without the length prefix) used in the bip143 sighash algo. /// /// See https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#specification, item 5: @@ -502,6 +508,54 @@ fn setup_xpub_cache(cache: &mut Bip32XpubCache, script_configs: &[pb::BtcScriptC } } +impl TryFrom for streaming_silent_payments::Network { + type Error = Error; + fn try_from(value: pb::BtcCoin) -> Result { + match value { + pb::BtcCoin::Btc => Ok(streaming_silent_payments::Network::Btc), + pb::BtcCoin::Tbtc => Ok(streaming_silent_payments::Network::Tbtc), + _ => Err(Error::InvalidInput), + } + } +} + +impl From for streaming_silent_payments::InputType { + fn from(value: pb::btc_script_config::SimpleType) -> streaming_silent_payments::InputType { + match value { + pb::btc_script_config::SimpleType::P2wpkhP2sh => { + streaming_silent_payments::InputType::P2wpkhP2sh + } + pb::btc_script_config::SimpleType::P2wpkh => { + streaming_silent_payments::InputType::P2wpkh + } + pb::btc_script_config::SimpleType::P2tr => { + streaming_silent_payments::InputType::P2trKeypathspend + } + } + } +} + +impl TryFrom<&pb::BtcScriptConfigWithKeypath> for streaming_silent_payments::InputType { + type Error = Error; + fn try_from( + value: &pb::BtcScriptConfigWithKeypath, + ) -> Result { + match value { + pb::BtcScriptConfigWithKeypath { + script_config: + Some(pb::BtcScriptConfig { + config: Some(pb::btc_script_config::Config::SimpleType(simple_type)), + }), + .. + } => { + let simple_type = pb::btc_script_config::SimpleType::try_from(*simple_type)?; + Ok(simple_type.into()) + } + _ => Err(Error::InvalidInput), + } + } +} + /// Singing flow: /// /// init @@ -603,6 +657,12 @@ async fn _process(request: &pb::BtcSignInitRequest) -> Result { // Are all inputs taproot? let taproot_only = request.script_configs.iter().all(is_taproot); + let mut silent_payment_verifier = if request.contains_silent_payment_outputs { + Some(streaming_silent_payments::Verifier::new(coin.try_into()?)) + } else { + None + }; + for input_index in 0..request.num_inputs { // Update progress. bitbox02::ui::progress_set( @@ -663,6 +723,28 @@ async fn _process(request: &pb::BtcSignInitRequest) -> Result { ) .await?; } + + if let Some(ref mut silent_payment_verifier) = silent_payment_verifier { + let private_key = bitcoin::secp256k1::SecretKey::from_slice( + &bitbox02::keystore::secp256k1_get_private_key( + &tx_input.keypath, + is_taproot(script_config_account), + )?, + ) + .map_err(|_| Error::Generic)?; + + silent_payment_verifier + .add_input( + script_config_account.try_into()?, + &private_key, + bitcoin::OutPoint::new( + bitcoin::Txid::from_slice(&tx_input.prev_out_hash) + .map_err(|_| Error::InvalidInput)?, + tx_input.prev_out_index, + ), + ) + .map_err(|_| Error::InvalidInput)?; + } } // The progress for loading the inputs is 100%. @@ -753,10 +835,34 @@ async fn _process(request: &pb::BtcSignInitRequest) -> Result { false }; + // Verify silent payment output. + if let Some(ref mut silent_payment_verifier) = silent_payment_verifier { + if let Some(silent_payment) = tx_output.silent_payment.as_ref() { + if is_change { + return Err(Error::InvalidInput); + } + if payload.output_type != pb::BtcOutputType::P2tr { + return Err(Error::InvalidInput); + } + let xonly = bitcoin::secp256k1::XOnlyPublicKey::from_slice(&payload.data) + .map_err(|_| Error::InvalidInput)?; + silent_payment_verifier + .check_output(&silent_payment.address, &xonly) + .map_err(|_| Error::InvalidInput)?; + } + } else if tx_output.silent_payment.is_some() { + return Err(Error::InvalidInput); + } + if !is_change { // Verify output if it is not a change output. // Assemble address to display, get user confirmation. - let address = payload.address(coin_params)?; + + let address = if let Some(sp) = tx_output.silent_payment.as_ref() { + sp.address.clone() + } else { + payload.address(coin_params)? + }; transaction::verify_recipient( &(if tx_output.ours { format!("This BitBox02: {}", address) @@ -1120,8 +1226,7 @@ mod tests { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, ], - keypath: vec![], - script_config_index: 0, + ..Default::default() }, pb::BtcSignOutputRequest { ours: false, @@ -1131,8 +1236,7 @@ mod tests { 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, ], - keypath: vec![], - script_config_index: 0, + ..Default::default() }, pb::BtcSignOutputRequest { ours: false, @@ -1142,8 +1246,7 @@ mod tests { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, ], - keypath: vec![], - script_config_index: 0, + ..Default::default() }, pb::BtcSignOutputRequest { ours: false, @@ -1154,26 +1257,23 @@ mod tests { 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, ], - keypath: vec![], - script_config_index: 0, + ..Default::default() }, pb::BtcSignOutputRequest { // change ours: true, r#type: 0, value: 690000000, // btc 6.9 - payload: vec![], keypath: vec![84 + HARDENED, bip44_coin, 10 + HARDENED, 1, 3], - script_config_index: 0, + ..Default::default() }, pb::BtcSignOutputRequest { // change #2 ours: true, r#type: 0, value: 100, - payload: vec![], keypath: vec![84 + HARDENED, bip44_coin, 10 + HARDENED, 1, 30], - script_config_index: 0, + ..Default::default() }, ], locktime: 0, @@ -1225,9 +1325,8 @@ mod tests { ours: true, r#type: pb::BtcOutputType::Unknown as _, value: 9825, // btc 0.00009825 - payload: vec![], keypath: vec![48 + HARDENED, bip44_coin, 0 + HARDENED, 2 + HARDENED, 1, 0], - script_config_index: 0, + ..Default::default() }, pb::BtcSignOutputRequest { ours: false, @@ -1238,8 +1337,7 @@ mod tests { 0x83, 0xe7, 0x57, 0x84, 0x67, 0x25, 0xa3, 0xf6, 0x23, 0xae, 0xc2, 0x09, 0x76, 0xd3, 0x0e, 0x29, 0xb0, 0xd4, 0xb3, 0x5b, ], - keypath: vec![], - script_config_index: 0, + ..Default::default() }, ], locktime: 1663289, @@ -1278,6 +1376,10 @@ mod tests { num_outputs: self.outputs.len() as _, locktime: self.locktime, format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: self + .outputs + .iter() + .any(|output| output.silent_payment.is_some()), } } @@ -1299,6 +1401,7 @@ mod tests { num_outputs: self.outputs.len() as _, locktime: self.locktime, format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: false, } } @@ -1386,6 +1489,7 @@ mod tests { num_outputs: 1, locktime: 0, format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: false, }; { @@ -1555,6 +1659,7 @@ mod tests { num_outputs: 1, locktime: 0, format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: false, })), Err(Error::InvalidInput) ); @@ -2244,6 +2349,58 @@ mod tests { } } + #[test] + fn test_silent_payment_output() { + let transaction = + alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc))); + + // Make an input a P2TR input to verify the right (tweaked) private key is used in the + // derivation of the silent payment output. + transaction.borrow_mut().inputs[0].input.script_config_index = 1; + transaction.borrow_mut().inputs[0].input.keypath[0] = 86 + HARDENED; + + // Make first output a silent payment output. + transaction.borrow_mut().outputs[0].r#type = pb::BtcOutputType::P2tr as _; + transaction.borrow_mut().outputs[0].payload = b"\x7b\x91\x01\xd6\x0c\x64\x61\xff\x3e\x18\xf0\x83\x2e\x7f\x1e\x95\x20\x84\x20\x50\x62\xd7\xe0\xb7\xb0\x88\x12\xc2\x64\xcf\xe7\x13".to_vec(); + transaction.borrow_mut().outputs[0].silent_payment = + Some(pb::btc_sign_output_request::SilentPayment { + address: "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv".into(), + }); + mock_host_responder(transaction.clone()); + static mut UI_COUNTER: u32 = 0; + mock(Data { + ui_transaction_address_create: Some(Box::new(|amount, address| unsafe { + UI_COUNTER += 1; + if UI_COUNTER == 1 { + assert_eq!( + address, + "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + ); + assert_eq!(amount, "1.00000000 BTC"); + } + true + })), + ui_transaction_fee_create: Some(Box::new(|_total, _fee, _longtouch| true)), + ui_confirm_create: Some(Box::new(move |_params| true)), + ..Default::default() + }); + mock_unlocked(); + + let mut init_request = transaction.borrow().init_request(); + init_request + .script_configs + .push(pb::BtcScriptConfigWithKeypath { + script_config: Some(pb::BtcScriptConfig { + config: Some(pb::btc_script_config::Config::SimpleType( + pb::btc_script_config::SimpleType::P2tr as _, + )), + }), + keypath: vec![86 + HARDENED, 0 + HARDENED, 10 + HARDENED], + }); + assert!(block_on(process(&init_request)).is_ok()); + assert!(unsafe { UI_COUNTER >= 1 }); + } + // Test an output that is marked ours but is not a change output by keypath. #[test] fn test_our_non_change_output() { @@ -2576,6 +2733,7 @@ mod tests { num_outputs: tx.outputs.len() as _, locktime: tx.locktime, format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: false, } }; let result = block_on(process(&init_request)); @@ -2637,6 +2795,7 @@ mod tests { num_outputs: tx.outputs.len() as _, locktime: tx.locktime, format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: false, } }; assert_eq!(block_on(process(&init_request)), Err(Error::InvalidInput)); @@ -2703,6 +2862,7 @@ mod tests { num_outputs: tx.outputs.len() as _, locktime: tx.locktime, format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: false, } }; let result = block_on(process(&init_request)); @@ -2779,6 +2939,7 @@ mod tests { num_outputs: tx.outputs.len() as _, locktime: tx.locktime, format_unit: FormatUnit::Default as _, + contains_silent_payment_outputs: false, } }; let result = block_on(process(&init_request)); diff --git a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs index 2d35eab9e6..77ec14f645 100644 --- a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs +++ b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs @@ -453,6 +453,8 @@ pub struct BtcSignInitRequest { pub locktime: u32, #[prost(enumeration = "btc_sign_init_request::FormatUnit", tag = "8")] pub format_unit: i32, + #[prost(bool, tag = "9")] + pub contains_silent_payment_outputs: bool, } /// Nested message and enum types in `BTCSignInitRequest`. pub mod btc_sign_init_request { @@ -613,6 +615,20 @@ pub struct BtcSignOutputRequest { /// If ours is true. References a script config from BTCSignInitRequest #[prost(uint32, tag = "6")] pub script_config_index: u32, + /// If provided, `type` must be `P2TR` and the payload must match the + /// output derived from the silent payment address. + #[prost(message, optional, tag = "7")] + pub silent_payment: ::core::option::Option, +} +/// Nested message and enum types in `BTCSignOutputRequest`. +pub mod btc_sign_output_request { + /// + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct SilentPayment { + #[prost(string, tag = "1")] + pub address: ::prost::alloc::string::String, + } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/src/rust/bitbox02/Cargo.toml b/src/rust/bitbox02/Cargo.toml index ccfc1e417d..c4b5eef056 100644 --- a/src/rust/bitbox02/Cargo.toml +++ b/src/rust/bitbox02/Cargo.toml @@ -27,6 +27,9 @@ util = {path = "../util"} zeroize = { workspace = true } lazy_static = { workspace = true, optional = true } +[dev-dependencies] +hex = { workspace = true } + [features] # Only to be enabled in unit tests. testing = ["lazy_static"] diff --git a/src/rust/bitbox02/src/keystore.rs b/src/rust/bitbox02/src/keystore.rs index 7e58b052ec..bcabef8f1e 100644 --- a/src/rust/bitbox02/src/keystore.rs +++ b/src/rust/bitbox02/src/keystore.rs @@ -224,6 +224,24 @@ pub fn encode_xpub_at_keypath(keypath: &[u32]) -> Result, ()> { } } +pub fn secp256k1_get_private_key( + keypath: &[u32], + tweak_bip86: bool, +) -> Result>, ()> { + let mut key = zeroize::Zeroizing::new(vec![0u8; 32]); + match unsafe { + bitbox02_sys::keystore_secp256k1_get_private_key( + keypath.as_ptr(), + keypath.len() as _, + tweak_bip86, + key.as_mut_ptr(), + ) + } { + true => Ok(key), + false => Err(()), + } +} + pub struct SignResult { pub signature: [u8; 64], pub recid: u8, @@ -361,6 +379,7 @@ pub fn secp256k1_schnorr_bip86_pubkey(pubkey33: &[u8]) -> Result<[u8; 32], ()> { mod tests { use super::*; use crate::testing::{mock_unlocked, mock_unlocked_using_mnemonic, TEST_MNEMONIC}; + use util::bip32::HARDENED; #[test] fn test_bip39_mnemonic_to_seed() { @@ -490,7 +509,7 @@ mod tests { "income soft level reunion height pony crane use unfold win keen satisfy", ); assert_eq!( - bip85_bip39(12, util::bip32::HARDENED - 1).unwrap().as_ref() as &str, + bip85_bip39(12, HARDENED - 1).unwrap().as_ref() as &str, "carry build nerve market domain energy mistake script puzzle replace mixture idea", ); assert_eq!( @@ -505,7 +524,7 @@ mod tests { // Invalid number of words. assert!(bip85_bip39(10, 0).is_err()); // Index too high. - assert!(bip85_bip39(12, util::bip32::HARDENED).is_err()); + assert!(bip85_bip39(12, HARDENED).is_err()); } #[test] @@ -527,11 +546,39 @@ mod tests { b"\xe7\xd9\xce\x75\xf8\xcb\x17\x57\x0e\x66\x54\x17\xb4\x7f\xa0\xbe", ); assert_eq!( - bip85_ln(util::bip32::HARDENED - 1).unwrap().as_slice(), + bip85_ln(HARDENED - 1).unwrap().as_slice(), b"\x1f\x3b\x75\xea\x25\x27\x49\x70\x0a\x1e\x45\x34\x69\x14\x8c\xa6", ); // Index too high. - assert!(bip85_ln(util::bip32::HARDENED).is_err()); + assert!(bip85_ln(HARDENED).is_err()); + } + + #[test] + fn test_secp256k1_get_private_key() { + lock(); + let keypath = &[84 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0]; + assert!(secp256k1_get_private_key(keypath, false).is_err()); + + mock_unlocked_using_mnemonic( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "", + ); + + assert_eq!( + hex::encode(secp256k1_get_private_key(keypath, false).unwrap()), + "4604b4b710fe91f584fff084e1a9159fe4f8408fff380596a604948474ce4fa3" + ); + + // See first test vector in + // https://github.com/bitcoin/bips/blob/edffe529056f6dfd33d8f716fb871467c3c09263/bip-0086.mediawiki#test-vectors + // The below privte key's public key is: a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c. + assert_eq!( + hex::encode( + secp256k1_get_private_key(&[86 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0], true) + .unwrap() + ), + "eaac016f36e8c18347fbacf05ab7966708fbfce7ce3bf1dc32a09dd0645db038", + ); } }