use crate::mnemonic::Mnemonic;
use crate::msg::Msg;
use crate::public_key::{CosmosPublicKey, PublicKey};
use crate::utils::bytes_to_hex_str;
use crate::utils::encode_any;
use crate::utils::hex_str_to_bytes;
use crate::{coin::Fee, Address, Contact, Coin};
use crate::{error::*, utils::contains_non_hex_chars};
use cosmos_sdk_proto::cosmos::crypto::secp256k1::PubKey as ProtoSecp256k1Pubkey;
use cosmos_sdk_proto::cosmos::tx::v1beta1::Tx;
use cosmos_sdk_proto::cosmos::tx::v1beta1::{
mode_info, AuthInfo, ModeInfo, SignDoc, SignerInfo, TxBody, TxRaw,
};
use num_bigint::BigUint;
use prost::Message;
use secp256k1::constants::CURVE_ORDER as CurveN;
use secp256k1::Message as CurveMessage;
use secp256k1::{All, Secp256k1};
use std::cell::RefCell;
use secp256k1::{SecretKey, PublicKey as PublicKeyEC};
use sha2::Sha512;
use sha2::{Digest, Sha256};
use std::str::FromStr;
use std::time::Duration;
thread_local!{
pub(crate) static SECP256K1: RefCell<Secp256k1<All>> = RefCell::new(Secp256k1::new());
}
pub const DEFAULT_COSMOS_HD_PATH: &str = "m/44'/118'/0'/0/0";
pub const DEFAULT_ETHEREUM_HD_PATH: &str = "m/44'/60'/0'/0/0";
#[derive(Debug, PartialEq, Clone)]
pub struct MessageArgs {
pub sequence: u64,
pub fee: Fee,
pub timeout_height: u64,
pub chain_id: String,
pub account_number: u64,
}
struct TxParts {
body: TxBody,
body_buf: Vec<u8>,
auth_info: AuthInfo,
auth_buf: Vec<u8>,
signatures: Vec<Vec<u8>>,
}
pub trait PrivateKey: Clone + Sized {
fn from_secret(secret: &[u8]) -> Self where Self: Sized;
fn from_phrase(phrase: &str, passphrase: &str) -> Result<Self, PrivateKeyError> where Self: Sized;
fn from_hd_wallet_path(path: &str, phrase: &str, passphrase: &str) -> Result<Self, PrivateKeyError> where Self: Sized;
fn to_address(&self, prefix: &str) -> Result<Address, PrivateKeyError>;
fn get_signed_tx(&self, messages: &[Msg], args: MessageArgs, memo: &str) -> Result<Tx, PrivateKeyError>;
fn sign_std_msg(&self, messages: &[Msg], args: MessageArgs, memo: &str) -> Result<Vec<u8>, PrivateKeyError>;
}
/// This structure represents a private key of a Cosmos Network.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub struct CosmosPrivateKey([u8; 32]);
impl PrivateKey for CosmosPrivateKey {
/// Create a private key using an arbitrary slice of bytes. This function is not resistant to side
/// channel attacks and may reveal your secret and private key. It is on the other hand more compact
/// than the bip32+bip39 logic.
fn from_secret(secret: &[u8]) -> CosmosPrivateKey {
CosmosPrivateKey(from_secret(secret))
}
/// This function will take the key_import phrase provided by CosmosCLI
/// and import that key. How this is done behind the scenes is quite
/// complex. The actual seed bytes from the key_import are used to derive
/// the root of a Bip32 HD wallet. From that root Cosmos keys are derived
/// on the path m/44'/118'/0'/0/a where a=0 is the most common value used.
/// Most Cosmos wallets do not even expose a=1..n much less the rest of
/// the potential key space. This function returns m/44'/118'/0'/0/0 because
/// that's going to be the key you want essentially all the time. If you need
/// a different path use from_hd_wallet_path()
fn from_phrase(phrase: &str, passphrase: &str) -> Result<CosmosPrivateKey, PrivateKeyError> {
if phrase.is_empty() {
return Err(HdWalletError::Bip39Error(Bip39Error::BadWordCount(0)).into());
}
CosmosPrivateKey::from_hd_wallet_path(DEFAULT_COSMOS_HD_PATH, phrase, passphrase)
}
/// Derives a private key from a mnemonic phrase and passphrase, using a BIP-44 HDPath
/// The actual seed bytes are derived from the mnemonic phrase, which are then used to derive
/// the root of a Bip32 HD wallet. From that application private keys are derived
/// on the given hd_path (e.g. Cosmos' m/44'/118'/0'/0/a where a=0 is the most common value used).
/// Most Cosmos wallets do not even expose a=1..n much less the rest of
/// the potential key space.
fn from_hd_wallet_path(
hd_path: &str,
phrase: &str,
passphrase: &str,
) -> Result<CosmosPrivateKey, PrivateKeyError> {
let secret_key = from_hd_wallet_path(hd_path, phrase, passphrase)?;
Ok(CosmosPrivateKey(secret_key))
}
/// Obtain an Address for a given private key, skipping the intermediate public key
fn to_address(&self, prefix: &str) -> Result<Address, PrivateKeyError> {
let pubkey = self.to_public_key("")?;
let address = pubkey.to_address_with_prefix(prefix)?;
Ok(address)
}
/// Signs a transaction that contains at least one message using a single
/// private key, returns the standard Tx type, useful for simulations
fn get_signed_tx(
&self,
messages: &[Msg],
args: MessageArgs,
memo: &str,
) -> Result<Tx, PrivateKeyError> {
let parts = self.build_tx(messages, args, memo)?;
Ok(Tx {
body: Some(parts.body),
auth_info: Some(parts.auth_info),
signatures: parts.signatures,
})
}
/// Signs a transaction that contains at least one message using a single
/// private key.
fn sign_std_msg(
&self,
messages: &[Msg],
args: MessageArgs,
memo: &str,
) -> Result<Vec<u8>, PrivateKeyError> {
let parts = self.build_tx(messages, args, memo)?;
let tx_raw = TxRaw {
body_bytes: parts.body_buf,
auth_info_bytes: parts.auth_buf,
signatures: parts.signatures,
};
let mut txraw_buf = Vec::new();
tx_raw.encode(&mut txraw_buf).unwrap();
let digest = Sha256::digest(&txraw_buf);
trace!("TXID {}", bytes_to_hex_str(&digest));
Ok(txraw_buf)
}
}
impl CosmosPrivateKey {
/// Obtain a public key for a given private key
pub fn to_public_key(&self, prefix: &str) -> Result<CosmosPublicKey, PrivateKeyError> {
let secp256k1 = Secp256k1::new();
let sk = SecretKey::from_slice(&self.0)?;
let pkey = PublicKeyEC::from_secret_key(&secp256k1, &sk);
let compressed = pkey.serialize();
Ok(CosmosPublicKey::from_bytes(compressed, prefix)?)
}
/// Internal function that that handles building a single message to sign
/// returns an internal struct containing the parts of the built transaction
/// in a way that's easy to mix and match for various uses and output types.
fn build_tx(
&self,
messages: &[Msg],
args: MessageArgs,
memo: impl Into<String>,
) -> Result<TxParts, PrivateKeyError> {
// prefix does not matter in this case, you could use a blank string
let our_pubkey = self.to_public_key(CosmosPublicKey::DEFAULT_PREFIX)?;
let key = ProtoSecp256k1Pubkey {
key: our_pubkey.to_vec(),
};
let mut unfinished = build_unfinished_tx(key, "/cosmos.crypto.secp256k1.PubKey", messages, args.clone(), memo);
let sign_doc = SignDoc {
body_bytes: unfinished.body_buf.clone(),
auth_info_bytes: unfinished.auth_buf.clone(),
chain_id: args.chain_id.to_string(),
account_number: args.account_number,
};
// Protobuf serialization of `SignDoc`
let mut signdoc_buf = Vec::new();
sign_doc.encode(&mut signdoc_buf).unwrap();
let secp256k1 = Secp256k1::new();
let sk = SecretKey::from_slice(&self.0)?;
let digest = Sha256::digest(&signdoc_buf);
let msg = CurveMessage::from_slice(&digest)?;
// Sign the signdoc
let signed = secp256k1.sign_ecdsa(&msg, &sk);
let compact = signed.serialize_compact().to_vec();
// Finish the TxParts and return
unfinished.signatures = vec![compact];
Ok(unfinished)
}
}
impl FromStr for CosmosPrivateKey {
type Err = PrivateKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match hex_str_to_bytes(s) {
Ok(bytes) => {
if bytes.len() == 32 {
let mut inner = [0; 32];
inner.copy_from_slice(&bytes[0..32]);
Ok(CosmosPrivateKey(inner))
} else {
Err(PrivateKeyError::HexDecodeErrorWrongLength)
}
}
Err(e) => {
if contains_non_hex_chars(s) {
CosmosPrivateKey::from_phrase(s, "")
} else {
Err(e.into())
}
}
}
}
}
/// This structure represents a private key of an EVM Network.
#[cfg(feature = "ethermint")]
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub struct EthermintPrivateKey([u8; 32]);
#[cfg(feature = "ethermint")]
impl PrivateKey for EthermintPrivateKey {
/// Create a private key using an arbitrary slice of bytes. This function is not resistant to side
/// channel attacks and may reveal your secret and private key. It is on the other hand more compact
/// than the bip32+bip39 logic.
fn from_secret(secret: &[u8]) -> EthermintPrivateKey {
EthermintPrivateKey(from_secret(secret))
}
/// This function will take the key_import phrase provided by CosmosCLI
/// and import that key. How this is done behind the scenes is quite
/// complex. The actual seed bytes from the key_import are used to derive
/// the root of a Bip32 HD wallet. From that root Ethereum keys are derived
/// on the path m/44'/60'/0'/0/a where a=0 is the most common value used.
/// This function returns m/44'/60'/0'/0/0 because that's going to be the key you want
/// essentially all the time. If you need a different path use from_hd_wallet_path()
fn from_phrase(phrase: &str, passphrase: &str) -> Result<EthermintPrivateKey, PrivateKeyError> {
if phrase.is_empty() {
return Err(HdWalletError::Bip39Error(Bip39Error::BadWordCount(0)).into());
}
EthermintPrivateKey::from_hd_wallet_path(DEFAULT_ETHEREUM_HD_PATH, phrase, passphrase)
}
/// Derives a private key from a mnemonic phrase and passphrase, using a BIP-44 HDPath
/// The actual seed bytes are derived from the mnemonic phrase, which are then used to derive
/// the root of a Bip32 HD wallet. From that application private keys are derived
/// on the given hd_path (e.g. Cosmos' m/44'/118'/0'/0/a where a=0 is the most common value used).
/// Most Cosmos wallets do not even expose a=1..n much less the rest of
/// the potential key space.
fn from_hd_wallet_path(
hd_path: &str,
phrase: &str,
passphrase: &str,
) -> Result<EthermintPrivateKey, PrivateKeyError> {
let secret_key = from_hd_wallet_path(hd_path, phrase, passphrase)?;
Ok(EthermintPrivateKey(secret_key))
}
fn to_address(&self, prefix: &str) -> Result<Address, PrivateKeyError> {
let pubkey = self.to_public_key("")?;
let address = pubkey.to_address_with_prefix(prefix)?;
Ok(address)
}
fn get_signed_tx(&self, messages: &[Msg], args: MessageArgs, memo: &str) -> Result<Tx, PrivateKeyError> {
let parts = self.build_tx(messages, args, memo)?;
Ok(Tx {
body: Some(parts.body),
auth_info: Some(parts.auth_info),
signatures: parts.signatures,
})
}
fn sign_std_msg(&self, messages: &[Msg], args: MessageArgs, memo: &str) -> Result<Vec<u8>, PrivateKeyError> {
let parts = self.build_tx(messages, args, memo)?;
let tx_raw = TxRaw {
body_bytes: parts.body_buf,
auth_info_bytes: parts.auth_buf,
signatures: parts.signatures,
};
let mut txraw_buf = Vec::new();
tx_raw.encode(&mut txraw_buf).unwrap();
let digest = Sha256::digest(&txraw_buf);
trace!("TXID {}", bytes_to_hex_str(&digest));
Ok(txraw_buf)
}
}
#[cfg(feature = "ethermint")]
impl EthermintPrivateKey {
fn to_public_key(&self, prefix: &str) -> Result<crate::public_key::EthermintPublicKey, PrivateKeyError> {
let sk = SecretKey::from_slice(&self.0)?;
let pkey = SECP256K1.with(move |object| -> Result<_, PrivateKeyError> {
let secp256k1 = object.borrow();
let pkey = PublicKeyEC::from_secret_key(&secp256k1, &sk);
// Serialize the recovered public key in uncompressed format
Ok(pkey.serialize())
})?;
if pkey[1..] == [0x00u8; 64][..] {
return Err(PrivateKeyError::ZeroPrivateKey);
}
let pubkey = crate::public_key::EthermintPublicKey::from_bytes(pkey, prefix)?;
Ok(pubkey)
}
/// Internal function that that handles building a single message to sign
/// returns an internal struct containing the parts of the built transaction
/// in a way that's easy to mix and match for various uses and output types.
fn build_tx(
&self,
messages: &[Msg],
args: MessageArgs,
memo: impl Into<String>,
) -> Result<TxParts, PrivateKeyError> {
let our_pubkey = self.to_public_key(CosmosPublicKey::DEFAULT_PREFIX)?;
// TODO: Use the ethermint proto here, not the cosmos-sdk one
let pubkey_proto = ProtoSecp256k1Pubkey {
key: our_pubkey.to_vec(),
};
let mut unfinished = build_unfinished_tx(pubkey_proto, "/ethermint.crypto.v1.ethsecp256k1.PubKey", messages, args.clone(), memo);
let sign_doc = SignDoc {
body_bytes: unfinished.body_buf.clone(),
auth_info_bytes: unfinished.auth_buf.clone(),
chain_id: args.chain_id.to_string(),
account_number: args.account_number,
};
// Protobuf serialization of `SignDoc`
let mut signdoc_buf = Vec::new();
sign_doc.encode(&mut signdoc_buf).unwrap();
// Sign the signdoc
let clarity_sk = clarity::PrivateKey::from_slice(&self.0).unwrap();
let signed = clarity_sk.sign_insecure_msg(&signdoc_buf);
// Finish the TxParts and return
unfinished.signatures = vec![signed.to_bytes().to_vec()];
Ok(unfinished)
}
}
/// Create a private key using an arbitrary slice of bytes. This function is not resistant to side
/// channel attacks and may reveal your secret and private key. It is on the other hand more compact
/// than the bip32+bip39 logic.
/// Note: This implementation is shared between Ethereum and standard Cosmos SDK chains
fn from_secret(secret: &[u8]) -> [u8; 32] {
let sec_hash = Sha256::digest(secret);
let mut i = BigUint::from_bytes_be(&sec_hash);
// Parameters of the curve as explained in https://en.bitcoin.it/wiki/Secp256k1
let mut n = BigUint::from_bytes_be(&CurveN);
n -= 1u64;
i %= n;
i += 1u64;
let mut result: [u8; 32] = Default::default();
let mut i_bytes = i.to_bytes_be();
// key has leading or trailing zero that's not displayed
// by default since this is a big int library missing a defined
// integer width.
while i_bytes.len() < 32 {
i_bytes.push(0);
}
result.copy_from_slice(&i_bytes);
result
}
/// Derives a private key from a mnemonic phrase and passphrase, using a BIP-44 HDPath
/// The actual seed bytes are derived from the mnemonic phrase, which are then used to derive
/// the root of a Bip32 HD wallet. From that application private keys are derived
/// on the given hd_path (e.g. Cosmos' m/44'/118'/0'/0/a where a=0 is the most common value used).
/// Most Cosmos wallets do not even expose a=1..n much less the rest of
/// the potential key space.
/// Note: This implementation is shared between Ethereum and standard Cosmos-SDK chains
fn from_hd_wallet_path(
hd_path: &str,
phrase: &str,
passphrase: &str,
) -> Result<[u8; 32], PrivateKeyError> {
if !hd_path.starts_with('m') || hd_path.contains('\\') {
return Err(HdWalletError::InvalidPathSpec(hd_path.to_string()).into());
}
let mut iterator = hd_path.split('/');
// discard the m
let _ = iterator.next();
let key_import = Mnemonic::from_str(phrase)?;
let seed_bytes = key_import.to_seed(passphrase);
let (master_secret_key, master_chain_code) = master_key_from_seed(&seed_bytes);
let mut secret_key = master_secret_key;
let mut chain_code = master_chain_code;
for mut val in iterator {
let mut hardened = false;
if val.contains('\'') {
hardened = true;
val = val.trim_matches('\'');
}
if let Ok(parsed_int) = val.parse() {
let (s, c) = get_child_key(secret_key, chain_code, parsed_int, hardened);
secret_key = s;
chain_code = c;
} else {
return Err(HdWalletError::InvalidPathSpec(hd_path.to_string()).into());
}
}
Ok(secret_key)
}
/// This derives the master key from seed bytes, the actual usage is typically
/// for Cosmos key_import support, where we import a seed phrase.
fn master_key_from_seed(seed_bytes: &[u8]) -> ([u8; 32], [u8; 32]) {
use hmac::Hmac;
use hmac::Mac;
type HmacSha512 = Hmac<Sha512>;
let mut hasher = HmacSha512::new_from_slice(b"Bitcoin seed").unwrap();
hasher.update(seed_bytes);
let hash = hasher.finalize().into_bytes();
let mut master_secret_key: [u8; 32] = [0; 32];
let mut master_chain_code: [u8; 32] = [0; 32];
master_secret_key.copy_from_slice(&hash[0..32]);
master_chain_code.copy_from_slice(&hash[32..64]);
// key check
let _ = SecretKey::from_slice(&master_secret_key).unwrap();
(master_secret_key, master_chain_code)
}
/// This keys the child key following the bip32 https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
/// specified derivation method. This method is internal because you should really be using the public API that
/// handles key path parsing.
fn get_child_key(
k_parent: [u8; 32],
c_parent: [u8; 32],
i: u32,
hardened: bool,
) -> ([u8; 32], [u8; 32]) {
use hmac::Hmac;
use hmac::Mac;
type HmacSha512 = Hmac<Sha512>;
let i = if hardened { 2u32.pow(31) + i } else { i };
let mut hasher = HmacSha512::new_from_slice(&c_parent).unwrap();
if hardened {
hasher.update(&[0u8]);
hasher.update(&k_parent);
} else {
let scep = Secp256k1::new();
let private_key = SecretKey::from_slice(&k_parent).unwrap();
let public_key = PublicKeyEC::from_secret_key(&scep, &private_key);
hasher.update(&public_key.serialize());
}
hasher.update(&i.to_be_bytes());
let l_param = hasher.finalize().into_bytes();
// If you wanted to do this on your own (without add_assign)
// it would go like this
//
// but this implementation is not constant time and performs
// allocations, opening us up to side channel attacks.
// that being said our Hmac and SHA libraries don't clearly
// indicate if they are constant time. So this ship may have
// already sailed
//
// let n = BigUint::from_bytes_be(&CurveN);
// let parse_i_l = BigUint::from_bytes_be(&l_param[0..32]);
// let parent_key = BigUint::from_bytes_be(&k_parent);
// let child_key = (parse_i_l.clone() + parent_key) % n.clone();
// if child_key == 0u32.into() {
// panic!("child key is zeros!");
// } else if parse_i_l > n {
// panic!("child key not in curve space!")
// }
let mut parse_i_l = SecretKey::from_slice(&l_param[0..32]).unwrap();
parse_i_l.add_assign(&k_parent).unwrap();
let child_key = parse_i_l;
let mut child_key_res: [u8; 32] = [0; 32];
child_key_res
.copy_from_slice(&hex_str_to_bytes(&child_key.display_secret().to_string()).unwrap());
let mut chain_code_res: [u8; 32] = [0; 32];
chain_code_res.copy_from_slice(&l_param[32..64]);
(child_key_res, chain_code_res)
}
fn build_unfinished_tx<P: prost::Message>(
pubkey_proto: P,
proto_type_url: &str,
messages: &[Msg],
args: MessageArgs,
memo: impl Into<String>,
) -> TxParts {
// Create TxBody
let body = TxBody {
messages: messages.iter().map(|msg| msg.0.clone()).collect(),
memo: memo.into(),
timeout_height: args.timeout_height,
extension_options: Default::default(),
non_critical_extension_options: Default::default(),
};
// A protobuf serialization of a TxBody
let mut body_buf = Vec::new();
body.encode(&mut body_buf).unwrap();
let pk_any = encode_any(pubkey_proto, proto_type_url.to_string());
let single = mode_info::Single { mode: 1 };
let mode = Some(ModeInfo {
sum: Some(mode_info::Sum::Single(single)),
});
let signer_info = SignerInfo {
public_key: Some(pk_any),
mode_info: mode,
sequence: args.sequence,
};
let auth_info = AuthInfo {
signer_infos: vec![signer_info],
fee: Some(args.fee.into()),
};
// Protobuf serialization of `AuthInfo`
let mut auth_buf = Vec::new();
auth_info.encode(&mut auth_buf).unwrap();
TxParts {
body,
body_buf,
auth_info,
auth_buf,
signatures: vec![], // Unfinished
}
}
#[test]
fn test_secret() {
let private_key = CosmosPrivateKey::from_secret(b"mySecret");
assert_eq!(
private_key.0,
[
208, 190, 115, 52, 41, 67, 47, 127, 0, 212, 37, 225, 171, 0, 52, 18, 175, 167, 93, 65,
254, 40, 13, 139, 178, 235, 62, 130, 254, 252, 86,
183,
// Amino bytes: 0xe1, 0xb0, 0xf7, 0x9b, 0x20, 0xd0, 0xbe, 0x73, 0x34, 0x29, 0x43, 0x2f, 0x7f, 0x0, 0xd4, 0x25, 0xe1, 0xab, 0x0, 0x34, 0x12, 0xaf, 0xa7, 0x5d, 0x41, 0xfe, 0x28, 0xd, 0x8b, 0xb2, 0xeb, 0x3e, 0x82, 0xfe, 0xfc, 0x56, 0xb7
]
);
let public_key = private_key
.to_public_key("cosmospub")
.expect("Unable to create public key");
assert_eq!(
public_key.as_bytes(),
&vec![
2, 150, 81, 169, 170, 196, 194, 43, 39, 179, 1, 154, 238, 109, 247, 70, 38, 110, 26,
231, 70, 238, 121, 119, 42, 110, 94, 173, 25, 142, 189, 7, 195
][..]
);
let address = public_key.to_address();
assert_eq!(
address.to_string(),
"cosmos1nx7vqq8hsy8chwe27mcr4cmazdwus7zjl2ds0p"
);
}
#[test]
fn test_cosmos_key_derivation_manual() {
let words = "purse sure leg gap above pull rescue glass circle attract erupt can sail gasp shy clarify inflict anger sketch hobby scare mad reject where";
// first test the underlying functions manually
let key_import = Mnemonic::from_str(words).unwrap();
let seed_bytes = key_import.to_seed("");
let (master_secret_key, master_chain_code) = master_key_from_seed(&seed_bytes);
let (m44h, c44h) = get_child_key(master_secret_key, master_chain_code, 44, true);
let (m44h_118h, c44h_118h) = get_child_key(m44h, c44h, 118, true);
let (m44h_118h_0h, c44h_118h_0h) = get_child_key(m44h_118h, c44h_118h, 0, true);
let (m44h_118h_0h_0, c44h_118h_0h_0) = get_child_key(m44h_118h_0h, c44h_118h_0h, 0, false);
let (m44h_118h_0h_0_0, _c44h_118h_0h_0_0) =
get_child_key(m44h_118h_0h_0, c44h_118h_0h_0, 0, false);
let private_key = CosmosPrivateKey(m44h_118h_0h_0_0);
let public_key = private_key.to_public_key("cosmospub").unwrap();
let address = public_key.to_address();
assert_eq!(
address.to_bech32("cosmos").unwrap(),
"cosmos1t0sgxmpxafdfjd3k6kgg50kdgn4muh5t0phml6",
);
assert_eq!(
public_key.to_bech32("cosmospub").unwrap(),
"cosmospub1addwnpepqfn2xmm5g2uackkn62ew309n3paf0xzhug6xshv4a4yq4algm9ksugt2dx6",
);
}
#[test]
fn test_cosmos_key_derivation_with_path_parsing() {
let words = "purse sure leg gap above pull rescue glass circle attract erupt can sail gasp shy clarify inflict anger sketch hobby scare mad reject where";
// now test with automated path parsing
let private_key = CosmosPrivateKey::from_phrase(words, "").unwrap();
let public_key = private_key.to_public_key("cosmospub").unwrap();
let address = public_key.to_address();
assert_eq!(
address.to_bech32("cosmos").unwrap(),
"cosmos1t0sgxmpxafdfjd3k6kgg50kdgn4muh5t0phml6",
);
assert_eq!(
public_key.to_bech32("cosmospub").unwrap(),
"cosmospub1addwnpepqfn2xmm5g2uackkn62ew309n3paf0xzhug6xshv4a4yq4algm9ksugt2dx6",
);
}
#[test]
/// This tests deriving HD wallet keys from a given seed and i value
fn test_vector_hardened() {
let seed = hex_str_to_bytes("000102030405060708090a0b0c0d0e0f").unwrap();
let (master_privkey, master_chaincode) = master_key_from_seed(&seed);
let correct_master_privkey =
hex_str_to_bytes("e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35")
.unwrap();
let correct_master_chaincode =
hex_str_to_bytes("873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508")
.unwrap();
assert_eq!(master_privkey.len(), correct_master_privkey.len());
assert_eq!(master_chaincode.len(), correct_master_chaincode.len());
assert_eq!(master_privkey.to_vec(), correct_master_privkey);
assert_eq!(master_chaincode.to_vec(), correct_master_chaincode);
// now we try deriving some child keys
// hardened zero
let (m0_dash, c0_dash) = get_child_key(master_privkey, master_chaincode, 0, true);
let correct_m0_dash_chaincode =
hex_str_to_bytes("47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141")
.unwrap();
let correct_m0_dash_privkey =
hex_str_to_bytes("edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea")
.unwrap();
assert_eq!(m0_dash.len(), correct_m0_dash_privkey.len());
assert_eq!(c0_dash.len(), correct_m0_dash_chaincode.len());
assert_eq!(m0_dash.to_vec(), correct_m0_dash_privkey);
assert_eq!(c0_dash.to_vec(), correct_m0_dash_chaincode);
}
#[test]
/// This tests deriving HD wallet keys from a given seed and i value
fn test_vector_unhardened() {
// new seed for unhardened test
let seed = hex_str_to_bytes("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
let (master_privkey, master_chaincode) = master_key_from_seed(&seed);
let correct_master_privkey =
hex_str_to_bytes("4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e")
.unwrap();
let correct_master_chaincode =
hex_str_to_bytes("60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689")
.unwrap();
assert_eq!(master_privkey.len(), correct_master_privkey.len());
assert_eq!(master_chaincode.len(), correct_master_chaincode.len());
assert_eq!(master_privkey.to_vec(), correct_master_privkey);
assert_eq!(master_chaincode.to_vec(), correct_master_chaincode);
//m/0
let (m0, c0) = get_child_key(master_privkey, master_chaincode, 0, false);
let correct_m0_chaincode =
hex_str_to_bytes("f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c")
.unwrap();
let correct_m0_privkey =
hex_str_to_bytes("abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e")
.unwrap();
assert_eq!(m0.len(), correct_m0_privkey.len());
assert_eq!(c0.len(), correct_m0_chaincode.len());
assert_eq!(m0.to_vec(), correct_m0_privkey);
assert_eq!(c0.to_vec(), correct_m0_chaincode);
//m/0/2147483647'
let (m0, c0) = get_child_key(m0, c0, 2147483647, true);
let correct_m0_chaincode =
hex_str_to_bytes("be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9")
.unwrap();
let correct_m0_privkey =
hex_str_to_bytes("877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93")
.unwrap();
assert_eq!(m0.len(), correct_m0_privkey.len());
assert_eq!(c0.len(), correct_m0_chaincode.len());
assert_eq!(m0.to_vec(), correct_m0_privkey);
assert_eq!(c0.to_vec(), correct_m0_chaincode);
//m/0/2147483647'/1
let (m0, c0) = get_child_key(m0, c0, 1, false);
let correct_m0_chaincode =
hex_str_to_bytes("f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb")
.unwrap();
let correct_m0_privkey =
hex_str_to_bytes("704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7")
.unwrap();
assert_eq!(m0.len(), correct_m0_privkey.len());
assert_eq!(c0.len(), correct_m0_chaincode.len());
assert_eq!(m0.to_vec(), correct_m0_privkey);
assert_eq!(c0.to_vec(), correct_m0_chaincode);
//m/0/2147483647'/1/2147483646'
let (m0, c0) = get_child_key(m0, c0, 2147483646, true);
let correct_m0_chaincode =
hex_str_to_bytes("637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29")
.unwrap();
let correct_m0_privkey =
hex_str_to_bytes("f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d")
.unwrap();
assert_eq!(m0.len(), correct_m0_privkey.len());
assert_eq!(c0.len(), correct_m0_chaincode.len());
assert_eq!(m0.to_vec(), correct_m0_privkey);
assert_eq!(c0.to_vec(), correct_m0_chaincode);
}
#[test]
// this tests generating many thousands of private keys
fn test_many_key_generation() {
use rand::Rng;
for _ in 0..1000 {
let mut rng = rand::thread_rng();
let secret: [u8; 32] = rng.gen();
let cosmos_key = CosmosPrivateKey::from_secret(&secret);
let _cosmos_address = cosmos_key.to_public_key("cosmospub").unwrap().to_address();
}
}
#[test]
// this tests that a bad phrase provides an error
fn test_bad_phrase() {
let cosmos_key = CosmosPrivateKey::from_phrase("bad phrase", "");
assert!(cosmos_key.is_err())
}
#[cfg(feature = "ethermint")]
#[test]
fn test_ethermint_compatibility() {
// Test Evmos key:
let expected_address = "evmos1zkunj49253lc6wgm0gp5nk8kj2naat0j8fzkfa";
// pubkey: '{"@type":"/ethermint.crypto.v1.ethsecp256k1.PubKey","key":"Av7SwLGHN5e+WVuLgYn5rfaBdQ5WlpasMiECekGh/5P0"}'
let mnemonic = "whisper unknown entire effort supreme believe supply position noble radar badge check cotton spider affair muffin gold bird trust venue hub core they veteran";
let sk = EthermintPrivateKey::from_phrase(mnemonic, "").unwrap();
let address = sk.to_address("evmos").unwrap();
assert_eq!(expected_address, address.to_string())
}
#[cfg(feature = "ethermint")]
#[test]
fn test_ethermint_signatures() {
use cosmos_sdk_proto::cosmos::bank::v1beta1::MsgSend;
use cosmos_sdk_proto::cosmos::base::v1beta1::Coin;
use num_traits::ToPrimitive;
// Signature of "hello world" by the below mnemonic using ethermint
let expected_hello_sig = "1d7c2d4440e925581ee737bea00593141caeeb96925614ccfdc43ca2c9421e6676ab3fd097d366dd399110a8664fefddab9b1dc1289053f095ec285884c1bd6600";
let mnemonic = "whisper unknown entire effort supreme believe supply position noble radar badge check cotton spider affair muffin gold bird trust venue hub core they veteran";
let address = "evmos1zkunj49253lc6wgm0gp5nk8kj2naat0j8fzkfa".to_string();
let sk = EthermintPrivateKey::from_phrase(mnemonic, "").unwrap();
let msg = "hello world".to_string();
let clarity_sk = clarity::private_key::PrivateKey::from_slice(&sk.0).unwrap();
let signature = clarity_sk.sign_insecure_msg(msg.as_bytes());
let mut sigbytes = signature.clone().to_bytes();
let v = signature.v;
sigbytes[64] = (v.to_u8().unwrap()) - 27u8; // Fix some weirdness in the clarity implementation
assert_eq!(sigbytes.to_vec(), hex_str_to_bytes(expected_hello_sig).unwrap());
/* Sample message to sign
{
"body": {
"messages": [
{
"@type": "/cosmos.bank.v1beta1.MsgSend",
"from_address": "evmos1zkunj49253lc6wgm0gp5nk8kj2naat0j8fzkfa",
"to_address": "evmos1zkunj49253lc6wgm0gp5nk8kj2naat0j8fzkfa",
"amount": [
{
"denom": "uatom",
"amount": "1"
}
]
}
],
"memo": "",
"timeout_height": "0",
"extension_options": [],
"non_critical_extension_options": []
},
"auth_info": {
"signer_infos": [
{
"public_key": {
"@type": "/ethermint.crypto.v1.ethsecp256k1.PubKey",
"key": "Av7SwLGHN5e+WVuLgYn5rfaBdQ5WlpasMiECekGh/5P0"
},
"mode_info": {
"single": {
"mode": "SIGN_MODE_DIRECT"
}
},
"sequence": "0"
}
],
"fee": {
"amount": [],
"gas_limit": "200000",
"payer": "",
"granter": ""
}
},
"signatures": [
"3qEDrYCnLjIdlH8N2+8rvt9M/k8fLzWa+CdpWB9b0AsK3uZO12UAm/62uilyeiAeBroBAJ+vPDzFDjC9j963KQE="
]
} */
// The above signature entry converted to hex bytes in Go [fmt.Printf("%s", ([]byte)("3qEDrYCnLjIdlH8N2+8rvt9M/k8fLzWa+CdpWB9b0AsK3uZO12UAm/62uilyeiAeBroBAJ+vPDzFDjC9j963KQE="))]
// let expected_msg_sig = "337145447259436e4c6a49646c48384e322b38727674394d2f6b38664c7a57612b436470574239623041734b33755a4f313255416d2f363275696c796569416542726f42414a2b7650447a46446a43396a3936334b51453d";
let msg_send = MsgSend{
from_address: address.clone(),
to_address: address.clone(),
amount: vec![Coin{ denom: "uatom".to_string(), amount: "1".to_string() }]
};
let msg_args = MessageArgs{
sequence: 0,
fee: Fee{
amount: vec![],
gas_limit: 200000,
payer: None,
granter: None
},
timeout_height: 0,
chain_id: "chain-0".to_string(),
account_number: 0
};
let msg = Msg(encode_any(msg_send, "/cosmos.bank.v1beta1.MsgSend"));
let _sig_tx = sk.sign_std_msg(&[msg], msg_args, "").unwrap();
// TODO: Figure out how to verify we are signing the message correctly, this is tricky
// println!("{:?}", sig_tx)
}
#[cfg(feature = "ethermint")]
#[test]
fn test_bank_send_msg() {
use actix_rt::System;
let runner = System::new();
runner.block_on(async move {
let validator_mnemonic = "story check aunt clown fence fine safe harbor transfer talent topic swing original rookie wrap movie speak message drop lava any ask soul angry";
let user_mnemonic = "express language around erase away okay brass enough mind slogan aisle pen dignity strike roof palace inmate art sponsor exact almost cricket basket topic";
let ovk = crate::old_private_key::OldPrivateKey::from_phrase(validator_mnemonic.clone(), "").unwrap();
let ouk = crate::old_private_key::OldPrivateKey::from_phrase(user_mnemonic.clone(), "").unwrap();
let addr = "gravity1uhqzd23elyvpw3qqnathw7zdwhxfnep6svl52n";
let receiver = "gravity1secgjkfe900uef3xg3d5kvmvqrxr4yphyc2fel";
let contact = Contact::new("http://localhost:26657", Duration::from_secs(30), "gravity").unwrap();
use cosmos_sdk_proto::cosmos::bank::v1beta1::MsgSend;
let destination = Address::from_bech32(receiver.to_string()).unwrap();
let output = contact.old_send_coins(Coin { amount: 100u8.into(), denom: "ugraviton".to_string() }, None, destination.clone(), Some(Duration::from_secs(30)), ovk).await;
println!("old output is {:?}", output);
let nvk = CosmosPrivateKey::from_phrase(validator_mnemonic, "").unwrap();
let nuk = EthermintPrivateKey::from_phrase(user_mnemonic, "").unwrap();
// use cosmos_sdk_proto::cosmos::base::v1beta1::Coin;
// use cosmos_sdk_proto::cosmos::bank::v1beta1::msg_client::MsgClient as BankMC;
// let msg_cli = BankMC::connect("http://localhost:26657").await.unwrap();
let output = contact.send_coins(Coin { amount: 100u8.into(), denom: "ugraviton".to_string() }, None, destination.clone(), Some(Duration::from_secs(30)), nvk).await;
println!("output is {:?}", output);
let output = contact.send_coins(Coin { amount: 100u8.into(), denom: "ugraviton".to_string() }, None, destination, Some(Duration::from_secs(30)), nuk).await;
println!("output is {:?}", output)
});
}