diff --git a/Cargo.lock b/Cargo.lock index 5575fb73..1a355dd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,7 @@ dependencies = [ "bech32 0.9.1", "bitcoin_hashes", "secp256k1", + "serde", ] [[package]] @@ -229,6 +230,9 @@ name = "bitcoin_hashes" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" +dependencies = [ + "serde", +] [[package]] name = "bitflags" @@ -3509,6 +3513,7 @@ dependencies = [ "bitcoin_hashes", "rand 0.8.5", "secp256k1-sys", + "serde", ] [[package]] @@ -3567,17 +3572,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-aux" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" -dependencies = [ - "chrono", - "serde", - "serde_json", -] - [[package]] name = "serde_derive" version = "1.0.147" @@ -4606,7 +4600,6 @@ dependencies = [ "prost", "prost-types", "serde", - "serde-aux", "serde_json", "tcx-atom", "tcx-btc-kin", diff --git a/VERSION b/VERSION index 2c9b4ef4..a4dd9dba 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.3 +2.7.4 diff --git a/token-core/tcx-btc-kin/Cargo.toml b/token-core/tcx-btc-kin/Cargo.toml index 9fe6f3a3..7eede65b 100644 --- a/token-core/tcx-btc-kin/Cargo.toml +++ b/token-core/tcx-btc-kin/Cargo.toml @@ -14,7 +14,7 @@ tcx-common = { path = "../tcx-common" } hex = "=0.4.3" base64 = "=0.13.1" -bitcoin = "=0.29.2" +bitcoin = {version = "=0.29.2", features = ["serde", "std", "secp-recovery"] } secp256k1 = {version ="=0.24.3", features = ["rand", "recovery", "rand-std"] } tiny-bip39 = "=1.0.0" bitcoin_hashes = "=0.11.0" diff --git a/token-core/tcx-btc-kin/src/bch_sighash.rs b/token-core/tcx-btc-kin/src/bch_sighash.rs index fc101d35..8961ffe9 100644 --- a/token-core/tcx-btc-kin/src/bch_sighash.rs +++ b/token-core/tcx-btc-kin/src/bch_sighash.rs @@ -181,4 +181,14 @@ impl TxSignatureHasher for BitcoinCashSighash { ) -> Result { Err(Error::UnsupportedTaproot.into()) } + + fn taproot_script_spend_signature_hash( + &mut self, + input_index: usize, + prevouts: &Prevouts, + tap_leaf_hash: TapLeafHash, + sighash_type: SchnorrSighashType, + ) -> Result { + Err(Error::UnsupportedTaproot.into()) + } } diff --git a/token-core/tcx-btc-kin/src/lib.rs b/token-core/tcx-btc-kin/src/lib.rs index 3c1a0151..f3fe6d15 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; @@ -20,7 +21,6 @@ extern crate num_traits; #[macro_use] extern crate tcx_keystore; -extern crate core; pub type Result = result::Result; @@ -57,6 +57,9 @@ pub enum Error { ConstructBchAddressFailed(String), #[error("unsupported_taproot")] UnsupportedTaproot, + + #[error("missing_signature")] + MissingSignature, } pub mod bitcoin { @@ -65,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 { @@ -77,6 +84,10 @@ pub mod bitcoincash { 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 omni { @@ -93,8 +104,10 @@ pub mod omni { #[cfg(test)] mod tests { - use tcx_constants::{TEST_MNEMONIC, TEST_PASSWORD}; + use tcx_common::ToHex; + use tcx_constants::{CurveType, TEST_MNEMONIC, TEST_PASSWORD, TEST_WIF}; use tcx_keystore::{Keystore, Metadata}; + use tcx_primitive::{PrivateKey, Secp256k1PrivateKey}; pub fn hd_keystore(mnemonic: &str) -> Keystore { let mut keystore = @@ -103,7 +116,47 @@ mod tests { keystore } + pub fn private_keystore(wif: &str) -> Keystore { + let sec_key = Secp256k1PrivateKey::from_wif(wif).unwrap(); + let mut keystore = Keystore::from_private_key( + &sec_key.to_bytes().to_hex(), + TEST_PASSWORD, + CurveType::SECP256k1, + Metadata::default(), + None, + ) + .unwrap(); + keystore.unlock_by_password(TEST_PASSWORD).unwrap(); + keystore + } + 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 84c32bc6..08ab127b 100644 --- a/token-core/tcx-btc-kin/src/psbt.rs +++ b/token-core/tcx-btc-kin/src/psbt.rs @@ -2,10 +2,12 @@ use crate::bch_sighash::BitcoinCashSighash; use crate::sighash::TxSignatureHasher; use crate::transaction::{PsbtInput, PsbtOutput}; use crate::{Error, Result, BITCOINCASH}; +use bitcoin::blockdata::script::Builder; use bitcoin::consensus::{Decodable, Encodable}; use bitcoin::psbt::{Prevouts, Psbt}; use bitcoin::schnorr::TapTweak; use bitcoin::util::sighash::SighashCache; +use bitcoin::util::taproot::TapLeafHash; use bitcoin::{ EcdsaSig, EcdsaSighashType, SchnorrSig, SchnorrSighashType, Script, TxOut, WPubkeyHash, Witness, }; @@ -23,62 +25,19 @@ pub struct PsbtSigner<'a> { psbt: &'a mut Psbt, keystore: &'a mut Keystore, derivation_path: String, + auto_finalize: bool, prevouts: Vec, sighash_cache: Box, } -pub trait PsbtInputExtra { - fn is_taproot(&self) -> bool; - - fn clear_finalized_input(&mut self); - - fn finalize(&mut self); -} - -impl PsbtInputExtra for bitcoin::psbt::Input { - fn is_taproot(&self) -> bool { - return self.tap_internal_key.is_some() - || !self.tap_key_origins.is_empty() - || self.tap_merkle_root.is_some() - || self.tap_key_sig.is_some() - || !self.tap_script_sigs.is_empty(); - } - - fn clear_finalized_input(&mut self) { - self.tap_key_sig = None; - self.tap_scripts = BTreeMap::new(); - self.tap_internal_key = None; - self.tap_merkle_root = None; - self.tap_script_sigs = BTreeMap::new(); - - self.partial_sigs = BTreeMap::new(); - self.sighash_type = None; - self.redeem_script = None; - self.witness_script = None; - self.bip32_derivation = BTreeMap::new(); - self.unknown = BTreeMap::new(); - } - - fn finalize(&mut self) { - if self.is_taproot() { - if self.tap_key_sig.is_some() { - let mut witness = Witness::new(); - witness.push(self.tap_key_sig.unwrap().to_vec()); - self.final_script_witness = Some(witness); - } - } - - self.clear_finalized_input(); - } -} - impl<'a> PsbtSigner<'a> { pub fn new( psbt: &'a mut Psbt, keystore: &'a mut Keystore, chain_type: &str, derivation_path: &str, + auto_finalize: bool, ) -> Self { let unsigned_tx = psbt.unsigned_tx.clone(); @@ -92,14 +51,15 @@ impl<'a> PsbtSigner<'a> { psbt, keystore, derivation_path: derivation_path.to_string(), + auto_finalize, sighash_cache, prevouts: Vec::new(), } } - fn hash160(&self, input: &[u8]) -> hash160::Hash { - hash160::Hash::hash(input) + fn hash160(input: &[u8]) -> hash160::Hash { + Hash::hash(input) } fn sign_ecdsa(data: &[u8], key: &Secp256k1PrivateKey) -> Result { @@ -128,13 +88,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, 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(self .keystore .get_private_key(CurveType::SECP256k1, &path)? @@ -142,8 +117,91 @@ impl<'a> PsbtSigner<'a> { .clone()) } + 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_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_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 sign_p2pkh(&mut self, index: usize) -> Result<()> { - let key = self.get_private_key()?; + let key = self.get_private_key(index, false)?; let prevout = &self.prevouts[index]; @@ -164,12 +222,12 @@ 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, false)?; let pub_key = key.public_key(); - let script = Script::new_v0_p2wpkh(&WPubkeyHash::from_hash( - self.hash160(&pub_key.to_compressed()), - )); + let script = Script::new_v0_p2wpkh(&WPubkeyHash::from_hash(Self::hash160( + &pub_key.to_compressed(), + ))); let hash = self.sighash_cache.segwit_hash( index, @@ -187,7 +245,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, false)?; let prevout = &self.prevouts[index]; let hash = self.sighash_cache.segwit_hash( @@ -209,7 +267,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, true)?; let key_pair = bitcoin::schnorr::UntweakedKeyPair::from_seckey_slice( &SECP256K1_ENGINE, @@ -236,7 +294,36 @@ impl<'a> PsbtSigner<'a> { Ok(()) } - fn sign(&mut self) -> Result<()> { + fn sign_p2tr_script(&mut self, index: usize) -> Result<()> { + let key = self.get_private_key(index, true)?; + + let key_pair = bitcoin::schnorr::UntweakedKeyPair::from_seckey_slice( + &SECP256K1_ENGINE, + &key.to_bytes(), + )?; + + let input = self.psbt.inputs[index].clone(); + let (_, script_leaf) = input.tap_scripts.first_key_value().unwrap(); + + let (script, leaf_version) = script_leaf; + let hash = self.sighash_cache.taproot_script_spend_signature_hash( + index, + &Prevouts::All(&self.prevouts.clone()), + TapLeafHash::from_script(script, leaf_version.clone()), + SchnorrSighashType::Default, + )?; + + let msg = Message::from_slice(&hash[..])?; + let sig = SECP256K1_ENGINE.sign_schnorr(&msg, &key_pair); + self.psbt.inputs[index].tap_key_sig = Some(SchnorrSig { + hash_ty: SchnorrSighashType::Default, + sig, + }); + + Ok(()) + } + + pub fn sign(&mut self) -> Result<()> { self.prevouts = self.prevouts()?; for idx in 0..self.prevouts.len() { @@ -244,12 +331,38 @@ impl<'a> PsbtSigner<'a> { if prevout.script_pubkey.is_p2pkh() { self.sign_p2pkh(idx)?; + + if self.auto_finalize { + self.finalize_p2pkh(idx); + } } else if prevout.script_pubkey.is_p2sh() { self.sign_p2sh_nested_p2wpkh(idx)?; + + if self.auto_finalize { + self.finalize_p2sh_nested_p2wpkh(idx); + } } else if prevout.script_pubkey.is_v0_p2wpkh() { self.sign_p2wpkh(idx)?; + + if self.auto_finalize { + self.finalize_p2wpkh(idx); + } + } else if !self.psbt.inputs.first().unwrap().tap_scripts.is_empty() { + self.sign_p2tr_script(idx)?; + + if self.auto_finalize { + self.finalize_p2tr(idx); + } } else if prevout.script_pubkey.is_v1_p2tr() { self.sign_p2tr(idx)?; + + if self.auto_finalize { + self.finalize_p2tr(idx); + } + } + + if self.auto_finalize { + self.clear_finalized_input(idx); } } @@ -266,18 +379,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(); - }) - } - - println!("psbt: {:?}", psbt.inputs); - let mut vec = Vec::::new(); let mut writer = Cursor::new(&mut vec); psbt.consensus_encode(&mut writer)?; @@ -287,13 +397,26 @@ pub fn sign_psbt( #[cfg(test)] mod tests { - use crate::tests::{hd_keystore, sample_hd_keystore}; + use crate::psbt::PsbtSigner; + use crate::tests::sample_hd_keystore; use crate::transaction::PsbtInput; use crate::BtcKinAddress; + use bitcoin::consensus::Decodable; + use bitcoin::psbt::serialize::{Deserialize, Serialize}; + use bitcoin::psbt::Psbt; + use bitcoin::schnorr::TapTweak; + use bitcoin::util::bip32::{DerivationPath, KeySource}; + use bitcoin::{schnorr, Script, Transaction, TxOut, Witness}; + use secp256k1::schnorr::Signature; + use secp256k1::{Message, XOnlyPublicKey}; + use std::io::Cursor; + use std::str::FromStr; + use tcx_common::{FromHex, ToHex}; use tcx_constants::{CoinInfo, CurveType}; + use tcx_primitive::{PublicKey, SECP256K1_ENGINE}; #[test] - fn test_sign_psbt() { + fn test_sign_psbt_no_script() { let mut hd = sample_hd_keystore(); let coin_info = CoinInfo { coin: "BITCOIN".to_string(), @@ -304,14 +427,222 @@ mod tests { }; let account = hd.derive_coin::(&coin_info).unwrap(); - println!("{:?}", account); let psbt_input = PsbtInput { data: "70736274ff0100db0200000001fa4c8d58b9b6c56ed0b03f78115246c99eb70f99b837d7b4162911d1016cda340200000000fdffffff0350c30000000000002251202114eda66db694d87ff15ddd5d3c4e77306b6e6dd5720cbd90cd96e81016c2b30000000000000000496a47626274340066f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229ec20acf33c17e5a6c92cced9f1d530cccab7aa3e53400456202f02fac95e9c481fa00d47b1700000000002251208f4ca6a7384f50a1fe00cba593d5a834b480c65692a76ae6202e1ce46cb1c233d80f03000001012be3bf1d00000000002251208f4ca6a7384f50a1fe00cba593d5a834b480c65692a76ae6202e1ce46cb1c23301172066f873ad53d80688c7739d0d268acd956366275004fdceab9e9fc30034a4229e00000000".to_string(), - auto_finalize: true + auto_finalize: true, + }; + + let psbt_output = super::sign_psbt("BITCOIN", "m/86'/1'/0'", &mut hd, psbt_input).unwrap(); + let mut reader = Cursor::new(Vec::::from_hex(psbt_output.data).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 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() { + let mut hd = sample_hd_keystore(); + let coin_info = 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(), }; - let result = super::sign_psbt("BITCOIN", "m/86'/1'/0'", &mut hd, psbt_input).unwrap(); - assert_eq!(result.data, "70736274ff0100db02000000017e4e5ccaa5a84f4e2761816d948db0530283d2ddab9e2b0bf14432247177b67c0000000000fdffffff0350c30000000000002251202f03f11af54df4be96db1c8d6ee9ab2a29558479ff93ad019d182deed8f8c33d0000000000000000496a4762627434001fa696928d908ffd29c2ab9ebf8ad48946bf9d57b64c2e4f588988c830bd2571f4940b238dcd00535fde9730345bab6ff4ea6d413cc3602c4033c10f251c7e81fa0057620000000000002251206649a3708d5510aeb8140ffb6ed5866db64b817ea62902628ad7d04730484aab080803000001012bf4260100000000002251206649a3708d5510aeb8140ffb6ed5866db64b817ea62902628ad7d04730484aab0117201fa696928d908ffd29c2ab9ebf8ad48946bf9d57b64c2e4f588988c830bd257100000000"); + let account = hd.derive_coin::(&coin_info).unwrap(); + + let psbt_input = PsbtInput { + data: "70736274ff01005e02000000012bd2f6479f3eeaffe95c03b5fdd76a873d346459114dec99c59192a0cb6409e90000000000ffffffff01409c000000000000225120677cc88dc36a75707b370e27efff3e454d446ad55004dac1685c1725ee1a89ea000000000001012b50c3000000000000225120a9a3350206de400f09a73379ec1bcfa161fc11ac095e5f3d7354126f0ec8e87f6215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0d2956573f010fa1a3c135279c5eb465ec2250205dcdfe2122637677f639b1021356c963cd9c458508d6afb09f3fa2f9b48faec88e75698339a4bbb11d3fc9b0efd570120aff94eb65a2fe773a57c5bd54e62d8436a5467573565214028422b41bd43e29bad200aee0509b16db71c999238a4827db945526859b13c95487ab46725357c9a9f25ac20113c3a32a9d320b72190a04a020a0db3976ef36972673258e9a38a364f3dc3b0ba2017921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4ba203bb93dfc8b61887d771f3630e9a63e97cbafcfcc78556a474df83a31a0ef899cba2040afaf47c4ffa56de86410d8e47baa2bb6f04b604f4ea24323737ddc3fe092dfba2079a71ffd71c503ef2e2f91bccfc8fcda7946f4653cef0d9f3dde20795ef3b9f0ba20d21faf78c6751a0d38e6bd8028b907ff07e9a869a43fc837d6b3f8dff6119a36ba20f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8eba20fa9d882d45f4060bdb8042183828cd87544f1ea997380e586cab77d5fd698737ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000".to_string(), + auto_finalize: true, + }; + + let psbt_output = super::sign_psbt("BITCOIN", "m/86'/1'/0'", &mut hd, psbt_input).unwrap(); + let mut reader = Cursor::new(Vec::::from_hex(psbt_output.data).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 = witness[1].to_hex(); + let control_block = witness[2].to_hex(); + 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() { + let mut hd = sample_hd_keystore(); + + let raw_tx = "02000000054adc61444e5a4dd7021e52dc6f5adadd9a3286d346f5d9f023ebcde2af80a0ae0000000000ffffffff4adc61444e5a4dd7021e52dc6f5adadd9a3286d346f5d9f023ebcde2af80a0ae0100000000ffffffff12cc8049bf85b5e18cb2be8aa7aefc3afb8df4ec5c1f766750014cc95ca2dc130000000000ffffffff729e6570928cc65200f1d53def65a7934d2e9b543059d90598ed1d166af422010100000000ffffffffa126724475cd2f3252352b3543c8455c7999a8283883bd7a712a7d66609d92d80100000000ffffffff02409c00000000000022512036079c540758a51a86eeaf9e17668d4d8543d8b1b7e56fe2da0982c390c5655ef8fa0700000000002251209303a116174dd21ea473766659568ac24eb6b828c3ee998982d2ba070ea0615500000000"; + let 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: BtcKinAddress::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: BtcKinAddress::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: BtcKinAddress::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: BtcKinAddress::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: BtcKinAddress::from_str("mkeNU5nVnozJiaACDELLCsVUc8Wxoh1rQN") + .unwrap() + .script_pubkey(), + }); + + let mut signer = PsbtSigner::new(&mut psbt, &mut hd, "BITCOIN", "", true); + signer.sign().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/token-core/tcx-btc-kin/src/sighash.rs b/token-core/tcx-btc-kin/src/sighash.rs index 4fc9a120..88fd173a 100644 --- a/token-core/tcx-btc-kin/src/sighash.rs +++ b/token-core/tcx-btc-kin/src/sighash.rs @@ -30,6 +30,14 @@ pub trait TxSignatureHasher { leaf_hash_code_separator: Option<(TapLeafHash, u32)>, sighash_type: SchnorrSighashType, ) -> Result; + + fn taproot_script_spend_signature_hash( + &mut self, + input_index: usize, + prevouts: &Prevouts, + tap_leaf_hash: TapLeafHash, + sighash_type: SchnorrSighashType, + ) -> Result; } impl TxSignatureHasher for SighashCache> { @@ -73,4 +81,19 @@ impl TxSignatureHasher for SighashCache> { sighash_type, )?) } + + fn taproot_script_spend_signature_hash( + &mut self, + input_index: usize, + prevouts: &Prevouts, + tap_leaf_hash: TapLeafHash, + sighash_type: SchnorrSighashType, + ) -> Result { + Ok(self.taproot_script_spend_signature_hash( + input_index, + &prevouts, + tap_leaf_hash, + sighash_type, + )?) + } } 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 7837aa5a..3efaa5d7 100644 --- a/token-core/tcx-btc-kin/src/transaction.rs +++ b/token-core/tcx-btc-kin/src/transaction.rs @@ -58,7 +58,7 @@ pub struct OmniTxInput { pub struct PsbtInput { #[prost(string, tag = "1")] pub data: ::prost::alloc::string::String, - #[prost(bool, tag = "3")] + #[prost(bool, tag = "2")] pub auto_finalize: bool, } #[allow(clippy::derive_partial_eq_without_eq)] @@ -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-migration/Cargo.toml b/token-core/tcx-migration/Cargo.toml index cd96e165..4b760f51 100644 --- a/token-core/tcx-migration/Cargo.toml +++ b/token-core/tcx-migration/Cargo.toml @@ -41,4 +41,3 @@ base64 = "=0.13.1" base58 = "=0.2.0" parking_lot = "=0.12.1" uuid = { version = "=1.2.2", features = ["serde", "v4"] } -serde-aux = {version = "=4.5.0"} diff --git a/token-core/tcx-proto/src/btc_kin.proto b/token-core/tcx-proto/src/btc_kin.proto index 9479affa..9238fb3f 100644 --- a/token-core/tcx-proto/src/btc_kin.proto +++ b/token-core/tcx-proto/src/btc_kin.proto @@ -40,9 +40,17 @@ message OmniTxInput { message PsbtInput { string data = 1; - bool auto_finalize = 3; + bool autoFinalize = 2; } message PsbtOutput { string data = 1; } + +message BtcMessageInput { + string message = 1; +} + +message BtcMessageOutput { + string signature = 1; +} diff --git a/token-core/tcx/src/filemanager.rs b/token-core/tcx/src/filemanager.rs index be1fb29e..6ee94195 100644 --- a/token-core/tcx/src/filemanager.rs +++ b/token-core/tcx/src/filemanager.rs @@ -45,3 +45,10 @@ pub fn delete_keystore_file(wid: &str) -> Result<()> { fs::remove_file(path)?; Ok(()) } + +pub fn exist_migrated_file(id: &str) -> bool { + let file_dir = WALLET_FILE_DIR.read(); + let ks_path = format!("{}/{}.json", file_dir, id); + let path = Path::new(&ks_path); + path.exists() +} diff --git a/token-core/tcx/src/handler.rs b/token-core/tcx/src/handler.rs index fe0a28ae..0d93c9eb 100644 --- a/token-core/tcx/src/handler.rs +++ b/token-core/tcx/src/handler.rs @@ -45,8 +45,8 @@ use crate::api::{EthBatchPersonalSignParam, EthBatchPersonalSignResult}; use crate::api::{InitTokenCoreXParam, SignParam}; use crate::error_handling::Result; use crate::filemanager::{ - cache_keystore, clean_keystore, flush_keystore, KEYSTORE_BASE_DIR, LEGACY_WALLET_FILE_DIR, - WALLET_FILE_DIR, WALLET_V1_DIR, WALLET_V2_DIR, + cache_keystore, clean_keystore, exist_migrated_file, flush_keystore, KEYSTORE_BASE_DIR, + LEGACY_WALLET_FILE_DIR, WALLET_FILE_DIR, WALLET_V1_DIR, WALLET_V2_DIR, }; use crate::filemanager::{delete_keystore_file, KEYSTORE_MAP}; @@ -152,7 +152,9 @@ fn import_private_key_internal( let fingerprint = fingerprint_from_any_format_pk(¶m.private_key)?; let map = KEYSTORE_MAP.read(); if let Some(founded) = map.values().find(|keystore| { - keystore.fingerprint() == fingerprint && keystore.id() != overwrite_id.to_string() + keystore.fingerprint() == fingerprint + && keystore.id() != overwrite_id.to_string() + && exist_migrated_file(&keystore.id()) }) { target_id = Some(founded.id()); } @@ -171,16 +173,6 @@ fn import_private_key_internal( } let decoded_ret = decode_private_key(¶m.private_key)?; - if !decoded_ret.network.is_empty() { - let expected_network = if param.network.is_empty() { - "MAINNET" - } else { - param.network.as_str() - }; - if decoded_ret.network != expected_network { - return Err(anyhow!("{}", "private_key_network_mismatch")); - } - } let private_key = decoded_ret.bytes.to_hex(); let meta_source = if let Some(source) = source { @@ -533,7 +525,9 @@ pub fn import_mnemonic(data: &[u8]) -> Result> { let fingerprint = fingerprint_from_mnemonic(¶m.mnemonic)?; let map = KEYSTORE_MAP.read(); if let Some(founded) = map.values().find(|keystore| { - keystore.fingerprint() == fingerprint && keystore.id() != param.overwrite_id.to_string() + keystore.fingerprint() == fingerprint + && keystore.id() != param.overwrite_id.to_string() + && exist_migrated_file(&keystore.id()) }) { target_id = Some(founded.id()); } diff --git a/token-core/tcx/tests/common/mod.rs b/token-core/tcx/tests/common/mod.rs index cea1e76d..30dc345d 100644 --- a/token-core/tcx/tests/common/mod.rs +++ b/token-core/tcx/tests/common/mod.rs @@ -175,3 +175,29 @@ pub fn remove_created_wallet(wid: &str) { let p = Path::new(&full_file_path); remove_file(p).expect("should remove file"); } + +fn copy_dir(src: &Path, dst: &Path) -> tcx::Result<()> { + if src.is_dir() { + fs::create_dir_all(dst)?; // Create destination directory if it doesn't exist + for entry in src.read_dir()? { + let entry = entry?; + let path = entry.path(); + let new_dest = dst.join(path.strip_prefix(src)?); + if path.is_dir() { + copy_dir(&path, &new_dest)?; // Recursively copy subdirectories + } else { + fs::copy(&path, &new_dest)?; // Copy files + } + } + } else { + return Err(anyhow!("source is not a directory")); + } + Ok(()) +} + +pub fn setup_test(old_wallet_dir: &str) { + let _ = fs::remove_dir_all("/tmp/token-core-x"); + copy_dir(&Path::new(old_wallet_dir), &Path::new("/tmp/token-core-x")).unwrap(); + + init_token_core_x("/tmp/token-core-x"); +} diff --git a/token-core/tcx/tests/derive_test.rs b/token-core/tcx/tests/derive_test.rs index e9c999b7..e29cd1e7 100644 --- a/token-core/tcx/tests/derive_test.rs +++ b/token-core/tcx/tests/derive_test.rs @@ -16,8 +16,8 @@ use tcx::api::{ }; use tcx::handler::encode_message; use tcx::handler::import_mnemonic; -use tcx_constants::TEST_PRIVATE_KEY; use tcx_constants::{OTHER_MNEMONIC, TEST_MNEMONIC, TEST_PASSWORD}; +use tcx_constants::{TEST_PRIVATE_KEY, TEST_WIF}; use sp_core::ByteArray; @@ -821,3 +821,42 @@ fn test_derive_other_curve_on_pk_keystore() { ); }) } + +#[test] +#[serial] +fn test_derive_mainnet_account_on_test_wif() { + run_test(|| { + let param = ImportPrivateKeyParam { + password: TEST_PASSWORD.to_string(), + private_key: TEST_WIF.to_string(), + name: "wif".to_string(), + password_hint: "".to_string(), + network: "MAINNET".to_string(), + overwrite_id: "".to_string(), + }; + let ret = call_api("import_private_key", param).unwrap(); + let imported = ImportPrivateKeyResult::decode(ret.as_slice()).unwrap(); + + let derive_param = DeriveAccountsParam { + id: imported.id.to_string(), + derivations: vec![Derivation { + chain_type: "BITCOIN".to_string(), + chain_id: "".to_string(), + path: "".to_string(), + network: "TESTNET".to_string(), + curve: "secp256k1".to_string(), + seg_wit: "VERSION_1".to_string(), + bech32_prefix: "".to_string(), + }], + key: Some(api::derive_accounts_param::Key::Password( + TEST_PASSWORD.to_string(), + )), + }; + let ret = call_api("derive_accounts", derive_param).unwrap(); + let accounts = DeriveAccountsResult::decode(ret.as_slice()).unwrap(); + assert_eq!( + accounts.accounts[0].address, + "tb1pqpae4d6594jj3yueluku5tlu7r6nqwm24xc8thk5g396s9e5anvqdwrut7" + ); + }) +} diff --git a/token-core/tcx/tests/import_test.rs b/token-core/tcx/tests/import_test.rs index 5f259b4c..a7f11dbc 100644 --- a/token-core/tcx/tests/import_test.rs +++ b/token-core/tcx/tests/import_test.rs @@ -13,11 +13,12 @@ use tcx_keystore::keystore::IdentityNetwork; use prost::Message; use tcx::api::{ - export_private_key_param, CreateKeystoreParam, DeriveAccountsParam, DeriveAccountsResult, - ExistsJsonParam, ExistsKeystoreResult, ExistsPrivateKeyParam, ExportJsonParam, - ExportJsonResult, ExportMnemonicParam, ExportMnemonicResult, ExportPrivateKeyParam, - ExportPrivateKeyResult, ImportJsonParam, ImportMnemonicParam, ImportPrivateKeyParam, - ImportPrivateKeyResult, KeystoreResult, WalletKeyParam, + export_private_key_param, wallet_key_param, BackupResult, CreateKeystoreParam, + DeriveAccountsParam, DeriveAccountsResult, ExistsJsonParam, ExistsKeystoreResult, + ExistsPrivateKeyParam, ExportJsonParam, ExportJsonResult, ExportMnemonicParam, + ExportMnemonicResult, ExportPrivateKeyParam, ExportPrivateKeyResult, ImportJsonParam, + ImportMnemonicParam, ImportPrivateKeyParam, ImportPrivateKeyResult, KeystoreResult, + MigrateKeystoreParam, WalletKeyParam, }; use tcx::handler::{encode_message, import_private_key}; @@ -1161,20 +1162,6 @@ pub fn test_import_hex_private_key() { #[serial] pub fn test_import_wif_network_mismatch() { run_test(|| { - let param: ImportPrivateKeyParam = ImportPrivateKeyParam { - private_key: TEST_WIF.to_string(), - password: TEST_PASSWORD.to_string(), - name: "import_private_key_wallet".to_string(), - password_hint: "".to_string(), - network: "".to_string(), - overwrite_id: "".to_string(), - }; - let ret = call_api("import_private_key", param); - assert_eq!( - format!("{}", ret.unwrap_err()), - "private_key_network_mismatch" - ); - let param: ImportPrivateKeyParam = ImportPrivateKeyParam { private_key: TEST_WIF.to_string(), password: TEST_PASSWORD.to_string(), @@ -1183,26 +1170,17 @@ pub fn test_import_wif_network_mismatch() { network: "MAINNET".to_string(), overwrite_id: "".to_string(), }; - let ret = call_api("import_private_key", param); - // let import_result: ImportPrivateKeyResult = - // ImportPrivateKeyResult::decode(ret.as_slice()); - assert_eq!( - format!("{}", ret.unwrap_err()), - "private_key_network_mismatch" - ); - - let param: ImportPrivateKeyParam = ImportPrivateKeyParam { - private_key: TEST_WIF.to_string(), - password: TEST_PASSWORD.to_string(), - name: "import_private_key_wallet".to_string(), - password_hint: "".to_string(), - network: "TESTNET".to_string(), - overwrite_id: "".to_string(), + let ret = call_api("import_private_key", param).expect("import_private_key_test"); + let import_private_key_ret = ImportPrivateKeyResult::decode(ret.as_slice()).unwrap(); + assert_eq!("TESTNET", import_private_key_ret.identified_network); + let backup_param = WalletKeyParam { + id: import_private_key_ret.id.to_string(), + key: Some(wallet_key_param::Key::Password(TEST_PASSWORD.to_string())), }; - let ret = call_api("import_private_key", param).unwrap(); - let import_result: ImportPrivateKeyResult = - ImportPrivateKeyResult::decode(ret.as_slice()).unwrap(); - assert_eq!(import_result.identified_network, "TESTNET"); + + let ret = call_api("backup", backup_param).unwrap(); + let backup_result = BackupResult::decode(ret.as_slice()).unwrap(); + assert_eq!(backup_result.original, TEST_WIF); }) } @@ -1588,6 +1566,50 @@ pub fn test_reset_password_hd_seed_already_exist() { }) } +#[test] +#[serial] +pub fn test_reset_password_multi_hd_wallet() { + setup_test("../test-data/reset-password-wallets"); + let import_param = ImportMnemonicParam { + mnemonic: TEST_MNEMONIC.to_string(), + password: "imToken@1".to_string(), + network: "MAINNET".to_string(), + name: "reset_password".to_string(), + password_hint: "".to_string(), + overwrite_id: "a7c5ed76-5249-4e23-adcc-a36c519383a5".to_string(), + }; + + let ret = call_api("import_mnemonic", import_param).unwrap(); + let import_mnemonic_result = KeystoreResult::decode(ret.as_slice()).unwrap(); + assert!(!import_mnemonic_result.is_existed); + + let migration_param = MigrateKeystoreParam { + id: "0551d81c-d923-4b8f-ac83-07b575eebcd4".to_string(), + network: "MAINNET".to_string(), + key: Some(api::migrate_keystore_param::Key::Password( + "imToken@1".to_string(), + )), + }; + + call_api("migrate_keystore", migration_param).unwrap(); + + let import_param = ImportMnemonicParam { + mnemonic: TEST_MNEMONIC.to_string(), + password: "imToken@1".to_string(), + network: "MAINNET".to_string(), + name: "reset_password".to_string(), + password_hint: "".to_string(), + overwrite_id: "b7e27e86-6214-4e31-9f8f-ec493754e0fc".to_string(), + }; + + let ret = call_api("import_mnemonic", import_param).unwrap(); + let import_mnemonic_result = KeystoreResult::decode(ret.as_slice()).unwrap(); + assert_eq!( + import_mnemonic_result.existed_id, + "a7c5ed76-5249-4e23-adcc-a36c519383a5" + ) +} + #[test] #[serial] pub fn test_reset_password_hd_valid_overwrite_wallet_first() { diff --git a/token-core/tcx/tests/migration_test.rs b/token-core/tcx/tests/migration_test.rs index 98307461..7572ac9b 100644 --- a/token-core/tcx/tests/migration_test.rs +++ b/token-core/tcx/tests/migration_test.rs @@ -741,32 +741,6 @@ pub fn test_migrate_keystores_identified_chain_types_mainnet() { ); } -fn copy_dir(src: &Path, dst: &Path) -> Result<()> { - if src.is_dir() { - fs::create_dir_all(dst)?; // Create destination directory if it doesn't exist - for entry in src.read_dir()? { - let entry = entry?; - let path = entry.path(); - let new_dest = dst.join(path.strip_prefix(src)?); - if path.is_dir() { - copy_dir(&path, &new_dest)?; // Recursively copy subdirectories - } else { - fs::copy(&path, &new_dest)?; // Copy files - } - } - } else { - return Err(anyhow!("source is not a directory")); - } - Ok(()) -} - -fn setup_test(old_wallet_dir: &str) { - let _ = fs::remove_dir_all("/tmp/token-core-x"); - copy_dir(&Path::new(old_wallet_dir), &Path::new("/tmp/token-core-x")).unwrap(); - - init_token_core_x("/tmp/token-core-x"); -} - #[test] #[serial] fn test_migrate_duplicate_then_delete_keystore() { diff --git a/token-core/tcx/tests/sign_test.rs b/token-core/tcx/tests/sign_test.rs index 45908c7e..beb80fc0 100644 --- a/token-core/tcx/tests/sign_test.rs +++ b/token-core/tcx/tests/sign_test.rs @@ -27,7 +27,7 @@ use tcx::api::{ use tcx::handler::encode_message; use tcx::handler::get_derived_key; -use tcx_btc_kin::transaction::BtcKinTxInput; +use tcx_btc_kin::transaction::{BtcKinTxInput, BtcMessageInput, BtcMessageOutput}; use tcx_btc_kin::Utxo; use tcx_ckb::{CachedCell, CellInput, CkbTxInput, CkbTxOutput, OutPoint, Script, Witness}; use tcx_constants::{sample_key, CurveType}; @@ -542,6 +542,46 @@ fn test_tron_sign_message() { }); } +#[test] +#[serial] +fn test_bitcoin_sign_message() { + run_test(|| { + let wallet = import_default_wallet(); + + let input_expects = vec![ + (BtcMessageInput{ + message: "hello world".to_string(), + }, "02473044022062775640116afb7f17d23c222b0a6904fdaf2aea0d76e550d75c8fd362b80dcb022067c299fde774aaab689f8a53ebd0956395ff45b7ff6b7e99569d0abec85110c80121031aee5e20399d68cf0035d1a21564868f22bc448ab205292b4279136b15ecaebc"), + (BtcMessageInput{ + message: "test1".to_string(), + }, "02483045022100b805ccd16f1a664ae394bf292962ea6d76e0ddd5beb0b050cca4a1aa9ababc9a02201503132e39dc600957ec8f33663b10ab0cff0c4e37cab2811619152be8d919300121031aee5e20399d68cf0035d1a21564868f22bc448ab205292b4279136b15ecaebc"), + (BtcMessageInput{ + message: "test2".to_string(), + }, "02483045022100e96bfdb41b3562a1ff5a4c816da2620e82bcc8d702843ae1cec506666d4569c302206477d7d93c082cb42d462200a136e6aef7edde053722008a206ab8b9b356f0380121031aee5e20399d68cf0035d1a21564868f22bc448ab205292b4279136b15ecaebc"), + ]; + + for (input, expected) in input_expects { + let tx = SignParam { + id: wallet.id.to_string(), + key: Some(Key::Password(TEST_PASSWORD.to_string())), + chain_type: "BITCOIN".to_string(), + path: "m/49'/1'/0'".to_string(), + curve: "secp256k1".to_string(), + network: "TESTNET".to_string(), + seg_wit: "VERSION_0".to_string(), + input: Some(::prost_types::Any { + type_url: "imtoken".to_string(), + value: encode_message(input).unwrap(), + }), + }; + + let sign_result = call_api("sign_msg", tx).unwrap(); + let ret: BtcMessageOutput = BtcMessageOutput::decode(sign_result.as_slice()).unwrap(); + assert_eq!(expected, ret.signature); + } + }); +} + #[test] #[serial] fn test_sign_by_dk_hd_store() {