From 43b10474424c78696e23374849be897fcaa7f362 Mon Sep 17 00:00:00 2001 From: Sun Feng Date: Sun, 23 Jun 2024 17:11:08 +0800 Subject: [PATCH] add bip322 message signature --- token-core/tcx-btc-kin/src/lib.rs | 36 +++- token-core/tcx-btc-kin/src/message.rs | 235 ++++++++++++++++++++++ token-core/tcx-btc-kin/src/psbt.rs | 67 ++++-- token-core/tcx-btc-kin/src/signer.rs | 35 +--- token-core/tcx-btc-kin/src/transaction.rs | 12 ++ token-core/tcx-proto/src/btc_kin.proto | 8 + 6 files changed, 345 insertions(+), 48 deletions(-) create mode 100644 token-core/tcx-btc-kin/src/message.rs diff --git a/token-core/tcx-btc-kin/src/lib.rs b/token-core/tcx-btc-kin/src/lib.rs index d408e432..7eb15e99 100644 --- a/token-core/tcx-btc-kin/src/lib.rs +++ b/token-core/tcx-btc-kin/src/lib.rs @@ -9,6 +9,7 @@ pub mod network; pub mod signer; pub mod transaction; +mod message; mod psbt; use core::result; @@ -56,6 +57,9 @@ pub enum Error { ConstructBchAddressFailed(String), #[error("unsupported_taproot")] UnsupportedTaproot, + + #[error("missing_signature")] + MissingSignature, } pub mod bitcoin { @@ -64,6 +68,10 @@ pub mod bitcoin { pub type Address = crate::BtcKinAddress; pub type TransactionInput = crate::transaction::BtcKinTxInput; pub type TransactionOutput = crate::transaction::BtcKinTxOutput; + + pub type MessageInput = crate::transaction::BtcMessageInput; + + pub type MessageOutput = crate::transaction::BtcMessageOutput; } pub mod bitcoincash { @@ -93,7 +101,7 @@ pub mod omni { #[cfg(test)] mod tests { use tcx_common::ToHex; - use tcx_constants::{CurveType, TEST_MNEMONIC, TEST_PASSWORD}; + use tcx_constants::{CurveType, TEST_MNEMONIC, TEST_PASSWORD, TEST_WIF}; use tcx_keystore::{Keystore, Metadata}; use tcx_primitive::{PrivateKey, Secp256k1PrivateKey}; @@ -121,4 +129,30 @@ mod tests { pub fn sample_hd_keystore() -> Keystore { hd_keystore(TEST_MNEMONIC) } + + pub fn hex_keystore(hex: &str) -> Keystore { + let mut keystore = Keystore::from_private_key( + hex, + TEST_PASSWORD, + CurveType::SECP256k1, + Metadata::default(), + None, + ) + .unwrap(); + keystore.unlock_by_password(TEST_PASSWORD).unwrap(); + keystore + } + + pub fn wif_keystore(wif: &str) -> Keystore { + let hex = Secp256k1PrivateKey::from_wif(wif) + .unwrap() + .to_bytes() + .to_hex(); + + hex_keystore(&hex) + } + + pub fn sample_wif_keystore() -> Keystore { + wif_keystore(TEST_WIF) + } } diff --git a/token-core/tcx-btc-kin/src/message.rs b/token-core/tcx-btc-kin/src/message.rs new file mode 100644 index 00000000..800241b4 --- /dev/null +++ b/token-core/tcx-btc-kin/src/message.rs @@ -0,0 +1,235 @@ +use crate::psbt::PsbtSigner; +use crate::transaction::{BtcMessageInput, BtcMessageOutput}; +use crate::{BtcKinAddress, Error, Result}; +use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::{ + OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Txid, Witness, +}; +use tcx_common::{sha256, utf8_or_hex_to_bytes, FromHex, ToHex}; +use tcx_constants::{CoinInfo, CurveType}; +use tcx_keystore::{Address, Keystore, MessageSigner, SignatureParameters}; + +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(&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(&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 +} + +impl MessageSigner for Keystore { + fn sign_message( + &mut self, + params: &SignatureParameters, + message_input: &BtcMessageInput, + ) -> tcx_keystore::Result { + let data = utf8_or_hex_to_bytes(&message_input.message)?; + let path = format!("{}/0/0", params.derivation_path); + + let public_key = self.get_public_key(CurveType::SECP256k1, &path)?; + let coin_info = CoinInfo { + coin: params.chain_type.to_string(), + derivation_path: path.clone(), + curve: CurveType::SECP256k1, + network: params.network.to_string(), + seg_wit: params.seg_wit.to_string(), + }; + + let address = BtcKinAddress::from_public_key(&public_key, &coin_info)?; + + let tx_id = get_spend_tx_id(&data, address.script_pubkey())?; + let mut psbt = create_to_sign_empty(tx_id, address.script_pubkey())?; + let mut psbt_signer = PsbtSigner::new( + &mut psbt, + self, + ¶ms.chain_type, + ¶ms.derivation_path, + true, + ); + + psbt_signer.sign()?; + + if let Some(witness) = &psbt.inputs[0].final_script_witness { + Ok(BtcMessageOutput { + signature: witness_to_vec(witness.to_vec()).to_hex(), + }) + } else { + Err(Error::MissingSignature.into()) + } + } +} + +#[cfg(test)] +mod tests { + use crate::tests::{sample_hd_keystore, wif_keystore}; + use crate::BtcKinAddress; + use tcx_constants::{CoinInfo, CurveType}; + use tcx_keystore::{Address, MessageSigner}; + + #[test] + fn test_to_spend_tx_id() { + let message = "hello world"; + let mut ks = sample_hd_keystore(); + let coin_info = CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/44'/0'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }; + + let account = ks.derive_coin::(&coin_info).unwrap(); + let address = BtcKinAddress::from_public_key(&account.public_key, &coin_info).unwrap(); + + assert_eq!( + super::get_spend_tx_id(message.as_bytes(), address.script_pubkey()) + .unwrap() + .to_string(), + "24bca2df5140bcf6a6aeafd141ad40b0595aa6998ca0fc733488d7131ca7763f" + ); + } + + #[test] + fn test_bip322_segwit() { + let message = "hello world"; + let mut ks = sample_hd_keystore(); + let coin_info = CoinInfo { + coin: "BITCOIN".to_string(), + derivation_path: "m/44'/0'/0'/0/0".to_string(), + curve: CurveType::SECP256k1, + network: "MAINNET".to_string(), + seg_wit: "VERSION_0".to_string(), + }; + + let account = ks.derive_coin::(&coin_info).unwrap(); + let address = BtcKinAddress::from_public_key(&account.public_key, &coin_info).unwrap(); + + let params = tcx_keystore::SignatureParameters { + curve: CurveType::SECP256k1, + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "VERSION_0".to_string(), + derivation_path: "m/44'/0'/0'".to_string(), + }; + + let output = ks + .sign_message( + ¶ms, + &super::BtcMessageInput { + message: message.to_string(), + }, + ) + .unwrap(); + + assert_eq!(output.signature, "024830450221009f003820d1db93bf78be08dafdd05b7dde7c31a73c9be36b705a15329bd3d0e502203eb6f1a34466995e4b9c281bf4a093a1f55a21b2ef961438c9ae284efab27dda0121026b5b6a9d041bc5187e0b34f9e496436c7bff261c6c1b5f3c06b433c61394b868"); + } + + #[test] + fn test_bip322_taproot() { + let message = "Sign this message to log in to https://www.subber.xyz // 200323342"; + let mut ks = wif_keystore("L4F5BYm82Bck6VEY64EbqQkoBXqkegq9X9yc6iLTV3cyJoqUasnY"); + let coin_info = 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(), + }; + + let account = ks.derive_coin::(&coin_info).unwrap(); + let address = BtcKinAddress::from_public_key(&account.public_key, &coin_info).unwrap(); + + let params = tcx_keystore::SignatureParameters { + curve: CurveType::SECP256k1, + chain_type: "BITCOIN".to_string(), + network: "MAINNET".to_string(), + seg_wit: "VERSION_1".to_string(), + derivation_path: "m/86'/0'/0'".to_string(), + }; + + let output = ks + .sign_message( + ¶ms, + &super::BtcMessageInput { + message: message.to_string(), + }, + ) + .unwrap(); + + // assert_eq!(output.signature, "0140717dbc46e9d816d7c9e26b5a5f6153c1fceb734489afaaee4ed80bc7c119a39af44de7f6d66c30e530c7c696a25d45bab052cc55012fc57ef6cb24313b31014b"); + } +} diff --git a/token-core/tcx-btc-kin/src/psbt.rs b/token-core/tcx-btc-kin/src/psbt.rs index 54549205..03d9f4a1 100644 --- a/token-core/tcx-btc-kin/src/psbt.rs +++ b/token-core/tcx-btc-kin/src/psbt.rs @@ -5,6 +5,7 @@ use crate::{Error, Result, BITCOINCASH}; use bitcoin::consensus::{Decodable, Encodable}; use bitcoin::psbt::{Prevouts, Psbt}; use bitcoin::schnorr::TapTweak; +use bitcoin::util::bip32::KeySource; use bitcoin::util::sighash::SighashCache; use bitcoin::util::taproot::TapLeafHash; use bitcoin::{ @@ -24,6 +25,7 @@ pub struct PsbtSigner<'a> { psbt: &'a mut Psbt, keystore: &'a mut Keystore, derivation_path: String, + auto_finalize: bool, prevouts: Vec, sighash_cache: Box, @@ -74,8 +76,19 @@ impl PsbtInputExtra for bitcoin::psbt::Input { witness.push(script.as_bytes().to_vec()); witness.push(control_block.serialize()) } + self.final_script_witness = Some(witness); } + } else { + if !self.partial_sigs.is_empty() { + let sig = self.partial_sigs.first_key_value().unwrap(); + let mut witness = Witness::new(); + + witness.push(sig.1.to_vec()); + witness.push(sig.0.to_bytes()); + + self.final_script_witness = Some(witness) + } } self.clear_finalized_input(); @@ -88,6 +101,7 @@ impl<'a> PsbtSigner<'a> { keystore: &'a mut Keystore, chain_type: &str, derivation_path: &str, + auto_finalize: bool, ) -> Self { let unsigned_tx = psbt.unsigned_tx.clone(); @@ -101,6 +115,7 @@ impl<'a> PsbtSigner<'a> { psbt, keystore, derivation_path: derivation_path.to_string(), + auto_finalize, sighash_cache, prevouts: Vec::new(), @@ -137,13 +152,28 @@ impl<'a> PsbtSigner<'a> { Ok(utxos) } - fn get_private_key(&mut self) -> Result { - let path = if !self.derivation_path.is_empty() { - self.derivation_path.clone() + "/0/0" + fn get_private_key(&mut self, index: usize) -> 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 input.is_taproot() { + 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(self .keystore .get_private_key(CurveType::SECP256k1, &path)? @@ -152,7 +182,7 @@ impl<'a> PsbtSigner<'a> { } fn sign_p2pkh(&mut self, index: usize) -> Result<()> { - let key = self.get_private_key()?; + let key = self.get_private_key(index)?; let prevout = &self.prevouts[index]; @@ -173,7 +203,7 @@ impl<'a> PsbtSigner<'a> { fn sign_p2sh_nested_p2wpkh(&mut self, index: usize) -> Result<()> { let prevout = &self.prevouts[index].clone(); - let key = self.get_private_key()?; + let key = self.get_private_key(index)?; let pub_key = key.public_key(); let script = Script::new_v0_p2wpkh(&WPubkeyHash::from_hash( @@ -196,7 +226,7 @@ impl<'a> PsbtSigner<'a> { } fn sign_p2wpkh(&mut self, index: usize) -> Result<()> { - let key = self.get_private_key()?; + let key = self.get_private_key(index)?; let prevout = &self.prevouts[index]; let hash = self.sighash_cache.segwit_hash( @@ -218,7 +248,7 @@ impl<'a> PsbtSigner<'a> { } fn sign_p2tr(&mut self, index: usize) -> Result<()> { - let key = self.get_private_key()?; + let key = self.get_private_key(index)?; let key_pair = bitcoin::schnorr::UntweakedKeyPair::from_seckey_slice( &SECP256K1_ENGINE, @@ -246,7 +276,7 @@ impl<'a> PsbtSigner<'a> { } fn sign_p2tr_script(&mut self, index: usize) -> Result<()> { - let key = self.get_private_key()?; + let key = self.get_private_key(index)?; let key_pair = bitcoin::schnorr::UntweakedKeyPair::from_seckey_slice( &SECP256K1_ENGINE, @@ -274,7 +304,7 @@ impl<'a> PsbtSigner<'a> { Ok(()) } - fn sign(&mut self) -> Result<()> { + pub fn sign(&mut self) -> Result<()> { self.prevouts = self.prevouts()?; for idx in 0..self.prevouts.len() { @@ -291,6 +321,10 @@ impl<'a> PsbtSigner<'a> { } else if prevout.script_pubkey.is_v1_p2tr() { self.sign_p2tr(idx)?; } + + if self.auto_finalize { + self.psbt.inputs[idx].finalize(); + } } Ok(()) @@ -306,16 +340,15 @@ pub fn sign_psbt( let mut reader = Cursor::new(Vec::::from_hex(psbt_input.data)?); let mut psbt = Psbt::consensus_decode(&mut reader)?; - let mut signer = PsbtSigner::new(&mut psbt, keystore, chain_type, derivation_path); + let mut signer = PsbtSigner::new( + &mut psbt, + keystore, + chain_type, + derivation_path, + psbt_input.auto_finalize, + ); signer.sign()?; - // FINALIZER - if psbt_input.auto_finalize { - psbt.inputs.iter_mut().for_each(|input| { - input.finalize(); - }) - } - let mut vec = Vec::::new(); let mut writer = Cursor::new(&mut vec); psbt.consensus_encode(&mut writer)?; diff --git a/token-core/tcx-btc-kin/src/signer.rs b/token-core/tcx-btc-kin/src/signer.rs index 1248ebac..dcad738e 100644 --- a/token-core/tcx-btc-kin/src/signer.rs +++ b/token-core/tcx-btc-kin/src/signer.rs @@ -447,38 +447,13 @@ mod tests { use super::*; - fn hex_keystore(hex: &str) -> Keystore { - let mut keystore = Keystore::from_private_key( - hex, - TEST_PASSWORD, - CurveType::SECP256k1, - Metadata::default(), - None, - ) - .unwrap(); - keystore.unlock_by_password(TEST_PASSWORD).unwrap(); - keystore - } - - fn wif_keystore(_wif: &str) -> Keystore { - let hex = Secp256k1PrivateKey::from_wif(TEST_WIF) - .unwrap() - .to_bytes() - .to_hex(); - - hex_keystore(&hex) - } - - fn sample_private_key_keystore() -> Keystore { - wif_keystore(TEST_WIF) - } - mod kin { use super::*; + use crate::tests::sample_wif_keystore; #[test] fn test_sign_less_than_dust() { - let mut ks = sample_private_key_keystore(); + let mut ks = sample_wif_keystore(); let inputs = vec![Utxo { tx_hash: "e112b1215813c8888b31a80d215169809f7901359c0f4bf7e7374174ab2a64f4" @@ -636,7 +611,7 @@ mod tests { mod btc { use super::*; - use crate::tests::sample_hd_keystore; + use crate::tests::{sample_hd_keystore, sample_wif_keystore}; use bitcoin::psbt::serialize::Deserialize; use secp256k1::schnorr::Signature; use secp256k1::XOnlyPublicKey; @@ -840,7 +815,7 @@ mod tests { #[test] fn test_sign_with_private_key_on_testnet() { - let mut ks = sample_private_key_keystore(); + let mut ks = sample_wif_keystore(); let inputs = vec![Utxo { tx_hash: "e112b1215813c8888b31a80d215169809f7901359c0f4bf7e7374174ab2a64f4" @@ -1145,7 +1120,7 @@ mod tests { mod ltc { use super::*; - use crate::tests::sample_hd_keystore; + use crate::tests::{hex_keystore, sample_hd_keystore, wif_keystore}; #[test] fn test_sign_with_hd_on_testnet() { diff --git a/token-core/tcx-btc-kin/src/transaction.rs b/token-core/tcx-btc-kin/src/transaction.rs index 9bc58e74..3efaa5d7 100644 --- a/token-core/tcx-btc-kin/src/transaction.rs +++ b/token-core/tcx-btc-kin/src/transaction.rs @@ -67,3 +67,15 @@ pub struct PsbtOutput { #[prost(string, tag = "1")] pub data: ::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/token-core/tcx-proto/src/btc_kin.proto b/token-core/tcx-proto/src/btc_kin.proto index 7dfec591..9238fb3f 100644 --- a/token-core/tcx-proto/src/btc_kin.proto +++ b/token-core/tcx-proto/src/btc_kin.proto @@ -46,3 +46,11 @@ message PsbtInput { message PsbtOutput { string data = 1; } + +message BtcMessageInput { + string message = 1; +} + +message BtcMessageOutput { + string signature = 1; +}