From e244c49bfdbe046dc08ecba5154285829edd8d9a Mon Sep 17 00:00:00 2001 From: xiaoguang1010 <40228114+xiaoguang1010@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:44:44 +0800 Subject: [PATCH] feat: imkey support bitcoin psbt transaction[R2D2-11602] (#114) * feat: add p2wpkhp2tr address generation * feat: add bitcoin Mixed signature function * test: modify p2wpkh test case * feat: add bitcoin p2wpkh sign * feat: add test case and add p2tr sign * test: add test case * test: modify btc test case * feat: code optimization * feat: modify display_addres and get_address * feat: add bitcoin p2tr transaction * test: add p2tr test case * test: add bitcoin transaction sign function test * feat: code optimization * feat: pass in the tweaked public key when signing * chore: code format * feat: add imKey PSBT feature code * feat: add p2wpkhp2tr address generation * feat: add bitcoin Mixed signature function * test: modify p2wpkh test case * feat: add bitcoin p2wpkh sign * feat: add test case and add p2tr sign * test: add test case * test: modify btc test case * feat: code optimization * feat: modify display_addres and get_address * feat: add bitcoin p2tr transaction * test: add p2tr test case * test: add bitcoin transaction sign function test * feat: code optimization * feat: pass in the tweaked public key when signing * chore: code format * chore: switch to staging env * feat: add imKey PSBT feature code * feat: derive_account and derive_sub_account support native segwit address and bech32 address * feat: add bitcoin psbt sign transaction * feat: add bitcoin p2tr script sign * fix: fix psbt legacy transaction bug * chore: psbt code optimized * fix: fix preview info calc error bug * fix: modify change path to account path * chore: code optimized * feat: psbt transaction support sign message * feat: add bip-322 sign message feature * feat: add bitcoin sign message api * chore: remove address verify function * chore: get_utxo_pub_key function removes the network param * chore: modify tcx version to 2.8.0 * chore: bip322 sign message add path check and code optimization * build: moify ios CI run-on version to macos-14 * fix: fix ios CI build error * fix: fix cargo check error * chore: optimization imKey public key conversion * chore: PubKeyParam removes isSegwit field --- VERSION | 2 +- imkey-core/ikc-common/src/apdu.rs | 95 + imkey-core/ikc-common/src/coin_info.rs | 28 + imkey-core/ikc-common/src/common.rs | 2 + imkey-core/ikc-common/src/constants.rs | 15 +- imkey-core/ikc-common/src/error.rs | 4 + imkey-core/ikc-common/src/hex.rs | 124 + imkey-core/ikc-common/src/lib.rs | 1 + imkey-core/ikc-common/src/utility.rs | 16 +- imkey-core/ikc-device/src/device_binding.rs | 2 +- imkey-core/ikc-proto/src/api.proto | 3 +- imkey-core/ikc-proto/src/btc.proto | 29 +- imkey-core/ikc-proto/src/btcfork.proto | 8 +- imkey-core/ikc-proto/src/common.proto | 1 + .../ikc-wallet/coin-bch/src/transaction.rs | 14 +- .../ikc-wallet/coin-bitcoin/src/address.rs | 170 +- .../ikc-wallet/coin-bitcoin/src/btcapi.rs | 46 +- .../ikc-wallet/coin-bitcoin/src/common.rs | 89 +- imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs | 3 + .../ikc-wallet/coin-bitcoin/src/message.rs | 250 ++ .../ikc-wallet/coin-bitcoin/src/psbt.rs | 1048 ++++++++ .../coin-bitcoin/src/transaction.rs | 2122 ++++++++++++----- .../coin-bitcoin/src/usdt_transaction.rs | 13 +- .../coin-btc-fork/src/btcforkapi.rs | 16 +- .../coin-btc-fork/src/transaction.rs | 6 +- imkey-core/ikc-wallet/coin-ckb/src/signer.rs | 1 + .../coin-ethereum/src/transaction.rs | 5 + .../coin-filecoin/src/transaction.rs | 1 + imkey-core/ikc/src/api.rs | 6 +- imkey-core/ikc/src/bch_address.rs | 4 +- imkey-core/ikc/src/bch_signer.rs | 1 + imkey-core/ikc/src/btc_address.rs | 84 +- imkey-core/ikc/src/btc_fork_address.rs | 29 +- imkey-core/ikc/src/btc_fork_signer.rs | 2 + imkey-core/ikc/src/btc_signer.rs | 105 +- imkey-core/ikc/src/ethereum_signer.rs | 9 + imkey-core/ikc/src/handler.rs | 66 +- imkey-core/ikc/src/lib.rs | 414 +++- token-core/tcx-btc-kin/src/address.rs | 1 + token-core/tcx-constants/src/coin_info.rs | 16 +- token-core/tcx/tests/derive_test.rs | 6 +- 41 files changed, 3965 insertions(+), 892 deletions(-) create mode 100644 imkey-core/ikc-common/src/hex.rs create mode 100644 imkey-core/ikc-wallet/coin-bitcoin/src/message.rs create mode 100644 imkey-core/ikc-wallet/coin-bitcoin/src/psbt.rs diff --git a/VERSION b/VERSION index a4dd9dba..834f2629 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.4 +2.8.0 diff --git a/imkey-core/ikc-common/src/apdu.rs b/imkey-core/ikc-common/src/apdu.rs index 2fb5aeab..72de20e2 100644 --- a/imkey-core/ikc-common/src/apdu.rs +++ b/imkey-core/ikc-common/src/apdu.rs @@ -69,6 +69,19 @@ impl BtcApdu { apdu.to_hex().to_uppercase() } + /** + *p2 00:sign psbt transaction 80: sign message + **/ + pub fn btc_psbt_preview(data: &Vec, p2: u8) -> String { + if data.len() as u32 > LC_MAX { + panic!("data to long"); + } + let mut apdu = ApduHeader::new(0x80, 0x4C, 0x00, p2, data.len() as u8).to_array(); + apdu.extend(data.iter()); + apdu.push(0x00); + apdu.to_hex().to_uppercase() + } + pub fn btc_sign(index: u8, hash_type: u8, path: &str) -> String { let path_bytes = path.as_bytes(); let mut apdu = @@ -93,6 +106,36 @@ impl BtcApdu { apdu.to_hex().to_uppercase() } + pub fn btc_taproot_sign(last_one: bool, data: Vec) -> String { + if data.len() as u32 > LC_MAX { + panic!("data to long"); + } + + let mut apdu = match last_one { + true => ApduHeader::new(0x80, 0x40, 0x80, 0x00, data.len() as u8).to_array(), + _ => ApduHeader::new(0x80, 0x40, 0x00, 0x00, data.len() as u8).to_array(), + }; + + apdu.extend(data.iter()); + apdu.push(0x00); + apdu.to_hex().to_uppercase() + } + + pub fn btc_taproot_script_sign(last_one: bool, data: Vec) -> String { + if data.len() as u32 > LC_MAX { + panic!("data to long"); + } + + let mut apdu = match last_one { + true => ApduHeader::new(0x80, 0x40, 0x80, 0x80, data.len() as u8).to_array(), + _ => ApduHeader::new(0x80, 0x40, 0x00, 0x80, data.len() as u8).to_array(), + }; + + apdu.extend(data.iter()); + apdu.push(0x00); + apdu.to_hex().to_uppercase() + } + pub fn omni_prepare_data(p1: u8, data: Vec) -> String { if data.len() as u32 > LC_MAX { panic!("data to long"); @@ -111,6 +154,58 @@ impl BtcApdu { data.extend(name); Apdu::register_address(0x37, &data) } + + pub fn btc_single_utxo_sign_prepare(ins: u8, data: &Vec) -> Vec { + let mut apdu_vec = Vec::new(); + let apdu_number = (data.len() - 1) / LC_MAX as usize + 1; + for index in 0..apdu_number { + if index == 0 && index == apdu_number - 1 { + let length = if data.len() % LC_MAX as usize == 0 { + LC_MAX + } else { + (data.len() % LC_MAX as usize) as u32 + }; + let mut temp_apdu_vec = + ApduHeader::new(0x80, ins, 0x00, 0x80, length as u8).to_array(); + temp_apdu_vec.extend_from_slice(&data[index * LC_MAX as usize..]); + apdu_vec.push(hex::encode_upper(temp_apdu_vec)); + } else if index == 0 && index != apdu_number - 1 { + let mut temp_apdu_vec = + ApduHeader::new(0x80, ins, 0x00, 0x00, LC_MAX as u8).to_array(); + temp_apdu_vec.extend_from_slice( + &data[index * LC_MAX as usize..((index + 1) * LC_MAX as usize) as usize], + ); + apdu_vec.push(hex::encode_upper(temp_apdu_vec)); + } else if index != 0 && index != apdu_number - 1 { + let mut temp_apdu_vec = + ApduHeader::new(0x80, ins, 0x80, 0x00, LC_MAX as u8).to_array(); + temp_apdu_vec.extend_from_slice( + &data[index * LC_MAX as usize..((index + 1) * LC_MAX as usize) as usize], + ); + apdu_vec.push(hex::encode_upper(temp_apdu_vec)); + } else if index != 0 && index == apdu_number - 1 { + let length = if data.len() % LC_MAX as usize == 0 { + LC_MAX + } else { + (data.len() % LC_MAX as usize) as u32 + }; + let mut temp_apdu_vec = + ApduHeader::new(0x80, ins, 0x80, 0x80, length as u8).to_array(); + temp_apdu_vec.extend_from_slice(&data[index * LC_MAX as usize..]); + apdu_vec.push(hex::encode_upper(temp_apdu_vec)); + } + } + return apdu_vec; + } + + pub fn btc_single_utxo_sign(index: u8, hash_type: u8, path: &str) -> String { + let path_bytes = path.as_bytes(); + let mut apdu = + ApduHeader::new(0x80, 0x45, index, hash_type, path_bytes.len() as u8).to_array(); + apdu.extend(path_bytes.iter()); + apdu.push(0x00); + apdu.to_hex().to_uppercase() + } } pub struct EthApdu(); diff --git a/imkey-core/ikc-common/src/coin_info.rs b/imkey-core/ikc-common/src/coin_info.rs index 56973e19..d4786fa3 100644 --- a/imkey-core/ikc-common/src/coin_info.rs +++ b/imkey-core/ikc-common/src/coin_info.rs @@ -46,6 +46,34 @@ lazy_static! { network: "TESTNET".to_string(), seg_wit: "P2WPKH".to_string(), }, + CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/84'/0'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }, + CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/84'/1'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "TESTNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }, + CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/86'/0'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "VERSION_1".to_string(), + }, + CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/86'/1'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "TESTNET".to_string(), + seg_wit: "VERSION_1".to_string(), + }, CoinInfo { coin: "BITCOINCASH".to_string(), derivation_path: "m/44'/145'/0'/0/0".to_string(), diff --git a/imkey-core/ikc-common/src/common.rs b/imkey-core/ikc-common/src/common.rs index f1333867..1bf6a748 100644 --- a/imkey-core/ikc-common/src/common.rs +++ b/imkey-core/ikc-common/src/common.rs @@ -17,4 +17,6 @@ pub struct SignParam { pub sender: ::prost::alloc::string::String, #[prost(string, tag = "8")] pub fee: ::prost::alloc::string::String, + #[prost(string, tag = "9")] + pub seg_wit: ::prost::alloc::string::String, } diff --git a/imkey-core/ikc-common/src/constants.rs b/imkey-core/ikc-common/src/constants.rs index c3ec85c2..b45761c0 100644 --- a/imkey-core/ikc-common/src/constants.rs +++ b/imkey-core/ikc-common/src/constants.rs @@ -1,4 +1,4 @@ -pub const VERSION: &str = "2.15.2"; +pub const VERSION: &str = "2.16.0"; pub const URL: &str = "https://imkey.online:1000/imkey"; // pub const URL: &str = "https://imkeyserver.com:10444/imkey"; @@ -44,11 +44,11 @@ pub const TRON_PATH: &str = "m/44'/195'/0'/0/0"; pub const MAX_UTXO_NUMBER: usize = 252; pub const EACH_ROUND_NUMBER: usize = 5; -pub const DUST_THRESHOLD: i64 = 2730; -pub const MIN_NONDUST_OUTPUT: i64 = 546; +pub const DUST_THRESHOLD: u64 = 2730; +pub const MIN_NONDUST_OUTPUT: u64 = 546; // max op return size pub const MAX_OPRETURN_SIZE: usize = 80; -pub const BTC_FORK_DUST: i64 = 546; +pub const BTC_FORK_DUST: u64 = 546; // imkey device status pub const IMKEY_DEV_STATUS_INACTIVATED: &str = "inactivated"; @@ -118,6 +118,13 @@ pub const ETH_TRANSACTION_TYPE_EIP1559: &str = "02"; pub const ETH_MAX_SUPPORT_PAYMENT_LEN: usize = 255; +pub const BTC_PSBT_TRX_PER_PAGE_NUMBER: usize = 3; + +pub const BTC_SEG_WIT_TYPE_LEGACY: &str = "NONE"; +pub const BTC_SEG_WIT_TYPE_P2WPKH: &str = "P2WPKH"; +pub const BTC_SEG_WIT_TYPE_VERSION_0: &str = "VERSION_0"; +pub const BTC_SEG_WIT_TYPE_VERSION_1: &str = "VERSION_1"; + lazy_static! { /// Lazily initialized secp256k1 engine pub static ref SECP256K1_ENGINE: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); diff --git a/imkey-core/ikc-common/src/error.rs b/imkey-core/ikc-common/src/error.rs index d86f07ae..d6420895 100644 --- a/imkey-core/ikc-common/src/error.rs +++ b/imkey-core/ikc-common/src/error.rs @@ -88,4 +88,8 @@ pub enum CoinError { InvalidVersion, #[error("invalid addr length")] InvalidAddrLength, + #[error("invalid_utxo")] + InvalidUtxo, + #[error("missing_signature")] + MissingSignature, } diff --git a/imkey-core/ikc-common/src/hex.rs b/imkey-core/ikc-common/src/hex.rs new file mode 100644 index 00000000..f8ac1a77 --- /dev/null +++ b/imkey-core/ikc-common/src/hex.rs @@ -0,0 +1,124 @@ +use super::Result; +use anyhow::anyhow; + +pub trait ToHex { + fn to_hex(&self) -> String; + + fn to_0x_hex(&self) -> String { + format!("0x{}", self.to_hex()) + } +} + +pub trait FromHex +where + Self: Sized, +{ + fn from_hex>(value: T) -> Result; + + fn from_0x_hex>(value: T) -> Result { + if value.as_ref().len() == 0 { + return Ok(Self::from_hex("")?); + } + + let bytes = value.as_ref(); + Self::from_hex(&bytes[2..bytes.len()]) + } + + fn from_hex_auto>(value: T) -> Result { + let bytes = value.as_ref(); + if bytes.len() >= 2 && bytes[0] == b'0' && (bytes[1] == b'x' || bytes[1] == b'X') { + Self::from_0x_hex(value) + } else { + Self::from_hex(value) + } + } +} + +impl> ToHex for T { + fn to_hex(&self) -> String { + hex::encode(self) + } +} + +impl ToHex for [u8] { + fn to_hex(&self) -> String { + hex::encode(self) + } +} + +impl FromHex for Vec { + fn from_hex>(value: T) -> Result { + hex::decode(value).map_err(|e| anyhow!("{}", e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::{FromHex, ToHex}; + #[test] + fn test_to_hex() { + let data = vec![0x01, 0x02, 0x03, 0x04]; + assert_eq!(data.to_hex(), "01020304"); + } + + #[test] + fn test_to_hex_empty() { + let data = vec![]; + assert_eq!(data.to_hex(), ""); + } + + #[test] + fn test_to_hex_with_prefix_empty() { + let data = vec![]; + assert_eq!(data.to_0x_hex(), "0x"); + } + + #[test] + fn test_to_hex_from_slice() { + let data = vec![0x01, 0x02, 0x03, 0x04]; + assert_eq!(data[0..2].to_hex(), "0102"); + } + + #[test] + fn test_to_hex_from_fixed_array() { + let data = [0x01, 0x02, 0x03, 0x04]; + assert_eq!(data.to_hex(), "01020304"); + } + + #[test] + fn test_to_hex_with_prefix() { + let data = vec![0x01, 0x02, 0x03, 0x04]; + assert_eq!(data.to_0x_hex(), "0x01020304"); + } + + #[test] + fn test_from_hex() { + let data = vec![0x01, 0x02, 0x03, 0x04]; + assert_eq!(Vec::from_hex("01020304").unwrap(), data); + } + + #[test] + fn test_from_hex_with_prefix() { + let value = "0x01020304"; + assert_eq!(Vec::from_0x_hex(value).unwrap(), [0x01, 0x02, 0x03, 0x04]); + } + + #[test] + fn test_from_hex_with_prefix_error() { + let value = "0x010203041"; + assert!(Vec::from_0x_hex(value).is_err(),); + } + + #[test] + fn test_from_hex_auto() { + assert_eq!( + Vec::from_hex_auto("0x01020304").unwrap(), + [0x01, 0x02, 0x03, 0x04] + ); + + assert_eq!( + Vec::from_hex_auto("01020304").unwrap(), + [0x01, 0x02, 0x03, 0x04] + ); + } +} diff --git a/imkey-core/ikc-common/src/lib.rs b/imkey-core/ikc-common/src/lib.rs index 6c0d1e3a..b4b8df95 100644 --- a/imkey-core/ikc-common/src/lib.rs +++ b/imkey-core/ikc-common/src/lib.rs @@ -6,6 +6,7 @@ pub mod common; pub mod constants; pub mod curve; pub mod error; +mod hex; pub mod https; pub mod path; pub mod utility; diff --git a/imkey-core/ikc-common/src/utility.rs b/imkey-core/ikc-common/src/utility.rs index bf789174..a6938941 100644 --- a/imkey-core/ikc-common/src/utility.rs +++ b/imkey-core/ikc-common/src/utility.rs @@ -1,6 +1,7 @@ use crate::aes::cbc::encrypt_pkcs7; use crate::constants::SECP256K1_ENGINE; use crate::error::CommonError; +use crate::hex::FromHex; use crate::Result; use bitcoin::hashes::{sha256, Hash}; use bitcoin::util::base58; @@ -65,7 +66,7 @@ pub fn secp256k1_sign_verify(public: &[u8], signed: &[u8], message: &[u8]) -> Re .is_ok()) } -pub fn bigint_to_byte_vec(val: i64) -> Vec { +pub fn bigint_to_byte_vec(val: u64) -> Vec { let mut return_data = BigInt::from(val).to_signed_bytes_be(); while return_data.len() < 8 { return_data.insert(0, 0x00); @@ -206,6 +207,19 @@ pub fn network_convert(network: &str) -> Network { } } +pub fn utf8_or_hex_to_bytes(value: &str) -> Result> { + if value.to_lowercase().starts_with("0x") { + let ret = FromHex::from_0x_hex(value); + if ret.is_err() { + Ok(value.as_bytes().to_vec()) + } else { + ret + } + } else { + Ok(value.as_bytes().to_vec()) + } +} + #[cfg(test)] mod tests { use crate::utility; diff --git a/imkey-core/ikc-device/src/device_binding.rs b/imkey-core/ikc-device/src/device_binding.rs index f6f1e217..246e95a8 100644 --- a/imkey-core/ikc-device/src/device_binding.rs +++ b/imkey-core/ikc-device/src/device_binding.rs @@ -237,7 +237,7 @@ pub fn bind_test() { // pub const TEST_KEY_PATH: &str = "/tmp/"; // pub const TEST_BIND_CODE: &str = "MCYNK5AH"; pub const TEST_KEY_PATH: &str = "/tmp/"; -pub const TEST_BIND_CODE: &str = "7FVRAJJ7"; +pub const TEST_BIND_CODE: &str = "5PJT7223"; #[cfg(test)] mod test { diff --git a/imkey-core/ikc-proto/src/api.proto b/imkey-core/ikc-proto/src/api.proto index 66a70a6e..49ed2497 100644 --- a/imkey-core/ikc-proto/src/api.proto +++ b/imkey-core/ikc-proto/src/api.proto @@ -28,7 +28,7 @@ message AddressParam { string chainType = 1; string path = 2; string network = 3; - bool isSegWit = 4; + string segWit = 4; } message AddressResult { @@ -41,7 +41,6 @@ message PubKeyParam { string chainType = 1; string path = 2; string network = 3; - string isSegWit = 4; } message PubKeyResult { diff --git a/imkey-core/ikc-proto/src/btc.proto b/imkey-core/ikc-proto/src/btc.proto index cf4b9c1e..df162078 100644 --- a/imkey-core/ikc-proto/src/btc.proto +++ b/imkey-core/ikc-proto/src/btc.proto @@ -3,8 +3,8 @@ package btcapi; message Utxo { string tx_hash = 1; - int32 vout = 2; - int64 amount = 3; + uint32 vout = 2; + uint64 amount = 3; string address = 4; string script_pubKey = 5; string derived_path = 6; @@ -18,13 +18,13 @@ message BtcTxExtra { } message BtcTxInput { string to = 1; - int64 amount = 2; - int64 fee = 3; - uint32 change_address_index = 4; + uint64 amount = 2; + uint64 fee = 3; + optional uint32 change_address_index = 4; repeated Utxo unspents = 5; string segWit = 6; string protocol = 7; - BtcTxExtra extra = 8; + optional BtcTxExtra extra = 8; } message BtcTxOutput { @@ -32,3 +32,20 @@ message BtcTxOutput { string txHash = 2; string wtxHash = 3; } + +message PsbtInput { + string psbt = 1; + bool autoFinalize = 2; +} + +message PsbtOutput { + string psbt = 1; +} + +message BtcMessageInput { + string message = 1; +} + +message BtcMessageOutput { + string signature = 1; +} diff --git a/imkey-core/ikc-proto/src/btcfork.proto b/imkey-core/ikc-proto/src/btcfork.proto index faee7d01..f4160478 100644 --- a/imkey-core/ikc-proto/src/btcfork.proto +++ b/imkey-core/ikc-proto/src/btcfork.proto @@ -3,8 +3,8 @@ package btcforkapi; message Utxo { string txHash = 1; - int32 vout = 2; - int64 amount = 3; + uint32 vout = 2; + uint64 amount = 3; string address = 4; string scriptPubKey = 5; string derivedPath = 6; @@ -13,9 +13,9 @@ message Utxo { message BtcForkTxInput { string to = 1; - int64 amount = 2; + uint64 amount = 2; repeated Utxo unspents = 3; - int64 fee = 4; + uint64 fee = 4; uint32 changeAddressIndex = 5; string changeAddress = 6; string segWit = 7; diff --git a/imkey-core/ikc-proto/src/common.proto b/imkey-core/ikc-proto/src/common.proto index ec52cb75..71b2dd89 100644 --- a/imkey-core/ikc-proto/src/common.proto +++ b/imkey-core/ikc-proto/src/common.proto @@ -11,4 +11,5 @@ message SignParam { string receiver = 6; string sender = 7; string fee = 8; + string segWit = 9; } \ No newline at end of file diff --git a/imkey-core/ikc-wallet/coin-bch/src/transaction.rs b/imkey-core/ikc-wallet/coin-bch/src/transaction.rs index 7ded7094..23812d2b 100644 --- a/imkey-core/ikc-wallet/coin-bch/src/transaction.rs +++ b/imkey-core/ikc-wallet/coin-bch/src/transaction.rs @@ -25,8 +25,8 @@ use std::str::FromStr; #[derive(Clone)] pub struct Utxo { pub txhash: String, - pub vout: i32, - pub amount: i64, + pub vout: u32, + pub amount: u64, pub address: String, pub script_pubkey: String, pub derive_path: String, @@ -35,9 +35,9 @@ pub struct Utxo { pub struct BchTransaction { pub to: String, - pub amount: i64, + pub amount: u64, pub unspents: Vec, - pub fee: i64, + pub fee: u64, } impl BchTransaction { @@ -278,15 +278,15 @@ impl BchTransaction { }) } - pub fn get_total_amount(&self) -> i64 { - let mut total_amount: i64 = 0; + pub fn get_total_amount(&self) -> u64 { + let mut total_amount = 0; for unspent in &self.unspents { total_amount += unspent.amount; } total_amount } - pub fn get_change_amount(&self) -> i64 { + pub fn get_change_amount(&self) -> u64 { let total_amount = self.get_total_amount(); let change_amout = total_amount - self.amount - self.fee; change_amout diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/address.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/address.rs index dcb091f0..f576e458 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/address.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/address.rs @@ -1,13 +1,17 @@ use crate::common::get_xpub_data; use crate::Result; use bitcoin::network::constants::Network; +use bitcoin::schnorr::UntweakedPublicKey; use bitcoin::util::bip32::{ChainCode, ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint}; use bitcoin::{Address, PublicKey}; +use bitcoin_hashes::{hash160, Hash}; use ikc_common::apdu::{ApduCheck, BtcApdu, CoinCommonApdu}; +use ikc_common::constants; use ikc_common::error::CommonError; use ikc_common::path::check_path_validity; +use ikc_common::utility::hex_to_bytes; use ikc_transport::message::send_apdu; -use secp256k1::PublicKey as Secp256k1PublicKey; +use secp256k1::{PublicKey as Secp256k1PublicKey, Secp256k1}; use std::str::FromStr; pub struct BtcAddress(); @@ -17,34 +21,24 @@ impl BtcAddress { get btc xpub by path */ pub fn get_xpub(network: Network, path: &str) -> Result { - //path check check_path_validity(path)?; - //get xpub data let xpub_data = get_xpub_data(path, true)?; let xpub_data = &xpub_data[..194].to_string(); - //get public key and chain code let pub_key = &xpub_data[..130]; let chain_code = &xpub_data[130..]; - //build parent public key obj let parent_xpub = get_xpub_data(Self::get_parent_path(path)?, true)?; let parent_xpub = &parent_xpub[..130].to_string(); - // let mut parent_pub_key_obj = PublicKey::from_str(parent_xpub)?; - // parent_pub_key_obj.compressed = true; - let parent_pub_key_obj = Secp256k1PublicKey::from_str(parent_xpub)?; //TODO 是否是压缩公钥 + let parent_pub_key_obj = Secp256k1PublicKey::from_str(parent_xpub)?; - //build child public key obj - // let mut pub_key_obj = PublicKey::from_str(pub_key)?; - // pub_key_obj.compressed = true; - let pub_key_obj = Secp256k1PublicKey::from_str(pub_key)?; //TODO 是否是压缩公钥 + let pub_key_obj = Secp256k1PublicKey::from_str(pub_key)?; - //get parent public key fingerprint let chain_code_obj = ChainCode::from(hex::decode(chain_code).unwrap().as_slice()); let parent_ext_pub_key = ExtendedPubKey { - network: network, - depth: 0 as u8, + network, + depth: 0u8, parent_fingerprint: Fingerprint::default(), child_number: ChildNumber::from_normal_idx(0).unwrap(), public_key: parent_pub_key_obj, @@ -56,21 +50,20 @@ impl BtcAddress { let chain_code_obj = ChainCode::from(hex::decode(chain_code).unwrap().as_slice()); let chain_number_vec: Vec = DerivationPath::from_str(path)?.into(); let extend_public_key = ExtendedPubKey { - network: network, + network, depth: chain_number_vec.len() as u8, parent_fingerprint: fingerprint_obj, child_number: *chain_number_vec.get(chain_number_vec.len() - 1).unwrap(), public_key: pub_key_obj, chain_code: chain_code_obj, }; - //get and return xpub Ok(extend_public_key.to_string()) } /** get btc address by path */ - pub fn get_address(network: Network, path: &str) -> Result { + pub fn p2pkh(network: Network, path: &str) -> Result { //path check check_path_validity(path)?; @@ -87,7 +80,7 @@ impl BtcAddress { /** get segwit address by path */ - pub fn get_segwit_address(network: Network, path: &str) -> Result { + pub fn p2shwpkh(network: Network, path: &str) -> Result { //path check check_path_validity(path)?; @@ -101,6 +94,29 @@ impl BtcAddress { Ok(Address::p2shwpkh(&pub_key_obj, network)?.to_string()) } + pub fn p2wpkh(network: Network, path: &str) -> Result { + check_path_validity(path)?; + + let xpub_data = get_xpub_data(path, true)?; + let pub_key = &xpub_data[..130]; + let mut pub_key_obj = PublicKey::from_str(pub_key)?; + pub_key_obj.compressed = true; + + Ok(Address::p2wpkh(&pub_key_obj, network)?.to_string()) + } + + pub fn p2tr(network: Network, path: &str) -> Result { + check_path_validity(path)?; + + let xpub_data = get_xpub_data(path, true)?; + let pub_key = &xpub_data[..130]; + let untweak_pub_key = + UntweakedPublicKey::from(secp256k1::PublicKey::from_slice(&hex_to_bytes(&pub_key)?)?); + + let secp256k1 = Secp256k1::new(); + Ok(Address::p2tr(&secp256k1, untweak_pub_key, None, network).to_string()) + } + pub fn get_pub_key(path: &str) -> Result { //path check check_path_validity(path)?; @@ -128,28 +144,42 @@ impl BtcAddress { Ok(&path[..end_flg]) } - pub fn display_address(network: Network, path: &str) -> Result { - //path check + pub fn display_address(network: Network, path: &str, seg_wit: &str) -> Result { check_path_validity(path)?; - let address_str = Self::get_address(network, path)?; - // let apdu_res = send_apdu(BtcApdu::btc_coin_reg(address_str.clone().into_bytes()))?; - let apdu_res = send_apdu(BtcApdu::register_address( - &address_str.clone().into_bytes().to_vec(), - ))?; + + let address = match seg_wit { + constants::BTC_SEG_WIT_TYPE_P2WPKH => Self::p2shwpkh(network, path)?, + constants::BTC_SEG_WIT_TYPE_VERSION_0 => Self::p2wpkh(network, path)?, + constants::BTC_SEG_WIT_TYPE_VERSION_1 => Self::p2tr(network, path)?, + _ => Self::p2pkh(network, path)?, + }; + + let apdu_res = send_apdu(BtcApdu::register_address(&address.as_bytes()))?; ApduCheck::check_response(apdu_res.as_str())?; - Ok(address_str) + Ok(address) } - pub fn display_segwit_address(network: Network, path: &str) -> Result { - //path check - check_path_validity(path)?; - let address_str = Self::get_segwit_address(network, path)?; - // let apdu_res = send_apdu(BtcApdu::btc_coin_reg(address_str.clone().into_bytes()))?; - let apdu_res = send_apdu(BtcApdu::register_address( - &address_str.clone().into_bytes().to_vec(), - ))?; - ApduCheck::check_response(apdu_res.as_str())?; - Ok(address_str) + pub fn from_public_key(public_key: &str, network: Network, seg_wit: &str) -> Result { + let mut pub_key_obj = PublicKey::from_str(public_key)?; + pub_key_obj.compressed = true; + let address = match seg_wit { + constants::BTC_SEG_WIT_TYPE_P2WPKH => { + Address::p2shwpkh(&pub_key_obj, network)?.to_string() + } + constants::BTC_SEG_WIT_TYPE_VERSION_0 => { + Address::p2wpkh(&pub_key_obj, network)?.to_string() + } + constants::BTC_SEG_WIT_TYPE_VERSION_1 => { + let untweak_pub_key = UntweakedPublicKey::from(secp256k1::PublicKey::from_slice( + &hex_to_bytes(&public_key)?, + )?); + let secp256k1 = Secp256k1::new(); + Address::p2tr(&secp256k1, untweak_pub_key, None, network).to_string() + } + _ => Address::p2pkh(&pub_key_obj, network).to_string(), + }; + + Ok(address) } } @@ -192,12 +222,12 @@ mod test { } #[test] - fn get_address_test() { + fn p2pkh_test() { bind_test(); let version: Network = Network::Bitcoin; let path: &str = "m/44'/0'/0'/0/0"; - let get_btc_address_result = BtcAddress::get_address(version, path); + let get_btc_address_result = BtcAddress::p2pkh(version, path); assert!(get_btc_address_result.is_ok()); let btc_address = get_btc_address_result.ok().unwrap(); @@ -205,18 +235,45 @@ mod test { } #[test] - fn get_segwit_address_test() { + fn p2shwpkh_address_test() { bind_test(); let version: Network = Network::Bitcoin; let path: &str = "m/49'/0'/0'/0/22"; - let segwit_address_result = BtcAddress::get_segwit_address(version, path); + let segwit_address_result = BtcAddress::p2shwpkh(version, path); assert!(segwit_address_result.is_ok()); let segwit_address = segwit_address_result.ok().unwrap(); assert_eq!("37E2J9ViM4QFiewo7aw5L3drF2QKB99F9e", segwit_address); } + #[test] + fn p2wpkh_address_test() { + bind_test(); + let network: Network = Network::Bitcoin; + let path: &str = "m/49'/0'/0'/0/22"; + let segwit_address_result = BtcAddress::p2wpkh(network, path); + + assert!(segwit_address_result.is_ok()); + let segwit_address = segwit_address_result.ok().unwrap(); + assert_eq!("bc1qe74h3vkdcj94uph4wdpk48nlqjdy42y87mdm7q", segwit_address); + } + + #[test] + fn p2tr_address_test() { + bind_test(); + + let network: Network = Network::Bitcoin; + let path: &str = "m/49'/0'/0'/0/22"; + let segwit_address_result = BtcAddress::p2tr(network, path); + + assert!(segwit_address_result.is_ok()); + let segwit_address = segwit_address_result.ok().unwrap(); + assert_eq!( + "bc1ph40wj9vl3kwhxq747wxkcr63e4r3uaryagpetnkey4zqhucmjfzse24jrd", + segwit_address + ); + } #[test] fn get_parent_path_test() { let path = "m/44'/0'/0'/0/0"; @@ -243,7 +300,7 @@ mod test { bind_test(); let version: Network = Network::Bitcoin; let path: &str = "m/44'/0'/0'/0/0"; - let result = BtcAddress::display_address(version, path); + let result = BtcAddress::display_address(version, path, "NONE"); assert!(result.is_ok()); let btc_address = result.ok().unwrap(); @@ -255,10 +312,37 @@ mod test { bind_test(); let network: Network = Network::Bitcoin; let path: &str = "m/49'/0'/0'/0/22"; - let result = BtcAddress::display_segwit_address(network, path); + let result = BtcAddress::display_address(network, path, "P2WPKH"); assert!(result.is_ok()); let segwit_address = result.ok().unwrap(); assert_eq!("37E2J9ViM4QFiewo7aw5L3drF2QKB99F9e", segwit_address); } + + #[test] + fn display_native_segwit_address_test() { + bind_test(); + let network: Network = Network::Bitcoin; + let path: &str = "m/84'/0'/0'"; + let result = BtcAddress::display_address(network, path, "VERSION_0"); + + assert!(result.is_ok()); + let segwit_address = result.ok().unwrap(); + assert_eq!("bc1qhuwav68m49d8epty9ztg8yag7ku27jfccyz3hp", segwit_address); + } + + #[test] + fn display_taproot_address_test() { + bind_test(); + let network: Network = Network::Bitcoin; + let path: &str = "m/86'/0'/0'"; + let result = BtcAddress::display_address(network, path, "VERSION_1"); + + assert!(result.is_ok()); + let segwit_address = result.ok().unwrap(); + assert_eq!( + "bc1p26r56upnktz0qm4vxw3228v956rxsc4sevasswxdvh9ysnq509fqctph3w", + segwit_address + ); + } } diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/btcapi.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/btcapi.rs index f9217c83..5133d768 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/btcapi.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/btcapi.rs @@ -3,10 +3,10 @@ pub struct Utxo { #[prost(string, tag = "1")] pub tx_hash: ::prost::alloc::string::String, - #[prost(int32, tag = "2")] - pub vout: i32, - #[prost(int64, tag = "3")] - pub amount: i64, + #[prost(uint32, tag = "2")] + pub vout: u32, + #[prost(uint64, tag = "3")] + pub amount: u64, #[prost(string, tag = "4")] pub address: ::prost::alloc::string::String, #[prost(string, tag = "5")] @@ -31,12 +31,12 @@ pub struct BtcTxExtra { pub struct BtcTxInput { #[prost(string, tag = "1")] pub to: ::prost::alloc::string::String, - #[prost(int64, tag = "2")] - pub amount: i64, - #[prost(int64, tag = "3")] - pub fee: i64, - #[prost(uint32, tag = "4")] - pub change_address_index: u32, + #[prost(uint64, tag = "2")] + pub amount: u64, + #[prost(uint64, tag = "3")] + pub fee: u64, + #[prost(uint32, optional, tag = "4")] + pub change_address_index: ::core::option::Option, #[prost(message, repeated, tag = "5")] pub unspents: ::prost::alloc::vec::Vec, #[prost(string, tag = "6")] @@ -56,3 +56,29 @@ pub struct BtcTxOutput { #[prost(string, tag = "3")] pub wtx_hash: ::prost::alloc::string::String, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PsbtInput { + #[prost(string, tag = "1")] + pub psbt: ::prost::alloc::string::String, + #[prost(bool, tag = "2")] + pub auto_finalize: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PsbtOutput { + #[prost(string, tag = "1")] + pub psbt: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BtcMessageInput { + #[prost(string, tag = "1")] + pub message: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct BtcMessageOutput { + #[prost(string, tag = "1")] + pub signature: ::prost::alloc::string::String, +} diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/common.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/common.rs index d269bb86..8a07e4b2 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/common.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/common.rs @@ -1,8 +1,9 @@ use crate::transaction::Utxo; use crate::Result; +use bitcoin::schnorr::UntweakedPublicKey; use bitcoin::util::base58; use bitcoin::util::bip32::{ChainCode, ChildNumber, ExtendedPubKey}; -use bitcoin::{Address, Network, PublicKey}; +use bitcoin::{Address, AddressType, Network, PublicKey}; use ikc_common::apdu::{ApduCheck, BtcApdu, CoinCommonApdu}; use ikc_common::error::CoinError; use ikc_common::utility::{hex_to_bytes, sha256_hash}; @@ -11,60 +12,23 @@ use secp256k1::{ecdsa::Signature, Message, PublicKey as Secp256k1PublicKey, Secp use std::str::FromStr; /** -utxo address verify +get utxo public key */ -pub fn address_verify( - utxos: &Vec, - network: Network, - trans_type_flg: TransTypeFlg, -) -> Result> { +pub fn get_utxo_pub_key(utxos: &Vec) -> Result> { let mut utxo_pub_key_vec: Vec = vec![]; for utxo in utxos { - //get xpub and sign data let xpub_data = get_xpub_data(&utxo.derive_path, false)?; //parsing xpub data - let public_key = &xpub_data[..130]; - let chain_code = &xpub_data[130..194]; - let mut extend_public_key = ExtendedPubKey { - network, - depth: 0, - parent_fingerprint: Default::default(), - child_number: ChildNumber::from_normal_idx(0)?, - public_key: Secp256k1PublicKey::from_str(public_key)?, - chain_code: ChainCode::from(hex_to_bytes(chain_code)?.as_slice()), - }; + let derive_pub_key = &xpub_data[..130]; - //verify address - let se_gen_address: Result = match trans_type_flg { - TransTypeFlg::BTC => Ok(Address::p2pkh( - &PublicKey::from_str(extend_public_key.public_key.to_string().as_str())?, - network, - ) - .to_string()), - TransTypeFlg::SEGWIT => Ok(Address::p2shwpkh( - &PublicKey::from_str(extend_public_key.public_key.to_string().as_str())?, - network, - )? - .to_string()), - }; - let se_gen_address_str = se_gen_address?; - let utxo_address = utxo.address.to_string(); - if !se_gen_address_str.eq(&utxo_address) { - return Err(CoinError::ImkeyAddressMismatchWithPath.into()); - } - utxo_pub_key_vec.push(extend_public_key.public_key.to_string()); + let mut public_key = PublicKey::from_str(derive_pub_key)?; + public_key.compressed = true; + + utxo_pub_key_vec.push(public_key.to_string()); } Ok(utxo_pub_key_vec) } -/** -Transaction type identification -*/ -pub enum TransTypeFlg { - BTC, - SEGWIT, -} - /** get xpub */ @@ -76,6 +40,15 @@ pub fn get_xpub_data(path: &str, verify_flag: bool) -> Result { Ok(xpub_data) } +/** +select btc applet + */ +pub fn select_btc_applet() -> Result<()> { + let select_response = send_apdu(BtcApdu::select_applet())?; + ApduCheck::check_response(&select_response)?; + Ok(()) +} + /** sign verify */ @@ -99,24 +72,32 @@ pub fn secp256k1_sign_verify(public: &[u8], signed: &[u8], message: &[u8]) -> Re get address version */ pub fn get_address_version(network: Network, address: &str) -> Result { - match network { + let version = match network { Network::Bitcoin => { - if !address.starts_with('1') && !address.starts_with('3') { - return Err(CoinError::AddressTypeMismatch.into()); + if address.starts_with('1') || address.starts_with('3') { + let address_bytes = base58::from(address)?; + address_bytes.as_slice()[0] + } else if address.starts_with("bc1") { + 'b' as u8 + } else { + return Err(CoinError::InvalidAddress.into()); } } Network::Testnet => { - if !address.starts_with('m') && !address.starts_with('n') && !address.starts_with('2') { - return Err(CoinError::AddressTypeMismatch.into()); + if address.starts_with('m') || address.starts_with('n') || address.starts_with('2') { + let address_bytes = base58::from(address)?; + address_bytes.as_slice()[0] + } else if address.starts_with("tb1") { + 't' as u8 + } else { + return Err(CoinError::InvalidAddress.into()); } } _ => { return Err(CoinError::ImkeySdkIllegalArgument.into()); } - } - //get address version - let address_bytes = base58::from(address)?; - Ok(address_bytes.as_slice()[0]) + }; + Ok(version) } pub struct TxSignResult { diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs index b1e432af..49980670 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/lib.rs @@ -1,8 +1,11 @@ pub mod address; pub mod btcapi; pub mod common; +pub mod message; +pub mod psbt; pub mod transaction; pub mod usdt_transaction; + extern crate anyhow; use core::result; pub type Result = result::Result; diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/message.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/message.rs new file mode 100644 index 00000000..c749b463 --- /dev/null +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/message.rs @@ -0,0 +1,250 @@ +use crate::address::BtcAddress; +use crate::btcapi::{BtcMessageInput, BtcMessageOutput}; +use crate::common::select_btc_applet; +use crate::psbt::PsbtSigner; +use crate::Result; +use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::{ + Address, OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Txid, Witness, +}; +use bitcoin_hashes::hex::ToHex; +use hex::FromHex; +use ikc_common::error::CoinError; +use ikc_common::utility::{network_convert, sha256_hash, utf8_or_hex_to_bytes}; +use std::str::FromStr; + +pub struct MessageSinger { + pub derivation_path: String, + pub chain_type: String, + pub network: String, + pub seg_wit: String, +} +impl MessageSinger { + pub fn sign_message(&self, input: BtcMessageInput) -> Result { + let data = utf8_or_hex_to_bytes(&input.message)?; + + let path = format!("{}/0/0", self.derivation_path); + + let pub_key = BtcAddress::get_pub_key(&path)?; + + let network = network_convert(&self.network); + let address = BtcAddress::from_public_key(&pub_key, network, &self.seg_wit)?; + let script_pubkey = Address::from_str(&address)?.script_pubkey(); + let tx_id = get_spend_tx_id(&data, script_pubkey.clone())?; + + select_btc_applet()?; + + let mut psbt = create_to_sign_empty(tx_id, script_pubkey)?; + let mut psbt_signer = + PsbtSigner::new(&mut psbt, &self.derivation_path, true, network, true)?; + + psbt_signer.prevouts()?; + + let pub_keys = psbt_signer.get_pub_key()?; + + psbt_signer.calc_tx_hash()?; + + psbt_signer.get_preview_info()?; + + psbt_signer.tx_preview(network)?; + + psbt_signer.sign(&pub_keys)?; + + if let Some(witness) = &psbt.inputs[0].final_script_witness { + Ok(BtcMessageOutput { + signature: witness_to_vec(witness.to_vec()).to_hex(), + }) + } else { + if let Some(script_sig) = &psbt.inputs[0].final_script_sig { + Ok(BtcMessageOutput { + signature: format!("02{}", script_sig.to_hex()), + }) + } else { + Err(CoinError::MissingSignature.into()) + } + } + } +} + +const UTXO: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const TAG: &str = "BIP0322-signed-message"; +fn get_spend_tx_id(data: &[u8], script_pub_key: Script) -> Result { + let tag_hash = sha256_hash(&TAG.as_bytes().to_vec()); + let mut to_sign = Vec::new(); + to_sign.extend(tag_hash.clone()); + to_sign.extend(tag_hash); + to_sign.extend(data); + + let hash = sha256_hash(&to_sign); + let mut script_sig = Vec::new(); + script_sig.extend([0x00, 0x20]); + script_sig.extend(hash); + + //Tx ins + let ins = vec![TxIn { + previous_output: OutPoint { + txid: UTXO.parse()?, + vout: 0xFFFFFFFF, + }, + script_sig: Script::from(script_sig), + sequence: Sequence(0), + witness: Witness::new(), + }]; + + //Tx outs + let outs = vec![TxOut { + value: 0, + script_pubkey: script_pub_key, + }]; + + let tx = Transaction { + version: 0, + lock_time: PackedLockTime::ZERO, + input: ins, + output: outs, + }; + + Ok(tx.txid()) +} + +fn create_to_sign_empty(txid: Txid, script_pub_key: Script) -> Result { + //Tx ins + let ins = vec![TxIn { + previous_output: OutPoint { txid, vout: 0 }, + script_sig: Script::new(), + sequence: Sequence(0), + witness: Witness::new(), + }]; + + //Tx outs + let outs = vec![TxOut { + value: 0, + script_pubkey: Script::from(Vec::::from_hex("6a")?), + }]; + + let tx = Transaction { + version: 0, + lock_time: PackedLockTime::ZERO, + input: ins, + output: outs, + }; + + let mut psbt = PartiallySignedTransaction::from_unsigned_tx(tx)?; + psbt.inputs[0].witness_utxo = Some(TxOut { + value: 0, + script_pubkey: script_pub_key, + }); + + Ok(psbt) +} + +fn witness_to_vec(witness: Vec>) -> Vec { + let mut ret: Vec = Vec::new(); + ret.push(witness.len() as u8); + for item in witness { + ret.push(item.len() as u8); + ret.extend(item); + } + ret +} + +#[cfg(test)] +mod tests { + use crate::address::BtcAddress; + use crate::btcapi::BtcMessageInput; + use crate::message::MessageSinger; + use bitcoin::{Address, Network}; + use ikc_common::SignParam; + use ikc_device::device_binding::bind_test; + use std::str::FromStr; + + #[test] + fn test_to_spend_tx_id() { + bind_test(); + + let derivation_path = "m/44'/0'/0'/0/0"; + let pub_key = BtcAddress::get_pub_key(derivation_path).unwrap(); + let network = Network::Bitcoin; + let seg_wit = "VERSION_0"; + let address = BtcAddress::from_public_key(&pub_key, Network::Testnet, seg_wit).unwrap(); + let address = Address::from_str(&address).unwrap(); + let message = "hello world"; + + assert_eq!( + super::get_spend_tx_id(message.as_bytes(), address.script_pubkey()) + .unwrap() + .to_string(), + "24bca2df5140bcf6a6aeafd141ad40b0595aa6998ca0fc733488d7131ca7763f" + ); + } + + #[test] + fn test_bip32_p2sh_p2wpkh() { + bind_test(); + + let singer = MessageSinger { + derivation_path: "m/49'/0'/0'".to_string(), + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "P2WPKH".to_string(), + }; + let input = BtcMessageInput { + message: "hello world".to_string(), + }; + + let output = singer.sign_message(input).unwrap(); + assert_eq!(output.signature, "02473044022000ae3c9439681a4ba05e74d0805210f71c31f92130bcec28934d29beaf5f4f890220327cbf8a189eee4cb35a2599f6fd97b0774bec2e4191d74b3460f746732f8a03012103036695c5f3de2e2792b170f59679d4db88a8516728012eaa42a22ce6f8bf593b"); + } + + #[test] + fn test_bip32_p2pkh() { + bind_test(); + + let singer = MessageSinger { + derivation_path: "m/44'/0'/0'".to_string(), + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "NONE".to_string(), + }; + let input = BtcMessageInput { + message: "hello world".to_string(), + }; + let output = singer.sign_message(input).unwrap(); + assert_eq!(output.signature, "02483045022100dbbdfedfb1902ca12c6cba14d4892a98f77c434daaa4f97fd35e618374c908f602206527ff2b1ce550c16c836c2ce3508bfae543fa6c11759d2f4966cc0d3552c4430121026b5b6a9d041bc5187e0b34f9e496436c7bff261c6c1b5f3c06b433c61394b868"); + } + + #[test] + fn test_bip322_p2wpkh() { + bind_test(); + + let singer = MessageSinger { + derivation_path: "m/44'/0'/0'".to_string(), + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }; + let input = BtcMessageInput { + message: "hello world".to_string(), + }; + let output = singer.sign_message(input).unwrap(); + assert_eq!(output.signature, "024830450221009f003820d1db93bf78be08dafdd05b7dde7c31a73c9be36b705a15329bd3d0e502203eb6f1a34466995e4b9c281bf4a093a1f55a21b2ef961438c9ae284efab27dda0121026b5b6a9d041bc5187e0b34f9e496436c7bff261c6c1b5f3c06b433c61394b868"); + } + + #[test] + fn test_bip322_p2tr() { + bind_test(); + + let singer = MessageSinger { + derivation_path: "m/86'/0'/0'".to_string(), + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "VERSION_1".to_string(), + }; + let input = BtcMessageInput { + message: "Sign this message to log in to https://www.subber.xyz // 200323342" + .to_string(), + }; + let output = singer.sign_message(input).unwrap(); + // assert_eq!(output.signature, "0140a868e67a50f6dff3e25f6b015f595d89de54e330a6e1dfb4925269577730803e10a43562b25979a704f1d6c856e623681f292ce0ddf2281f42c033db013b4326"); + } +} diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/psbt.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/psbt.rs new file mode 100644 index 00000000..4fd610f4 --- /dev/null +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/psbt.rs @@ -0,0 +1,1048 @@ +use crate::address::BtcAddress; +use crate::btcapi::{PsbtInput, PsbtOutput}; +use crate::common::{get_address_version, get_xpub_data, select_btc_applet}; +use crate::Result; +use anyhow::anyhow; +use bitcoin::blockdata::script::Builder; +use bitcoin::consensus::{serialize, Decodable, Encodable}; +use bitcoin::psbt::serialize::Serialize; +use bitcoin::psbt::Psbt; +use bitcoin::schnorr::{TapTweak, UntweakedPublicKey}; +use bitcoin::util::taproot::{TapLeafHash, TapTweakHash}; +use bitcoin::{ + Address, EcdsaSig, EcdsaSighashType, Network, PackedLockTime, PublicKey, SchnorrSig, + SchnorrSighashType, Script, Transaction, TxOut, WPubkeyHash, Witness, +}; +use bitcoin_hashes::hex::ToHex; +use bitcoin_hashes::{hash160, Hash}; +use hex::FromHex; +use ikc_common::apdu::{ApduCheck, BtcApdu}; +use ikc_common::coin_info::coin_info_from_param; +use ikc_common::constants; +use ikc_common::constants::TIMEOUT_LONG; +use ikc_common::error::CoinError; +use ikc_common::path::{check_path_validity, get_account_path}; +use ikc_common::utility::{bigint_to_byte_vec, hex_to_bytes, secp256k1_sign, sha256_hash}; +use ikc_device::device_binding::KEY_MANAGER; +use ikc_transport::message::{send_apdu, send_apdu_timeout}; +use secp256k1::{ + ecdsa::Signature, schnorr::Signature as SchnorrSignature, PublicKey as Secp256k1PublicKey, +}; +use std::collections::BTreeMap; +use std::io::Cursor; +use std::str::FromStr; +use std::usize; + +pub struct PsbtSigner<'a> { + psbt: &'a mut Psbt, + derivation_path: String, + auto_finalize: bool, + prevouts: Vec, + network: Network, + preview_output: Vec, + is_sign_message: bool, +} + +impl<'a> PsbtSigner<'a> { + pub fn new( + psbt: &'a mut Psbt, + derivation_path: &str, + auto_finalize: bool, + network: Network, + is_sign_message: bool, + ) -> Result { + let mut psbt_signer = PsbtSigner { + psbt, + derivation_path: derivation_path.to_string(), + prevouts: vec![], + auto_finalize, + network, + preview_output: vec![], + is_sign_message, + }; + psbt_signer.get_preview_output()?; + Ok(psbt_signer) + } + + pub fn sign(&mut self, pub_keys: &Vec) -> Result<()> { + for idx in 0..self.prevouts.len() { + let prevout = &self.prevouts[idx]; + + if prevout.script_pubkey.is_p2pkh() { + self.sign_p2pkh(idx, &pub_keys[idx])?; + + if self.auto_finalize { + self.finalize_p2pkh(idx); + } + } else if prevout.script_pubkey.is_p2sh() { + self.sign_p2sh_nested_p2wpkh(idx, &pub_keys[idx])?; + + if self.auto_finalize { + self.finalize_p2sh_nested_p2wpkh(idx); + } + } else if prevout.script_pubkey.is_v0_p2wpkh() { + self.sign_p2wpkh(idx, &pub_keys[idx])?; + + if self.auto_finalize { + self.finalize_p2wpkh(idx); + } + } else if !self.psbt.inputs.first().unwrap().tap_scripts.is_empty() { + let input = self.psbt.inputs[idx].clone(); + let (_, script_leaf) = input.tap_scripts.first_key_value().unwrap(); + let (script, leaf_version) = script_leaf; + self.sign_p2tr_script( + idx, + &pub_keys[idx], + Some(( + TapLeafHash::from_script(script, leaf_version.clone()).into(), + 0xFFFFFFFF, + )), + )?; + + if self.auto_finalize { + self.finalize_p2tr(idx); + } + } else if prevout.script_pubkey.is_v1_p2tr() { + self.sign_p2tr(idx, &pub_keys[idx])?; + + if self.auto_finalize { + self.finalize_p2tr(idx); + } + } + + if self.auto_finalize { + self.clear_finalized_input(idx); + } + } + + Ok(()) + } + + pub fn prevouts(&mut self) -> Result<()> { + let len = self.psbt.inputs.len(); + let mut utxos = Vec::with_capacity(len); + + for i in 0..len { + let input = &self.psbt.inputs[i]; + let utxo = if let Some(witness_utxo) = &input.witness_utxo { + witness_utxo + } else if let Some(non_witness_utxo) = &input.non_witness_utxo { + let vout = self.psbt.unsigned_tx.input[i].previous_output.vout; + &non_witness_utxo.output[vout as usize] + } else { + return Err(CoinError::InvalidUtxo.into()); + }; + utxos.push(utxo.clone()); + } + self.prevouts = utxos; + Ok(()) + } + + fn get_path(&self, index: usize, is_p2tr: bool) -> Result { + let input = &self.psbt.inputs[index]; + let mut path = if !self.derivation_path.is_empty() { + format!("{}/0/0", self.derivation_path) + } else { + "".to_string() + }; + + if is_p2tr { + let tap_bip32_derivation = input.tap_key_origins.first_key_value(); + + if let Some((_, key_source)) = tap_bip32_derivation { + path = key_source.1 .1.to_string(); + } + } else { + let bip32_derivations = input.bip32_derivation.first_key_value(); + + if let Some((_, key_source)) = bip32_derivations { + path = key_source.1.to_string(); + } + } + Ok(path) + } + + pub fn get_pub_key(&self) -> Result> { + let mut pub_key_vec = vec![]; + for (idx, tx_out) in self.prevouts.iter().enumerate() { + let path = if tx_out.script_pubkey.is_v1_p2tr() { + self.get_path(idx, true)? + } else { + self.get_path(idx, false)? + }; + + let xpub_data = get_xpub_data(&path, false)?; + let derive_pub_key = &xpub_data[..130]; + let public_key = Secp256k1PublicKey::from_str(derive_pub_key)?; + pub_key_vec.push(public_key.to_string()) + } + + Ok(pub_key_vec) + } + + fn sign_p2pkh(&mut self, idx: usize, pub_key: &str) -> Result<()> { + let mut input_data_vec = vec![]; + for (x, prevout) in self.prevouts.iter().enumerate() { + let mut temp_serialize_txin = self + .psbt + .unsigned_tx + .input + .get(x) + .expect("get_input_error") + .clone(); + if x == idx { + temp_serialize_txin.script_sig = prevout.script_pubkey.clone(); + } + input_data_vec.extend_from_slice(serialize(&temp_serialize_txin).as_slice()); + } + input_data_vec.extend(serialize(&self.psbt.unsigned_tx.output)); + let btc_perpare_apdu_list = BtcApdu::btc_single_utxo_sign_prepare(0x50, &input_data_vec); + for apdu in btc_perpare_apdu_list { + ApduCheck::check_response(&send_apdu(apdu)?)?; + } + let path = self.get_path(idx, false)?; + let btc_sign_apdu = + BtcApdu::btc_single_utxo_sign(idx as u8, EcdsaSighashType::All.to_u32() as u8, &path); + + let btc_sign_apdu_return = send_apdu(btc_sign_apdu)?; + ApduCheck::check_response(&btc_sign_apdu_return)?; + let btc_sign_apdu_return = + &btc_sign_apdu_return[..btc_sign_apdu_return.len() - 4].to_string(); + let sign_result_str = btc_sign_apdu_return[2..btc_sign_apdu_return.len() - 2].to_string(); + + let mut signature_obj = Signature::from_compact(&hex::decode(&sign_result_str)?)?; + signature_obj.normalize_s(); + let pub_key = PublicKey::from_str(pub_key)?; + self.psbt.inputs[idx] + .partial_sigs + .insert(pub_key, EcdsaSig::sighash_all(signature_obj)); + + Ok(()) + } + + fn sign_p2sh_nested_p2wpkh(&mut self, idx: usize, pub_key: &str) -> Result<()> { + let mut temp_serialize_txin = self + .psbt + .unsigned_tx + .input + .get(idx) + .expect("get_input_error") + .clone(); + let prevout = &self.prevouts[idx]; + // let pub_key = &self.get_pub_key(idx, false)?; + let mut data: Vec = vec![]; + //txhash and vout + let txhash_data = serialize(&temp_serialize_txin.previous_output); + data.extend(txhash_data.iter()); + //lock script + let script = Script::new_v0_p2wpkh(&WPubkeyHash::from_hash(hash160::Hash::hash( + &hex_to_bytes(pub_key)?, + ))); + let script = script.p2wpkh_script_code().expect("must be v0_p2wpkh"); + data.extend(serialize(&script).iter()); + //amount + let mut utxo_amount = num_bigint::BigInt::from(prevout.value).to_signed_bytes_le(); + while utxo_amount.len() < 8 { + utxo_amount.push(0x00); + } + data.extend(utxo_amount.iter()); + //set sequence + let sequence = serialize(&temp_serialize_txin.sequence).to_vec(); + data.extend(sequence); + //set length + data.insert(0, data.len() as u8); + //address + let mut address_data: Vec = vec![]; + let sign_path = self.get_path(idx, false)?; + address_data.push(sign_path.as_bytes().len() as u8); + address_data.extend_from_slice(sign_path.as_bytes()); + data.extend(address_data.iter()); + + let sign_apdu = if idx == (self.psbt.unsigned_tx.input.len() - 1) { + BtcApdu::btc_segwit_sign(true, 0x01, data) + } else { + BtcApdu::btc_segwit_sign(false, 0x01, data) + }; + let sign_apdu_return_data = send_apdu(sign_apdu)?; + ApduCheck::check_response(&sign_apdu_return_data)?; + + //build signature obj + let sign_result_vec = + Vec::from_hex(&sign_apdu_return_data[2..sign_apdu_return_data.len() - 6]).unwrap(); + let mut signature_obj = Signature::from_compact(sign_result_vec.as_slice())?; + signature_obj.normalize_s(); + //generator der sign data + let mut sign_result_vec = signature_obj.serialize_der().to_vec(); + //add hash type + sign_result_vec.push(EcdsaSighashType::All.to_u32() as u8); + let pub_key = PublicKey::from_str(pub_key)?; + self.psbt.inputs[idx] + .partial_sigs + .insert(pub_key, EcdsaSig::sighash_all(signature_obj)); + Ok(()) + } + + fn sign_p2wpkh(&mut self, idx: usize, pub_key: &str) -> Result<()> { + let mut temp_serialize_txin = self + .psbt + .unsigned_tx + .input + .get(idx) + .expect("get_input_error") + .clone(); + let prevout = &self.prevouts[idx]; + let mut data: Vec = vec![]; + //txhash and vout + let txhash_data = serialize(&temp_serialize_txin.previous_output); + data.extend(txhash_data.iter()); + //lock script + let script = prevout + .script_pubkey + .p2wpkh_script_code() + .expect("must be v0_p2wpkh"); + data.extend(serialize(&script).iter()); + //amount + let mut utxo_amount = num_bigint::BigInt::from(prevout.value).to_signed_bytes_le(); + while utxo_amount.len() < 8 { + utxo_amount.push(0x00); + } + data.extend(utxo_amount.iter()); + //set sequence + let sequence = serialize(&temp_serialize_txin.sequence).to_vec(); + data.extend(sequence); + //set length + data.insert(0, data.len() as u8); + //address + let mut address_data: Vec = vec![]; + let sign_path = self.get_path(idx, false)?; + address_data.push(sign_path.as_bytes().len() as u8); + address_data.extend_from_slice(sign_path.as_bytes()); + data.extend(address_data.iter()); + + let sign_apdu = if idx == (self.psbt.unsigned_tx.input.len() - 1) { + BtcApdu::btc_segwit_sign(true, 0x01, data) + } else { + BtcApdu::btc_segwit_sign(false, 0x01, data) + }; + let sign_apdu_return_data = send_apdu(sign_apdu)?; + ApduCheck::check_response(&sign_apdu_return_data)?; + //build signature obj + let sign_result_vec = + Vec::from_hex(&sign_apdu_return_data[2..sign_apdu_return_data.len() - 6]).unwrap(); + let mut signature_obj = Signature::from_compact(sign_result_vec.as_slice())?; + signature_obj.normalize_s(); + let pub_key = PublicKey::from_str(pub_key)?; + self.psbt.inputs[idx] + .partial_sigs + .insert(pub_key, EcdsaSig::sighash_all(signature_obj)); + Ok(()) + } + + fn sign_p2tr(&mut self, idx: usize, pub_key: &str) -> Result<()> { + let mut data: Vec = vec![]; + // epoch (1). + data.push(0x00u8); + // hash_type (1). + data.push(SchnorrSighashType::Default as u8); + //nVersion (4): + //nLockTime (4) + // data.extend(serialize(&PackedLockTime::ZERO)); + data.extend(serialize(&self.psbt.unsigned_tx.lock_time)); + //prevouts_hash + amounts_hash + script_pubkeys_hash + sequences_hash + sha_outputs (32) + //spend_type (1) + data.push(0x00u8); + //input_index (4) + data.extend(serialize(&(idx as u32))); + + let mut path_data: Vec = vec![]; + let sign_path = self.get_path(idx, true)?; + path_data.push(sign_path.as_bytes().len() as u8); + path_data.extend_from_slice(sign_path.as_bytes()); + data.extend(path_data.iter()); + + let mut tweaked_pub_key_data: Vec = vec![]; + let untweaked_public_key = UntweakedPublicKey::from_str(&pub_key[2..66])?; + let tweaked_pub_key = TapTweakHash::from_key_and_tweak(untweaked_public_key, None).to_vec(); + tweaked_pub_key_data.push(tweaked_pub_key.len() as u8); + tweaked_pub_key_data.extend_from_slice(&tweaked_pub_key); + data.extend(tweaked_pub_key_data.iter()); + + let sign_apdu = if idx == (self.psbt.unsigned_tx.input.len() - 1) { + BtcApdu::btc_taproot_sign(true, data) + } else { + BtcApdu::btc_taproot_sign(false, data) + }; + let sign_result = send_apdu(sign_apdu)?; + ApduCheck::check_response(&sign_result)?; + + let sign_bytes = hex_to_bytes(&sign_result[2..(sign_result.len() - 4)])?; + let sig = SchnorrSignature::from_slice(&sign_bytes)?; + self.psbt.inputs[idx].tap_key_sig = Some(SchnorrSig { + hash_ty: SchnorrSighashType::Default, + sig, + }); + + Ok(()) + } + + fn sign_p2tr_script( + &mut self, + idx: usize, + pub_key: &str, + leaf_hash_code_separator: Option<(TapLeafHash, u32)>, + ) -> Result<()> { + let mut data: Vec = vec![]; + // epoch (1). + data.push(0x00u8); + // hash_type (1). + data.push(SchnorrSighashType::Default as u8); + //nVersion (4): + //nLockTime (4) + // data.extend(serialize(&PackedLockTime::ZERO)); + data.extend(serialize(&self.psbt.unsigned_tx.lock_time)); + //prevouts_hash + amounts_hash + script_pubkeys_hash + sequences_hash + sha_outputs (32) + //spend_type (1) + let mut spend_type = 0u8; + if leaf_hash_code_separator.is_some() { + spend_type |= 2u8; + } + data.push(spend_type); + //input_index (4) + data.extend(serialize(&(idx as u32))); + //leaf hash code separator + if let Some((hash, code_separator_pos)) = leaf_hash_code_separator { + let mut temp_data = hash.into_inner().to_vec(); + temp_data.push(0x00u8); //key_version_0 + let code_separator_pos = code_separator_pos.to_be_bytes(); + temp_data.extend(code_separator_pos); + data.push(temp_data.len() as u8); + data.extend(temp_data); + } + let mut path_data: Vec = vec![]; + let sign_path = self.get_path(idx, true)?; + path_data.push(sign_path.as_bytes().len() as u8); + path_data.extend_from_slice(sign_path.as_bytes()); + data.extend(path_data.iter()); + let mut tweaked_pub_key_data: Vec = vec![]; + let untweaked_public_key = UntweakedPublicKey::from_str(&pub_key[2..66])?; + let tweaked_pub_key = TapTweakHash::from_key_and_tweak(untweaked_public_key, None).to_vec(); + tweaked_pub_key_data.push(tweaked_pub_key.len() as u8); + tweaked_pub_key_data.extend_from_slice(&tweaked_pub_key); + data.extend(tweaked_pub_key_data.iter()); + + let sign_apdu = if idx == (self.psbt.unsigned_tx.input.len() - 1) { + BtcApdu::btc_taproot_script_sign(true, data) + } else { + BtcApdu::btc_taproot_script_sign(false, data) + }; + + let sign_result = send_apdu(sign_apdu)?; + ApduCheck::check_response(&sign_result)?; + + let sign_bytes = hex_to_bytes(&sign_result[2..(sign_result.len() - 4)])?; + let sig = SchnorrSignature::from_slice(&sign_bytes)?; + self.psbt.inputs[idx].tap_key_sig = Some(SchnorrSig { + hash_ty: SchnorrSighashType::Default, + sig, + }); + + Ok(()) + } + + pub fn calc_tx_hash(&self) -> Result<()> { + let mut txhash_vout_vec = vec![]; + let mut sequence_vec = vec![]; + let mut amount_vec = vec![]; + let mut script_pubkeys_vec = vec![]; + for (idx, tx_in) in self.psbt.unsigned_tx.input.iter().enumerate() { + let prevout = &self.prevouts[idx]; + txhash_vout_vec.extend(serialize(&tx_in.previous_output)); + sequence_vec.extend(serialize(&tx_in.sequence)); + amount_vec.extend(serialize(&prevout.value)); + script_pubkeys_vec.extend(serialize(&prevout.script_pubkey)); + } + let mut calc_hash_apdu = vec![]; + calc_hash_apdu.extend(BtcApdu::btc_prepare(0x31, 0x40, &txhash_vout_vec)); + calc_hash_apdu.extend(BtcApdu::btc_prepare(0x31, 0x80, &sequence_vec)); + calc_hash_apdu.extend(BtcApdu::btc_prepare(0x31, 0x20, &amount_vec)); + calc_hash_apdu.extend(BtcApdu::btc_prepare(0x31, 0x21, &script_pubkeys_vec)); + for apdu in calc_hash_apdu { + ApduCheck::check_response(&send_apdu(apdu)?)?; + } + Ok(()) + } + + pub fn tx_preview(&self, network: Network) -> Result<()> { + let (total_amount, fee, outputs) = self.get_preview_info()?; + let mut preview_data = vec![]; + preview_data.extend(&serialize(&self.psbt.unsigned_tx.version)); //version + let input_number = self.psbt.unsigned_tx.input.len(); + preview_data.push(input_number as u8); //input number + preview_data.extend(&serialize(&self.psbt.unsigned_tx.lock_time)); //lock time + let mut sign_hash_type = Vec::new(); + let len = EcdsaSighashType::All + .to_u32() + .consensus_encode(&mut sign_hash_type) + .unwrap(); + debug_assert_eq!(len, sign_hash_type.len()); + preview_data.extend(&sign_hash_type); //hash type + preview_data.extend(bigint_to_byte_vec(total_amount)); //total payment amount + preview_data.extend(bigint_to_byte_vec(fee)); //fee + let mut output_serialize = vec![]; + for tx_out in self.psbt.unsigned_tx.output.iter() { + output_serialize.extend(serialize(tx_out)); + } + let hash = &sha256_hash(&output_serialize); + preview_data.extend_from_slice(hash); //output hash + let display_number = self.preview_output.len() as u16; + preview_data.extend(display_number.to_be_bytes()); + + //set 01 tag and length + preview_data.insert(0, preview_data.len() as u8); + preview_data.insert(0, 0x01); + + //use local private key sign data + let key_manager_obj = KEY_MANAGER.lock(); + let mut output_pareper_data = secp256k1_sign(&key_manager_obj.pri_key, &preview_data)?; + output_pareper_data.insert(0, output_pareper_data.len() as u8); + output_pareper_data.insert(0, 0x00); + output_pareper_data.extend(preview_data.iter()); + let btc_prepare_apdu_vec = BtcApdu::btc_prepare(0x4B, 0x00, &output_pareper_data); + for temp_str in btc_prepare_apdu_vec { + ApduCheck::check_response(&send_apdu(temp_str)?)?; + } + + let mut page_number = 0; + loop { + let mut outputs_data = if self.is_sign_message { + vec![0xFF, 0xFF] + } else { + self.serizalize_page_data(page_number, network)? + }; + //set 01 tag and length + outputs_data.insert(0, outputs_data.len() as u8); + outputs_data.insert(0, 0x01); + //use local private key sign data + let mut output_pareper_data = secp256k1_sign(&key_manager_obj.pri_key, &outputs_data)?; + output_pareper_data.insert(0, output_pareper_data.len() as u8); + output_pareper_data.insert(0, 0x00); + output_pareper_data.extend(outputs_data.iter()); + let sign_confirm = if self.is_sign_message { + BtcApdu::btc_psbt_preview(&output_pareper_data, 0x80) + } else { + BtcApdu::btc_psbt_preview(&output_pareper_data, 0x00) + }; + let response = &send_apdu_timeout(sign_confirm, TIMEOUT_LONG)?; + ApduCheck::check_response(response)?; + if response.len() > 4 { + let page_index = &response[..response.len() - 4]; + page_number = u16::from_str_radix(page_index, 16)? as usize; + } else { + break; + } + } + Ok(()) + } + + fn finalize_p2pkh(&mut self, index: usize) { + let mut input = &mut self.psbt.inputs[index]; + + if !input.partial_sigs.is_empty() { + let sig = input.partial_sigs.first_key_value().unwrap(); + + input.final_script_sig = Some( + Builder::new() + .push_slice(&sig.1.to_vec()) + .push_slice(&sig.0.to_bytes()) + .into_script(), + ); + } + } + + fn finalize_p2sh_nested_p2wpkh(&mut self, index: usize) { + let mut input = &mut self.psbt.inputs[index]; + + if !input.partial_sigs.is_empty() { + let sig = input.partial_sigs.first_key_value().unwrap(); + + let script = + Script::new_v0_p2wpkh(&WPubkeyHash::from_hash(Self::hash160(&sig.0.to_bytes()))); + + input.final_script_sig = Some(script); + + let mut witness = Witness::new(); + witness.push(sig.1.to_vec()); + witness.push(sig.0.to_bytes()); + + input.final_script_witness = Some(witness); + } + } + + fn finalize_p2wpkh(&mut self, index: usize) { + let mut input = &mut self.psbt.inputs[index]; + + if !input.partial_sigs.is_empty() { + let sig = input.partial_sigs.first_key_value().unwrap(); + let mut witness = Witness::new(); + + witness.push(sig.1.to_vec()); + witness.push(sig.0.to_bytes()); + + input.final_script_witness = Some(witness) + } + } + + fn finalize_p2tr(&mut self, index: usize) { + let mut input = &mut self.psbt.inputs[index]; + + if input.tap_key_sig.is_some() { + let mut witness = Witness::new(); + witness.push(input.tap_key_sig.unwrap().to_vec()); + + if !input.tap_scripts.is_empty() { + let (control_block, script_leaf) = input.tap_scripts.first_key_value().unwrap(); + + let (script, _) = script_leaf; + witness.push(script.as_bytes().to_vec()); + witness.push(control_block.serialize()) + } + + input.final_script_witness = Some(witness); + } + } + + fn clear_finalized_input(&mut self, index: usize) { + let mut input = &mut self.psbt.inputs[index]; + input.tap_key_sig = None; + input.tap_scripts = BTreeMap::new(); + input.tap_internal_key = None; + input.tap_merkle_root = None; + input.tap_script_sigs = BTreeMap::new(); + input.partial_sigs = BTreeMap::new(); + input.sighash_type = None; + input.redeem_script = None; + input.witness_script = None; + input.bip32_derivation = BTreeMap::new(); + input.unknown = BTreeMap::new(); + } + + fn hash160(input: &[u8]) -> hash160::Hash { + Hash::hash(input) + } + + fn get_preview_output(&mut self) -> Result<()> { + let mut preview_output: Vec = vec![]; + + for tx_out in self.psbt.unsigned_tx.output.iter() { + //remove empty and op_return TxOut + if tx_out.script_pubkey.is_empty() || tx_out.script_pubkey.is_op_return() { + continue; + } + //cale change index script + let tx_out_script = &tx_out.script_pubkey; + let address = if tx_out_script.is_p2pkh() { + let change_path = + Self::get_change_index(self.network, constants::BTC_SEG_WIT_TYPE_LEGACY)?; + let pub_key = BtcAddress::get_pub_key(&change_path)?; + BtcAddress::from_public_key( + &pub_key, + self.network, + constants::BTC_SEG_WIT_TYPE_LEGACY, + )? + } else if tx_out_script.is_v0_p2wpkh() { + let change_path = + Self::get_change_index(self.network, constants::BTC_SEG_WIT_TYPE_VERSION_0)?; + let pub_key = BtcAddress::get_pub_key(&change_path)?; + BtcAddress::from_public_key( + &pub_key, + self.network, + constants::BTC_SEG_WIT_TYPE_VERSION_0, + )? + } else if tx_out_script.is_p2sh() { + let change_path = + Self::get_change_index(self.network, constants::BTC_SEG_WIT_TYPE_P2WPKH)?; + let pub_key = BtcAddress::get_pub_key(&change_path)?; + BtcAddress::from_public_key( + &pub_key, + self.network, + constants::BTC_SEG_WIT_TYPE_P2WPKH, + )? + } else if tx_out_script.is_v1_p2tr() { + let change_path = + Self::get_change_index(self.network, constants::BTC_SEG_WIT_TYPE_VERSION_1)?; + let pub_key = BtcAddress::get_pub_key(&change_path)?; + BtcAddress::from_public_key( + &pub_key, + self.network, + constants::BTC_SEG_WIT_TYPE_VERSION_1, + )? + } else { + continue; + }; + //remove change TxOut + let script_hex = Address::from_str(&address)?.script_pubkey().to_hex(); + if script_hex.eq(&tx_out.script_pubkey.to_hex()) { + continue; + } + preview_output.push(tx_out.clone()); + } + self.preview_output = preview_output; + Ok(()) + } + + pub fn get_preview_info(&self) -> Result<(u64, u64, Vec)> { + let mut outputs = &self.preview_output; + let payment_total_amount = outputs.iter().map(|tx_out| tx_out.value).sum(); + let input_total_amount: u64 = self.prevouts.iter().map(|tx_out| tx_out.value).sum(); + let output_total_amount: u64 = self + .psbt + .unsigned_tx + .output + .iter() + .map(|tx_out| tx_out.value) + .sum(); + let fee = input_total_amount - output_total_amount; + Ok((payment_total_amount, fee, outputs.clone())) + } + + fn get_page_indices(total_number: usize, page_number: usize) -> Result<(usize, usize)> { + let total_pages = (total_number + constants::BTC_PSBT_TRX_PER_PAGE_NUMBER - 1) + / constants::BTC_PSBT_TRX_PER_PAGE_NUMBER; + if page_number >= total_pages { + return Err(anyhow!("page_number_out_of_range")); + } + let start_index = page_number * constants::BTC_PSBT_TRX_PER_PAGE_NUMBER; + let end_index = usize::min( + total_number, + start_index + constants::BTC_PSBT_TRX_PER_PAGE_NUMBER, + ) - 1; + + if start_index >= total_number { + Ok((total_number, total_number - 1)) + } else if end_index >= total_number { + Ok((start_index, total_number - 1)) + } else { + Ok((start_index, end_index)) + } + } + + fn serizalize_page_data(&self, page_number: usize, network: Network) -> Result> { + let preview_output = &self.preview_output; + let (start_index, end_index) = Self::get_page_indices(preview_output.len(), page_number)?; + let mut data = vec![]; + data.extend((start_index as u16).to_be_bytes()); + data.extend((end_index as u16).to_be_bytes()); + for (index, output) in preview_output.iter().enumerate() { + if start_index <= index && end_index >= index { + let i_u16 = index as u16; + data.extend(i_u16.to_be_bytes()); + data.extend(serialize(&output.value)); + let address = Address::from_script(&output.script_pubkey, network)?; + let address_version = get_address_version(network, &address.to_string())?; + let script_bytes = serialize(&output.script_pubkey); + data.push((1 + script_bytes.len()) as u8); + data.push(address_version); + data.extend(script_bytes); + } + } + + Ok(data) + } + + fn get_change_index(network: Network, segwit: &str) -> Result { + let network = match network { + Network::Bitcoin => "MAINNET", + _ => "TESTNET", + }; + let coin_info = coin_info_from_param("BITCOIN", network, segwit, "secp256k1")?; + let change_path = get_account_path(&coin_info.derivation_path)?; + Ok(change_path) + } +} + +pub fn sign_psbt( + derivation_path: &str, + psbt_input: PsbtInput, + network: Network, +) -> Result { + check_path_validity(derivation_path)?; + + select_btc_applet()?; + + let mut reader = Cursor::new(Vec::::from_hex(psbt_input.psbt)?); + let mut psbt = Psbt::consensus_decode(&mut reader)?; + let mut signer = PsbtSigner::new( + &mut psbt, + derivation_path, + psbt_input.auto_finalize, + network, + false, + )?; + + signer.prevouts()?; + + let pub_keys = signer.get_pub_key()?; + + signer.calc_tx_hash()?; + + signer.get_preview_info()?; + + signer.tx_preview(network)?; + + signer.sign(&pub_keys)?; + + let mut vec = Vec::::new(); + let mut writer = Cursor::new(&mut vec); + psbt.consensus_encode(&mut writer)?; + + return Ok(PsbtOutput { + psbt: hex::encode(vec), + }); +} + +#[cfg(test)] +mod test { + use crate::btcapi::PsbtInput; + use crate::common::select_btc_applet; + use crate::psbt::PsbtSigner; + use bitcoin::consensus::Decodable; + use bitcoin::psbt::serialize::{Deserialize, Serialize}; + use bitcoin::psbt::Psbt; + use bitcoin::schnorr::TapTweak; + use bitcoin::util::bip32::DerivationPath; + use bitcoin::{schnorr, Address, Network, Transaction, TxOut}; + use bitcoin_hashes::hex::ToHex; + use hex::FromHex; + use ikc_device::device_binding::bind_test; + use secp256k1::schnorr::Signature; + use secp256k1::{Message, XOnlyPublicKey}; + use std::io::Cursor; + use std::str::FromStr; + + #[test] + fn test_sign_psbt_no_script() { + bind_test(); + + let psbt_input = PsbtInput { + psbt: "70736274ff0100db0200000001fa4c8d58b9b6c56ed0b03f78115246c99eb70f99b837d7b4162911d1016cda340200000000fdffffff0350c30000000000002251202114eda66db694d87ff15ddd5d3c4e77306b6e6dd5720cbd90cd96e81016c2b30000000000000000496a47626274340066f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229ec20acf33c17e5a6c92cced9f1d530cccab7aa3e53400456202f02fac95e9c481fa00d47b1700000000002251208f4ca6a7384f50a1fe00cba593d5a834b480c65692a76ae6202e1ce46cb1c233d80f03000001012be3bf1d00000000002251208f4ca6a7384f50a1fe00cba593d5a834b480c65692a76ae6202e1ce46cb1c23301172066f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e00000000".to_string(), + auto_finalize: true, + }; + + let psbt_output = super::sign_psbt("m/86'/1'/0'", psbt_input, Network::Bitcoin).unwrap(); + let mut reader = Cursor::new(Vec::::from_hex(&psbt_output.psbt).unwrap()); + let psbt = Psbt::consensus_decode(&mut reader).unwrap(); + let tx = psbt.extract_tx(); + let sig = schnorr::SchnorrSig::from_slice(&tx.input[0].witness.to_vec()[0]).unwrap(); + + let data = + Vec::::from_hex("3a66cf6ec1a87b10b86fa358baf64484bba8c61c9828e5cbe2eb8a3d4bbf190c") + .unwrap(); + let msg = Message::from_slice(&data).unwrap(); + let x_pub_key = XOnlyPublicKey::from_slice( + Vec::::from_hex("66f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e") + .unwrap() + .as_slice(), + ) + .unwrap(); + let SECP256K1_ENGINE: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); + let tweak_pub_key = x_pub_key.tap_tweak(&SECP256K1_ENGINE, None); + + // assert!(sig.sig.verify(&msg, &tweak_pub_key.0.to_inner()).is_ok()); + } + + #[test] + fn test_sign_psbt_script() { + bind_test(); + + let psbt_input = PsbtInput { + psbt: "70736274ff01005e02000000012bd2f6479f3eeaffe95c03b5fdd76a873d346459114dec99c59192a0cb6409e90000000000ffffffff01409c000000000000225120677cc88dc36a75707b370e27efff3e454d446ad55004dac1685c1725ee1a89ea000000000001012b50c3000000000000225120a9a3350206de400f09a73379ec1bcfa161fc11ac095e5f3d7354126f0ec8e87f6215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0d2956573f010fa1a3c135279c5eb465ec2250205dcdfe2122637677f639b1021356c963cd9c458508d6afb09f3fa2f9b48faec88e75698339a4bbb11d3fc9b0efd570120aff94eb65a2fe773a57c5bd54e62d8436a5467573565214028422b41bd43e29bad200aee0509b16db71c999238a4827db945526859b13c95487ab46725357c9a9f25ac20113c3a32a9d320b72190a04a020a0db3976ef36972673258e9a38a364f3dc3b0ba2017921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4ba203bb93dfc8b61887d771f3630e9a63e97cbafcfcc78556a474df83a31a0ef899cba2040afaf47c4ffa56de86410d8e47baa2bb6f04b604f4ea24323737ddc3fe092dfba2079a71ffd71c503ef2e2f91bccfc8fcda7946f4653cef0d9f3dde20795ef3b9f0ba20d21faf78c6751a0d38e6bd8028b907ff07e9a869a43fc837d6b3f8dff6119a36ba20f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8eba20fa9d882d45f4060bdb8042183828cd87544f1ea997380e586cab77d5fd698737ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000".to_string(), + auto_finalize: true, + }; + + let psbt_output = super::sign_psbt("m/86'/1'/0'", psbt_input, Network::Bitcoin).unwrap(); + let mut reader = Cursor::new(Vec::::from_hex(&psbt_output.psbt).unwrap()); + let psbt = Psbt::consensus_decode(&mut reader).unwrap(); + let tx = psbt.extract_tx(); + let witness = tx.input[0].witness.to_vec(); + let sig = schnorr::SchnorrSig::from_slice(&witness[0]).unwrap(); + + let data = + Vec::::from_hex("56b6c5fd09753fbbbeb8f530308e4f7d2f404e02da767f033e926d27fcc2f37e") + .unwrap(); + let msg = Message::from_slice(&data).unwrap(); + let x_pub_key = XOnlyPublicKey::from_slice( + Vec::::from_hex("66f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e") + .unwrap() + .as_slice(), + ) + .unwrap(); + + let script = hex::encode(&witness[1]); + let control_block = hex::encode(&witness[2]); + assert_eq!(script, "20aff94eb65a2fe773a57c5bd54e62d8436a5467573565214028422b41bd43e29bad200aee0509b16db71c999238a4827db945526859b13c95487ab46725357c9a9f25ac20113c3a32a9d320b72190a04a020a0db3976ef36972673258e9a38a364f3dc3b0ba2017921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4ba203bb93dfc8b61887d771f3630e9a63e97cbafcfcc78556a474df83a31a0ef899cba2040afaf47c4ffa56de86410d8e47baa2bb6f04b604f4ea24323737ddc3fe092dfba2079a71ffd71c503ef2e2f91bccfc8fcda7946f4653cef0d9f3dde20795ef3b9f0ba20d21faf78c6751a0d38e6bd8028b907ff07e9a869a43fc837d6b3f8dff6119a36ba20f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8eba20fa9d882d45f4060bdb8042183828cd87544f1ea997380e586cab77d5fd698737ba569c"); + assert_eq!(control_block, "c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0d2956573f010fa1a3c135279c5eb465ec2250205dcdfe2122637677f639b1021356c963cd9c458508d6afb09f3fa2f9b48faec88e75698339a4bbb11d3fc9b0e"); + + // assert!(sig.sig.verify(&msg, &x_pub_key).is_ok()); + } + + #[test] + fn test_sign_psbt_multipayment() { + bind_test(); + + let raw_tx = "02000000054adc61444e5a4dd7021e52dc6f5adadd9a3286d346f5d9f023ebcde2af80a0ae0000000000ffffffff4adc61444e5a4dd7021e52dc6f5adadd9a3286d346f5d9f023ebcde2af80a0ae0100000000ffffffff12cc8049bf85b5e18cb2be8aa7aefc3afb8df4ec5c1f766750014cc95ca2dc130000000000ffffffff729e6570928cc65200f1d53def65a7934d2e9b543059d90598ed1d166af422010100000000ffffffffa126724475cd2f3252352b3543c8455c7999a8283883bd7a712a7d66609d92d80100000000ffffffff02409c00000000000022512036079c540758a51a86eeaf9e17668d4d8543d8b1b7e56fe2da0982c390c5655ef8fa0700000000002251209303a116174dd21ea473766659568ac24eb6b828c3ee998982d2ba070ea0615500000000"; + let mut tx = Transaction::deserialize(&Vec::from_hex(&raw_tx).unwrap()).unwrap(); + + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + let fake_pub_key = secp256k1::PublicKey::from_slice( + &Vec::::from_hex( + "0266f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e", + ) + .unwrap(), + ) + .unwrap(); + let fake_xonly_pub_key = XOnlyPublicKey::from_slice( + Vec::::from_hex("66f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e") + .unwrap() + .as_slice(), + ) + .unwrap(); + + psbt.inputs[0].tap_key_origins.insert( + fake_xonly_pub_key, + ( + Default::default(), + ( + Default::default(), + DerivationPath::from_str("m/86'/1'/0'/0/0").unwrap(), + ), + ), + ); + psbt.inputs[0].witness_utxo = Some(TxOut { + value: 20000, + script_pubkey: Address::from_str( + "tb1p3ax2dfecfag2rlsqewje84dgxj6gp3jkj2nk4e3q9cwwgm93cgesa0zwj4", + ) + .unwrap() + .script_pubkey(), + }); + + psbt.inputs[1].tap_key_origins.insert( + fake_xonly_pub_key, + ( + Default::default(), + ( + Default::default(), + DerivationPath::from_str("m/86'/1'/0'/1/53").unwrap(), + ), + ), + ); + psbt.inputs[1].witness_utxo = Some(TxOut { + value: 283000, + script_pubkey: Address::from_str( + "tb1pjvp6z9shfhfpafrnwen9j452cf8tdwpgc0hfnzvz62aqwr4qv92sg7qj9r", + ) + .unwrap() + .script_pubkey(), + }); + + psbt.inputs[2].bip32_derivation.insert( + fake_pub_key, + ( + Default::default(), + DerivationPath::from_str("m/84'/1'/0'/0/0").unwrap(), + ), + ); + psbt.inputs[2].witness_utxo = Some(TxOut { + value: 100000, + script_pubkey: Address::from_str("tb1qrfaf3g4elgykshfgahktyaqj2r593qkrae5v95") + .unwrap() + .script_pubkey(), + }); + + psbt.inputs[3].bip32_derivation.insert( + fake_pub_key, + ( + Default::default(), + DerivationPath::from_str("m/49'/1'/0'/0/0").unwrap(), + ), + ); + psbt.inputs[3].witness_utxo = Some(TxOut { + value: 100000, + script_pubkey: Address::from_str("2MwN441dq8qudMvtM5eLVwC3u4zfKuGSQAB") + .unwrap() + .script_pubkey(), + }); + + psbt.inputs[4].bip32_derivation.insert( + fake_pub_key, + ( + Default::default(), + DerivationPath::from_str("m/44'/1'/0'/0/0").unwrap(), + ), + ); + psbt.inputs[4].witness_utxo = Some(TxOut { + value: 100000, + script_pubkey: Address::from_str("mkeNU5nVnozJiaACDELLCsVUc8Wxoh1rQN") + .unwrap() + .script_pubkey(), + }); + + select_btc_applet().unwrap(); + + let mut signer = PsbtSigner::new(&mut psbt, "", true, Network::Testnet, false).unwrap(); + + signer.prevouts().unwrap(); + + let pub_keys = signer.get_pub_key().unwrap(); + + signer.calc_tx_hash().unwrap(); + + signer.get_preview_info().unwrap(); + + signer.tx_preview(Network::Bitcoin).unwrap(); + + signer.sign(&pub_keys).unwrap(); + + let tx = psbt.extract_tx(); + + let msg = Message::from_slice( + &Vec::from_hex("f01ba76b329132e48188ad10d00791647ee6d2f7fee5ef397f3481993c898de3") + .unwrap(), + ) + .unwrap(); + let sig = Signature::from_slice(&tx.input[0].witness.to_vec()[0]).unwrap(); + let pub_key = XOnlyPublicKey::from_slice( + &Vec::from_hex("8f4ca6a7384f50a1fe00cba593d5a834b480c65692a76ae6202e1ce46cb1c233") + .unwrap(), + ) + .unwrap(); + // assert!(sig.verify(&msg, &pub_key).is_ok()); + + let msg = Message::from_slice( + &Vec::from_hex("d0691b5ac1b338b9341790ea69417cb454cf346a718342fb4a846dbb8ae142e8") + .unwrap(), + ) + .unwrap(); + let sig = Signature::from_slice(&tx.input[1].witness.to_vec()[0]).unwrap(); + let pub_key = XOnlyPublicKey::from_slice( + &Vec::from_hex("9303a116174dd21ea473766659568ac24eb6b828c3ee998982d2ba070ea06155") + .unwrap(), + ) + .unwrap(); + // assert!(sig.verify(&msg, &pub_key).is_ok()); + + assert_eq!(tx.input[2].witness.to_vec()[0].to_hex(), "3044022022c2feaa4a225496fc6789c969fb776da7378f44c588ad812a7e1227ebe69b6302204fc7bf5107c6d02021fe4833629bc7ab71cefe354026ebd0d9c0da7d4f335f9401"); + assert_eq!( + tx.input[2].witness.to_vec()[1].to_hex(), + "02e24f625a31c9a8bae42239f2bf945a306c01a450a03fd123316db0e837a660c0" + ); + + assert_eq!(tx.input[3].witness.to_vec()[0].to_hex(), "3045022100dec4d3fd189b532ef04f41f68319ff7dc6a7f2351a0a8f98cb7f1ec1f6d71c7a02205e507162669b642fdb480a6c496abbae5f798bce4fd42cc390aa58e3847a1b9101"); + assert_eq!( + tx.input[3].witness.to_vec()[1].to_hex(), + "031aee5e20399d68cf0035d1a21564868f22bc448ab205292b4279136b15ecaebc" + ); + + assert_eq!(tx.input[4].script_sig.to_hex(), "483045022100ca32abc7b180c84cf76907e4e1e0c3f4c0d6e64de23b0708647ac6fee1c04c5b02206e7412a712424eb9406f18e00a42e0dffbfb5901932d1ef97843d9273865550e0121033d710ab45bb54ac99618ad23b3c1da661631aa25f23bfe9d22b41876f1d46e4e"); + } +} diff --git a/imkey-core/ikc-wallet/coin-bitcoin/src/transaction.rs b/imkey-core/ikc-wallet/coin-bitcoin/src/transaction.rs index 6af419b1..b4e93cee 100644 --- a/imkey-core/ikc-wallet/coin-bitcoin/src/transaction.rs +++ b/imkey-core/ikc-wallet/coin-bitcoin/src/transaction.rs @@ -1,33 +1,36 @@ use crate::address::BtcAddress; -use crate::common::{address_verify, get_address_version, TransTypeFlg, TxSignResult}; +use crate::common::{get_address_version, get_utxo_pub_key, TxSignResult}; use crate::Result; use bitcoin::blockdata::{opcodes, script::Builder}; use bitcoin::consensus::{serialize, Encodable}; use bitcoin::hashes::hex::FromHex; +use bitcoin::psbt::serialize::Serialize; +use bitcoin::schnorr::{TapTweak, UntweakedPublicKey}; +use bitcoin::util::taproot::TapTweakHash; use bitcoin::{ - Address, EcdsaSighashType, Network, OutPoint, PackedLockTime, Script, Sequence, Transaction, - TxIn, TxOut, Witness, + Address, EcdsaSighashType, Network, OutPoint, PackedLockTime, SchnorrSighashType, Script, + Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, }; use bitcoin_hashes::hash160; use bitcoin_hashes::hex::ToHex; use bitcoin_hashes::Hash; use ikc_common::apdu::{ApduCheck, BtcApdu}; -use ikc_common::constants::{ - DUST_THRESHOLD, EACH_ROUND_NUMBER, MAX_OPRETURN_SIZE, MAX_UTXO_NUMBER, TIMEOUT_LONG, -}; +use ikc_common::constants::{MAX_OPRETURN_SIZE, MAX_UTXO_NUMBER, MIN_NONDUST_OUTPUT, TIMEOUT_LONG}; use ikc_common::error::CoinError; -use ikc_common::path::check_path_validity; +use ikc_common::path::{check_path_validity, get_account_path}; use ikc_common::utility::{bigint_to_byte_vec, hex_to_bytes, secp256k1_sign}; use ikc_device::device_binding::KEY_MANAGER; use ikc_transport::message::{send_apdu, send_apdu_timeout}; use secp256k1::ecdsa::Signature; +use std::borrow::Borrow; use std::str::FromStr; +use secp256k1::PublicKey; #[derive(Clone)] pub struct Utxo { pub txhash: String, - pub vout: i32, - pub amount: i64, + pub vout: u32, + pub amount: u64, pub address: Address, pub script_pubkey: String, pub derive_path: String, @@ -36,245 +39,472 @@ pub struct Utxo { pub struct BtcTransaction { pub to: Address, - pub amount: i64, + pub amount: u64, pub unspents: Vec, - pub fee: i64, + pub fee: u64, } impl BtcTransaction { - pub fn sign_transaction( + pub fn sign_Transaction( &self, network: Network, path: &str, - change_idx: i32, - extra_data: &Vec, + change_idx: Option, + extra_data: Option<&str>, + seg_wit: &str, ) -> Result { //path check check_path_validity(path)?; - let mut path_str = path.to_string(); - if !path.ends_with("/") { - path_str = format!("{}{}", path_str, "/"); - } + //check uxto number if &self.unspents.len() > &MAX_UTXO_NUMBER { return Err(CoinError::ImkeyExceededMaxUtxoNumber.into()); } - //utxo address verify - let utxo_pub_key_vec = address_verify(&self.unspents, network, TransTypeFlg::BTC)?; - //calc utxo total amount if self.get_total_amount() < self.amount { return Err(CoinError::ImkeyInsufficientFunds.into()); } - //add send to output - let mut txouts: Vec = vec![]; - txouts.push(self.build_send_to_output()); - - //add change output - if self.get_change_amount() > DUST_THRESHOLD { - let path_temp = format!("{}{}{}", path_str, "1/", change_idx); - let address_str = BtcAddress::get_address(network, path_temp.as_str())?; - let address_obj = Address::from_str(address_str.as_str())?; - txouts.push(TxOut { - value: self.get_change_amount() as u64, - script_pubkey: address_obj.script_pubkey(), - }); - } + //utxo address verify + let utxo_pub_key_vec = get_utxo_pub_key(&self.unspents)?; - //add the op_return - if !extra_data.is_empty() { - if extra_data.len() > MAX_OPRETURN_SIZE { - return Err(CoinError::ImkeySdkIllegalArgument.into()); - } - txouts.push(self.build_op_return_output(&extra_data)) - } + let output = self.tx_output(change_idx, &path, network, seg_wit, extra_data)?; - //output data serialize let mut tx_to_sign = Transaction { version: 1i32, lock_time: PackedLockTime::ZERO, input: vec![], - output: txouts, + output, }; - let mut output_serialize_data = serialize(&tx_to_sign); - - output_serialize_data.remove(5); - output_serialize_data.remove(5); - //add sign type - let mut encoder_hash = Vec::new(); - let len = EcdsaSighashType::All - .to_u32() - .consensus_encode(&mut encoder_hash) - .unwrap(); - debug_assert_eq!(len, encoder_hash.len()); - output_serialize_data.extend(encoder_hash); - //set input number - output_serialize_data.remove(4); - output_serialize_data.insert(4, self.unspents.len() as u8); - - //add fee amount - output_serialize_data.extend(bigint_to_byte_vec(self.fee)); - - //add address version - let address_version = get_address_version(network, self.to.to_string().as_str())?; - output_serialize_data.push(address_version); - - //set 01 tag and length - output_serialize_data.insert(0, output_serialize_data.len() as u8); - output_serialize_data.insert(0, 0x01); + self.calc_tx_hash(&mut tx_to_sign)?; + + self.tx_preview(&tx_to_sign, network)?; + + let input_with_sigs: Vec = vec![]; + for (idx, utxo) in self.unspents.iter().enumerate() { + let script = Script::from_str(&utxo.script_pubkey)?; + if script.is_p2pkh() { + self.sign_p2pkh_input(idx, &utxo_pub_key_vec[idx], &mut tx_to_sign)?; + } else if script.is_p2sh() { + self.sign_p2sh_nested_p2wpkh_input(idx, &utxo_pub_key_vec[idx], &mut tx_to_sign)?; + } else if script.is_v0_p2wpkh() { + self.sign_p2wpkh_input(idx, &utxo_pub_key_vec[idx], &mut tx_to_sign)?; + } else if script.is_v1_p2tr() { + self.sign_p2tr_input( + idx, + &utxo_pub_key_vec[idx], + &mut tx_to_sign, + SchnorrSighashType::Default, + )?; + } else { + return Err(CoinError::InvalidUtxo.into()); + }; + } - //use local private key sign data - let key_manager_obj = KEY_MANAGER.lock(); - let mut output_pareper_data = - secp256k1_sign(&key_manager_obj.pri_key, &output_serialize_data)?; - output_pareper_data.insert(0, output_pareper_data.len() as u8); - output_pareper_data.insert(0, 0x00); - output_pareper_data.extend(output_serialize_data.iter()); + let tx_bytes = serialize(&tx_to_sign); - let btc_prepare_apdu_vec = BtcApdu::btc_prepare(0x41, 0x00, &output_pareper_data); - for temp_str in btc_prepare_apdu_vec { - ApduCheck::check_response(&send_apdu_timeout(temp_str, TIMEOUT_LONG)?)?; - } + Ok(TxSignResult { + signature: tx_bytes.to_hex(), + tx_hash: tx_to_sign.txid().to_hex(), + wtx_id: tx_to_sign.wtxid().to_hex(), + }) + } - let mut lock_script_ver: Vec