diff --git a/token-core/tcx-eos/src/signer.rs b/token-core/tcx-eos/src/signer.rs index 5d296964..e3396611 100644 --- a/token-core/tcx-eos/src/signer.rs +++ b/token-core/tcx-eos/src/signer.rs @@ -1,3 +1,5 @@ +use std::vec; + use crate::transaction::{EosMessageInput, EosMessageOutput, EosTxInput, EosTxOutput, SigData}; use tcx_keystore::{ tcx_ensure, Keystore, MessageSigner, Result, SignatureParameters, Signer, TransactionSigner, @@ -14,6 +16,40 @@ fn serial_eos_sig(sig: &[u8]) -> String { format!("SIG_K1_{}", base58::encode_slice(&data)) } +fn is_canonical(sig: &[u8]) -> bool { + !(sig[0] & 0x80 != 0) + && !(sig[0] == 0 && !(sig[1] & 0x80 != 0)) + && !(sig[32] & 0x80 != 0) + && !(sig[32] == 0 && !(sig[33] & 0x80 != 0)) +} + +fn i32_to_u8_array(value: i32) -> [u8; 32] { + let bytes = value.to_be_bytes(); + let mut result = [0u8; 32]; + result[..4].copy_from_slice(&bytes); + result +} + +fn eos_sign(keystore: &mut Keystore, hashed: &[u8], path: &str) -> Result { + let mut sign_result: Vec = vec![]; + let mut is_canon = false; + for nonce in 0..1000 { + sign_result = keystore.secp256k1_ecdsa_sign_recoverable_with_noncedata( + hashed, + &path, + &i32_to_u8_array(nonce), + )?; + if is_canonical(&sign_result) { + is_canon = true; + break; + } + } + tcx_ensure!(is_canon, anyhow!("cannot generate a eos canon sig")); + sign_result[64] += 27 + 4; + let sig = [sign_result[64..].to_vec(), sign_result[..64].to_vec()].concat(); + Ok(serial_eos_sig(&sig)) +} + impl TransactionSigner for Keystore { fn sign_transaction( &mut self, @@ -33,12 +69,11 @@ impl TransactionSigner for Keystore { ] .concat(); let hashed_tx = sha256(&tx_with_chain_id); - let sign_result = self - .secp256k1_ecdsa_sign_recoverable(hashed_tx.as_slice(), ¶ms.derivation_path)?; + let eos_sig = eos_sign(self, &hashed_tx, ¶ms.derivation_path)?; // EOS need v r s - let eos_sig = [sign_result[64..].to_vec(), sign_result[..64].to_vec()].concat(); + eos_sigs.push(SigData { - signature: serial_eos_sig(&eos_sig), + signature: eos_sig, hash: tx_hash.to_0x_hex(), }); } @@ -63,18 +98,138 @@ impl MessageSigner for Keystore { data_hashed.len() == 32, anyhow!("{}", "hashed data must be 32 bytes") ); - let sign_result = - self.secp256k1_ecdsa_sign_recoverable(data_hashed.as_slice(), ¶ms.derivation_path)?; - // EOS need v r s - let eos_sig = [sign_result[64..].to_vec(), sign_result[..64].to_vec()].concat(); - Ok(EosMessageOutput { - signature: serial_eos_sig(&eos_sig), - }) + + let eos_sig = eos_sign(self, &data_hashed, ¶ms.derivation_path)?; + Ok(EosMessageOutput { signature: eos_sig }) } } -// TODO: sign eos using RFC 6979 need new testcase -// #[cfg(test)] -// mod tests { +#[cfg(test)] +mod tests { + use std::vec; + + use bitcoin::hashes::hex::ToHex; + use tcx_constants::{TEST_MNEMONIC, TEST_PASSWORD}; + use tcx_keystore::{ + HdKeystore, Keystore, MessageSigner, Metadata, PrivateKeystore, SignatureParameters, + TransactionSigner, + }; + use tcx_primitive::{PrivateKey, Secp256k1PrivateKey}; + + use crate::transaction::{EosMessageInput, EosTxInput}; + + #[test] + fn test_eos_sign_tx() { + let meta = Metadata::default(); + + let hd_keystore = HdKeystore::from_mnemonic(TEST_MNEMONIC, TEST_PASSWORD, meta).unwrap(); + let mut keystore = Keystore::Hd(hd_keystore); + keystore.unlock_by_password(TEST_PASSWORD).unwrap(); + let sign_param = SignatureParameters { + chain_type: "EOS".to_string(), + derivation_path: "m/44'/194'/0'/0/0".to_string(), + ..SignatureParameters::default() + }; + let tx_input = EosTxInput { + chain_id: "aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906".to_string(), + tx_hexs: vec!["2b03b26547b625edc1c6000000000100a6823403ea3055000000572d3ccdcd0130069b34b2a9a48b00000000a8ed32322130069b34b2a9a48b10425e79aa47b374640000000000000004454f53000000000000".to_string(), + "8c05b2650abbf70ba628000000000100a6823403ea3055000000572d3ccdcd0130069b34b2a9a48b00000000a8ed32322130069b34b2a9a48b10425e79aa47b374e80300000000000004454f53000000000000".to_string(), + "5a09b265a5c205ed6bea000000000100a6823403ea3055000000572d3ccdcd0110425e79aa47b37400000000a8ed32322110425e79aa47b37430069b34b2a9a48b640000000000000004454f53000000000000".to_string(), + "db0bb265a7c74d49719c000000000100a6823403ea3055000000572d3ccdcd0110425e79aa47b37400000000a8ed32322110425e79aa47b37430069b34b2a9a48be80300000000000004454f53000000000000".to_string(), + "c578065b93aec6a7c811000000000100a6823403ea3055000000572d3ccdcd01000000602a48b37400000000a8ed323225000000602a48b374208410425c95b1ca80969800000000000453595300000000046d656d6f00".to_string()] + + }; + let ret = keystore.sign_transaction(&sign_param, &tx_input).unwrap(); + assert_eq!(ret.sig_data[0].signature, "SIG_K1_KYgqnbZkL57TAJtgZ4ntrCxQ38B313WWpZEDyGwA7s4sjmyHissY6WAeCdyYHukBWp2QsqEH8hdtQLchR2LZSzMhvvHmCm"); + assert_eq!( + ret.sig_data[0].hash, + "0x0461387aa99644399b1c8c876805fc775f96e6a00ce18ffbe4eaa930ad6e7af8" + ); + assert_eq!(ret.sig_data[1].signature, "SIG_K1_Jy7PBEwCpvvf5k4yzrqq1KeBCZi5qru7mp3CspNw8n8xENpN8Ar6s3ckuEeH66Rd9QFbUZzrD4pAemkBEWMyBM7PBdDR4t"); + assert_eq!( + ret.sig_data[1].hash, + "0xe36d9a49ca7768198a092c5b3f9b9766343ff14eb1fde851bed9cbda2ef1ab58" + ); + assert_eq!(ret.sig_data[2].signature, "SIG_K1_K1iY4LUoLwnYVFMaWZddr74NSmLcCBDEysybA7oTLMn7dYtFdeHpy8oSv4rEdGYoa8rzsE17QaPJikSyjDY4t3EeK2m1ir"); + assert_eq!(ret.sig_data[3].signature, "SIG_K1_Ki7M5TB9Di3i2orD1ntym5xmhh5rAeJPK8XxNfAUjeNc3SQyMA9UZ37ptfTLjngb9cfhdBG3j2DQXrderrXH59t5DcHwgT"); + assert_eq!(ret.sig_data[4].signature, "SIG_K1_K7EUD2iuUi4QFgTNuondGqjaWJ4AWzp1EMhqKg4t1oGoSKhjvTpfqv6EcD6M2R8qQvJjf7f2mV8zHEXHgLKH985DU1JPyf"); + } + + #[test] + fn test_pk_store_eos_sign_msg() { + let meta = Metadata::default(); -// } + let hex_sec_key = + Secp256k1PrivateKey::from_wif("5KAigHMamRhN7uwHFnk3yz7vUTyQT1nmXoAA899XpZKJpkqsPFp") + .unwrap() + .to_bytes() + .to_hex(); + let pk_keystore = PrivateKeystore::from_private_key( + &hex_sec_key, + TEST_PASSWORD, + tcx_constants::CurveType::SECP256k1, + meta, + None, + ) + .unwrap(); + let mut keystore = Keystore::PrivateKey(pk_keystore); + keystore.unlock_by_password(TEST_PASSWORD).unwrap(); + let sign_param = SignatureParameters { + chain_type: "EOS".to_string(), + ..SignatureParameters::default() + }; + let tx_input = EosTxInput { + chain_id: "aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906".to_string(), + tx_hexs: vec!["2b03b26547b625edc1c6000000000100a6823403ea3055000000572d3ccdcd0130069b34b2a9a48b00000000a8ed32322130069b34b2a9a48b10425e79aa47b374640000000000000004454f53000000000000".to_string(), + "8c05b2650abbf70ba628000000000100a6823403ea3055000000572d3ccdcd0130069b34b2a9a48b00000000a8ed32322130069b34b2a9a48b10425e79aa47b374e80300000000000004454f53000000000000".to_string(), + "5a09b265a5c205ed6bea000000000100a6823403ea3055000000572d3ccdcd0110425e79aa47b37400000000a8ed32322110425e79aa47b37430069b34b2a9a48b640000000000000004454f53000000000000".to_string(), + "db0bb265a7c74d49719c000000000100a6823403ea3055000000572d3ccdcd0110425e79aa47b37400000000a8ed32322110425e79aa47b37430069b34b2a9a48be80300000000000004454f53000000000000".to_string(), + "c578065b93aec6a7c811000000000100a6823403ea3055000000572d3ccdcd01000000602a48b37400000000a8ed323225000000602a48b374208410425c95b1ca80969800000000000453595300000000046d656d6f00".to_string()] + + }; + let ret = keystore.sign_transaction(&sign_param, &tx_input).unwrap(); + assert_eq!(ret.sig_data[0].signature, "SIG_K1_KYgqnbZkL57TAJtgZ4ntrCxQ38B313WWpZEDyGwA7s4sjmyHissY6WAeCdyYHukBWp2QsqEH8hdtQLchR2LZSzMhvvHmCm"); + assert_eq!( + ret.sig_data[0].hash, + "0x0461387aa99644399b1c8c876805fc775f96e6a00ce18ffbe4eaa930ad6e7af8" + ); + assert_eq!(ret.sig_data[1].signature, "SIG_K1_Jy7PBEwCpvvf5k4yzrqq1KeBCZi5qru7mp3CspNw8n8xENpN8Ar6s3ckuEeH66Rd9QFbUZzrD4pAemkBEWMyBM7PBdDR4t"); + assert_eq!( + ret.sig_data[1].hash, + "0xe36d9a49ca7768198a092c5b3f9b9766343ff14eb1fde851bed9cbda2ef1ab58" + ); + assert_eq!(ret.sig_data[2].signature, "SIG_K1_K1iY4LUoLwnYVFMaWZddr74NSmLcCBDEysybA7oTLMn7dYtFdeHpy8oSv4rEdGYoa8rzsE17QaPJikSyjDY4t3EeK2m1ir"); + assert_eq!(ret.sig_data[3].signature, "SIG_K1_Ki7M5TB9Di3i2orD1ntym5xmhh5rAeJPK8XxNfAUjeNc3SQyMA9UZ37ptfTLjngb9cfhdBG3j2DQXrderrXH59t5DcHwgT"); + assert_eq!(ret.sig_data[4].signature, "SIG_K1_K7EUD2iuUi4QFgTNuondGqjaWJ4AWzp1EMhqKg4t1oGoSKhjvTpfqv6EcD6M2R8qQvJjf7f2mV8zHEXHgLKH985DU1JPyf"); + } + + #[test] + fn test_eos_sign_msg() { + let meta = Metadata::default(); + + let hex_sec_key = + Secp256k1PrivateKey::from_wif("5HxQKWDznancXZXm7Gr2guadK7BhK9Zs8ejDhfA9oEBM89ZaAru") + .unwrap() + .to_bytes() + .to_hex(); + let pk_keystore = PrivateKeystore::from_private_key( + &hex_sec_key, + TEST_PASSWORD, + tcx_constants::CurveType::SECP256k1, + meta, + None, + ) + .unwrap(); + let mut keystore = Keystore::PrivateKey(pk_keystore); + keystore.unlock_by_password(TEST_PASSWORD).unwrap(); + let sign_param = SignatureParameters { + chain_type: "EOS".to_string(), + ..SignatureParameters::default() + }; + let tx_input = EosMessageInput { + data: "0x6cb75bc5a46a7fdb64b92efefca01ed7b060ab5e0d625226e8efbc0980c3ddc1".to_string(), + }; + let ret = keystore.sign_message(&sign_param, &tx_input).unwrap(); + assert_eq!(ret.signature, "SIG_K1_KkkPJXMxGUUeS6b5FmKrXE448N1Gc4x87j4JLVuENuba5QRUmFczGe9EmzeoCajRH5YLGEGcYjWSXxxfR5b6RTCoNUdCVy"); + } +} diff --git a/token-core/tcx-keystore/src/keystore/mod.rs b/token-core/tcx-keystore/src/keystore/mod.rs index d31f33ad..d1d22d95 100644 --- a/token-core/tcx-keystore/src/keystore/mod.rs +++ b/token-core/tcx-keystore/src/keystore/mod.rs @@ -475,6 +475,22 @@ impl Signer for Keystore { private_key.as_secp256k1()?.sign_recoverable(hash) } + fn secp256k1_ecdsa_sign_recoverable_with_noncedata( + &mut self, + hash: &[u8], + derivation_path: &str, + noncedata: &[u8; 32], + ) -> Result> { + let private_key = match self { + Keystore::PrivateKey(ks) => ks.get_private_key(CurveType::SECP256k1)?, + Keystore::Hd(ks) => ks.get_private_key(CurveType::SECP256k1, derivation_path)?, + }; + + private_key + .as_secp256k1()? + .sign_recoverable_with_noncedata(hash, noncedata) + } + fn bls_sign(&mut self, hash: &[u8], derivation_path: &str, dst: &str) -> Result> { let private_key = match self { Keystore::PrivateKey(ks) => ks.get_private_key(CurveType::BLS)?, diff --git a/token-core/tcx-keystore/src/lib.rs b/token-core/tcx-keystore/src/lib.rs index ec0c7e3e..2dde1b18 100644 --- a/token-core/tcx-keystore/src/lib.rs +++ b/token-core/tcx-keystore/src/lib.rs @@ -28,7 +28,7 @@ pub use keystore::{ PrivateKeystore, PublicKeyEncoder, Source, }; -pub use signer::{HashSigner, MessageSigner, SignatureParameters, Signer, TransactionSigner}; +pub use signer::{MessageSigner, SignatureParameters, Signer, TransactionSigner}; use thiserror::Error; diff --git a/token-core/tcx-keystore/src/signer.rs b/token-core/tcx-keystore/src/signer.rs index bdc0e3e2..4c02b352 100644 --- a/token-core/tcx-keystore/src/signer.rs +++ b/token-core/tcx-keystore/src/signer.rs @@ -32,11 +32,6 @@ pub trait MessageSigner { fn sign_message(&mut self, params: &SignatureParameters, message: &Input) -> Result; } -// The ec_sign -pub trait HashSigner { - fn sign(&self, ks: &mut Keystore, symbol: &str, address: &str, hash: &[u8]) -> Result>; -} - pub trait Signer { fn sign_hash( &mut self, @@ -52,6 +47,13 @@ pub trait Signer { derivation_path: &str, ) -> Result>; + fn secp256k1_ecdsa_sign_recoverable_with_noncedata( + &mut self, + hash: &[u8], + derivation_path: &str, + noncedata: &[u8; 32], + ) -> Result>; + fn bls_sign(&mut self, hash: &[u8], derivation_path: &str, dst: &str) -> Result>; fn sr25519_sign(&mut self, hash: &[u8], derivation_path: &str) -> Result>; diff --git a/token-core/tcx-primitive/src/secp256k1.rs b/token-core/tcx-primitive/src/secp256k1.rs index d1496ad8..d034bf08 100644 --- a/token-core/tcx-primitive/src/secp256k1.rs +++ b/token-core/tcx-primitive/src/secp256k1.rs @@ -74,6 +74,19 @@ impl Secp256k1PrivateKey { let signed_bytes = [sign[..].to_vec(), vec![(recover_id.to_i32()) as u8]].concat(); Ok(signed_bytes) } + + pub fn sign_recoverable_with_noncedata( + &self, + data: &[u8], + noncedata: &[u8; 32], + ) -> Result> { + let msg = secp256k1::Message::from_slice(data).map_err(transform_secp256k1_error)?; + let signature = + SECP256K1_ENGINE.sign_ecdsa_recoverable_with_noncedata(&msg, &self.0.inner, noncedata); + let (recover_id, sign) = signature.serialize_compact(); + let signed_bytes = [sign[..].to_vec(), vec![(recover_id.to_i32()) as u8]].concat(); + Ok(signed_bytes) + } } impl TraitPrivateKey for Secp256k1PrivateKey { diff --git a/token-core/tcx/src/lib.rs b/token-core/tcx/src/lib.rs index 2fcd4d01..68cf502a 100644 --- a/token-core/tcx/src/lib.rs +++ b/token-core/tcx/src/lib.rs @@ -792,6 +792,10 @@ mod tests { ); assert_eq!("", derived_accounts.accounts[10].address); + assert_eq!( + "EOS7Nf9TU1vZaQQgZA3cELTHJf1nnDJ6xVvqHvVzbHehsgcjrzNkq", + derived_accounts.accounts[10].public_key + ); assert_eq!( "0x37c6713aa848bCdeE372A620eEbCdcCBA55c695F", derived_accounts.accounts[11].address