From c1094829112402ab4d0e4f63706a3737fc6f796b Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Mon, 25 Mar 2024 16:13:27 -0400 Subject: [PATCH 01/16] feat(signer): ethereum implementation --- Cargo.lock | 115 +++++++++++++- Cargo.toml | 1 + signer/Cargo.toml | 6 +- signer/src/ecdsa.rs | 21 +-- signer/src/eth.rs | 269 ++++++++++++++++++++++++++++++++ signer/src/lib.rs | 5 + signer/wasm-tests/tests/wasm.rs | 26 ++- 7 files changed, 425 insertions(+), 18 deletions(-) create mode 100644 signer/src/eth.rs diff --git a/Cargo.lock b/Cargo.lock index f374c7f25a..b96d7124f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,9 +165,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "ark-bls12-377" @@ -582,6 +582,21 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitcoin-internals" version = "0.2.0" @@ -2454,6 +2469,16 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keccak-hash" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b286e6b663fb926e1eeb68528e69cb70ed46c6d65871a21b2215ae8154c6d3c" +dependencies = [ + "primitive-types", + "tiny-keccak", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2733,6 +2758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3192,6 +3218,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.2", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.2", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "psm" version = "0.1.21" @@ -3201,6 +3247,12 @@ dependencies = [ "cc", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.35" @@ -3252,6 +3304,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rayon" version = "1.8.1" @@ -3542,6 +3603,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ruzstd" version = "0.5.0" @@ -4763,8 +4836,10 @@ dependencies = [ "getrandom", "hex", "hmac 0.12.1", + "keccak-hash", "parity-scale-codec", "pbkdf2", + "proptest", "regex", "schnorrkel", "secp256k1", @@ -4811,6 +4886,18 @@ version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix 0.38.31", + "windows-sys 0.52.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -4872,6 +4959,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -5275,6 +5371,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -5414,6 +5516,15 @@ dependencies = [ "glob 0.2.11", ] +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index c61513dc6f..c0e8c00cf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,6 +150,7 @@ hmac = { version = "0.12.1", default-features = false } pbkdf2 = { version = "0.12.2", default-features = false } schnorrkel = { version = "0.11.4", default-features = false } secp256k1 = { version = "0.28.2", default-features = false } +keccak-hash = { version = "0.10.0", default-features = false } secrecy = "0.8.0" sha2 = { version = "0.10.8", default-features = false } zeroize = { version = "1", default-features = false } diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 7e307b2284..4b282310fa 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -15,7 +15,7 @@ description = "Sign extrinsics to be submitted by Subxt" keywords = ["parity", "subxt", "extrinsic", "signer"] [features] -default = ["sr25519", "ecdsa", "subxt", "std", "native"] +default = ["sr25519", "ecdsa", "eth", "subxt", "std", "native"] std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std", "bip39/std", "schnorrkel/std", "secp256k1/std", "sp-core/std"] # Pick the signer implementation(s) you need by enabling the @@ -24,6 +24,7 @@ std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std # https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699 sr25519 = ["schnorrkel"] ecdsa = ["secp256k1"] +eth = ["keccak-hash", "secp256k1"] # Make the keypair algorithms here compatible with Subxt's Signer trait, # so that they can be used to sign transactions for compatible chains. @@ -50,7 +51,7 @@ bip39 = { workspace = true } schnorrkel = { workspace = true, optional = true } secp256k1 = { workspace = true, optional = true, features = ["alloc", "recovery"] } secrecy = { workspace = true } - +keccak-hash = { workspace = true, optional = true } # We only pull this in to enable the JS flag for schnorrkel to use. getrandom = { workspace = true, optional = true } @@ -58,6 +59,7 @@ getrandom = { workspace = true, optional = true } [dev-dependencies] sp-core = { workspace = true } sp-keyring = { workspace = true } +proptest = "1.4.0" [package.metadata.cargo-machete] ignored = ["getrandom"] diff --git a/signer/src/ecdsa.rs b/signer/src/ecdsa.rs index de84d4bd9a..12b76a12ac 100644 --- a/signer/src/ecdsa.rs +++ b/signer/src/ecdsa.rs @@ -160,21 +160,22 @@ impl Keypair { /// Sign some message. These bytes can be used directly in a Substrate `MultiSignature::Ecdsa(..)`. pub fn sign(&self, message: &[u8]) -> Signature { - // From sp_core::ecdsa::sign: let message_hash = sp_crypto_hashing::blake2_256(message); - // From sp_core::ecdsa::sign_prehashed: let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed"); - let recsig: RecoverableSignature = - Secp256k1::signing_only().sign_ecdsa_recoverable(&wrapped, &self.0.secret_key()); - // From sp_core::ecdsa's `impl From for Signature`: - let (recid, sig): (_, [u8; 64]) = recsig.serialize_compact(); - let mut signature_bytes: [u8; 65] = [0; 65]; - signature_bytes[..64].copy_from_slice(&sig); - signature_bytes[64] = (recid.to_i32() & 0xFF) as u8; - Signature(signature_bytes) + Signature(sign(&self.0.secret_key(), &wrapped)) } } +pub(crate) fn sign(secret_key: &secp256k1::SecretKey, message: &Message) -> [u8; 65] { + let recsig: RecoverableSignature = + Secp256k1::signing_only().sign_ecdsa_recoverable(message, secret_key); + let (recid, sig): (_, [u8; 64]) = recsig.serialize_compact(); + let mut signature_bytes: [u8; 65] = [0; 65]; + signature_bytes[..64].copy_from_slice(&sig); + signature_bytes[64] = (recid.to_i32() & 0xFF) as u8; + signature_bytes +} + /// Verify that some signature for a message was created by the owner of the [`PublicKey`]. /// /// ```rust diff --git a/signer/src/eth.rs b/signer/src/eth.rs new file mode 100644 index 0000000000..c5642a242c --- /dev/null +++ b/signer/src/eth.rs @@ -0,0 +1,269 @@ +// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// This file is dual-licensed as Apache-2.0 or GPL-3.0. +// see LICENSE for license details. + +//! An ethereum signer implementation. +use derive_more::{Display, From}; +use hex::FromHex; +use keccak_hash::keccak; +use secp256k1::{Keypair, Message, Secp256k1, SecretKey}; + +/// An ethereum signer implementation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EthereumSigner(Keypair); + +impl From for EthereumSigner { + fn from(kp: crate::ecdsa::Keypair) -> Self { + EthereumSigner(kp.0) + } +} + +impl EthereumSigner { + /// Construct an ethereum signer from a hex-encoded private key. + pub fn from_private_key_hex(hex: &str) -> Result { + let seed = <[u8; 32]>::from_hex(hex).map_err(Error::Hex)?; + let secret = SecretKey::from_slice(&seed).map_err(|_| Error::InvalidPrivateKey)?; + Ok(EthereumSigner(secp256k1::Keypair::from_secret_key( + &Secp256k1::signing_only(), + &secret, + ))) + } + + /// Obtain the [`secp256k1::PublicKey`] of this signer. + pub fn public_key(&self) -> secp256k1::PublicKey { + self.0.public_key() + } + + /// Obtains the public address of the account by taking the last 20 bytes + /// of the Keccak-256 hash of the public key. + pub fn account_id(&self) -> AccountId20 { + let uncompressed = self.0.public_key().serialize_uncompressed(); + let hash = keccak(&uncompressed[1..]).0; + let hash20 = hash[12..].try_into().expect("should be 20 bytes"); + AccountId20(hash20) + } + + /// Sign any arbitrary message. + pub fn sign(&self, signer_payload: &[u8]) -> EthereumSignature { + let message_hash = keccak(signer_payload); + let wrapped = + Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed"); + EthereumSignature(crate::ecdsa::sign(&self.0.secret_key(), &wrapped)) + } +} + +/// A signature generated by [`EthereumSigner::sign()`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode)] +pub struct EthereumSignature(pub [u8; 65]); + +impl AsRef<[u8; 65]> for EthereumSignature { + fn as_ref(&self) -> &[u8; 65] { + &self.0 + } +} + +/// A 20-byte cryptographic identifier. +#[derive(Debug, Copy, Clone, PartialEq, Eq, codec::Encode)] +pub struct AccountId20(pub [u8; 20]); + +impl AsRef<[u8]> for AccountId20 { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// Verify that some signature for a message was created by the owner of the [`secp256k1::PublicKey`]. +/// +/// ```rust +/// use subxt_signer::eth; +/// +/// let signer = eth::dev::alice(); +/// let message = b"Hello!"; +/// +/// let signature = signer.sign(message); +/// let public_key = signer.public_key(); +/// assert!(eth::verify(&signature, message, &public_key)); +/// ``` +pub fn verify>( + sig: &EthereumSignature, + message: M, + pub_key: &secp256k1::PublicKey, +) -> bool { + let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig.0[..64]) else { + return false; + }; + let message_hash = keccak(message.as_ref()); + let wrapped = + Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed"); + + Secp256k1::verification_only() + .verify_ecdsa(&wrapped, &signature, pub_key) + .is_ok() +} + +/// An error handed back if creating the ethereum signer fails. +#[derive(Debug, PartialEq, Display, From)] +pub enum Error { + /// Invalid private key. + #[display(fmt = "Invalid private key")] + #[from(ignore)] + InvalidPrivateKey, + /// Invalid hex. + #[display(fmt = "Cannot parse hex string: {_0}")] + Hex(hex::FromHexError), +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +/// Dev accounts, helpful for testing but not to be used in production, +/// since the secret keys are known. +pub mod dev { + use super::*; + use crate::ecdsa::dev; + + once_static_cloned! { + /// Equivalent to `{DEV_PHRASE}//Alice`. + pub fn alice() -> EthereumSigner { + dev::alice().into() + } + /// Equivalent to `{DEV_PHRASE}//Bob`. + pub fn bob() -> EthereumSigner { + dev::bob().into() + } + /// Equivalent to `{DEV_PHRASE}//Charlie`. + pub fn charlie() -> EthereumSigner { + dev::charlie().into() + } + /// Equivalent to `{DEV_PHRASE}//Dave`. + pub fn dave() -> EthereumSigner { + dev::dave().into() + } + /// Equivalent to `{DEV_PHRASE}//Eve`. + pub fn eve() -> EthereumSigner { + dev::eve().into() + } + /// Equivalent to `{DEV_PHRASE}//Ferdie`. + pub fn ferdie() -> EthereumSigner { + dev::ferdie().into() + } + /// Equivalent to `{DEV_PHRASE}//One`. + pub fn one() -> EthereumSigner { + dev::one().into() + } + /// Equivalent to `{DEV_PHRASE}//Two`. + pub fn two() -> EthereumSigner { + dev::two().into() + } + } +} + +#[cfg(feature = "subxt")] +mod subxt_compat { + use super::*; + + impl subxt::tx::Signer for EthereumSigner + where + T::AccountId: From, + T::Address: From, + T::Signature: From, + { + fn account_id(&self) -> T::AccountId { + self.account_id().into() + } + + fn address(&self) -> T::Address { + self.account_id().into() + } + + fn sign(&self, signer_payload: &[u8]) -> T::Signature { + self.sign(signer_payload).into() + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use proptest::prelude::*; + + use hex::ToHex; + + enum StubEthRuntimeConfig {} + + impl subxt::Config for StubEthRuntimeConfig { + type Hash = subxt::utils::H256; + type AccountId = super::AccountId20; + type Address = super::AccountId20; + type Signature = super::EthereumSignature; + type Hasher = subxt::config::substrate::BlakeTwo256; + type Header = + subxt::config::substrate::SubstrateHeader; + type ExtrinsicParams = subxt::config::SubstrateExtrinsicParams; + type AssetId = u32; + } + + type Signer = dyn subxt::tx::Signer; + + prop_compose! { + fn keypair()(seed in any::<[u8; 32]>()) -> secp256k1::Keypair { + let secret = SecretKey::from_slice(&seed).expect("valid secret key"); + secp256k1::Keypair::from_secret_key( + &Secp256k1::new(), + &secret, + ) + } + } + + proptest! { + #[test] + fn check_subxt_signer_implementation_matches(keypair in keypair(), msg in ".*") { + let eth_signer = EthereumSigner(keypair); + let msg_as_bytes = msg.as_bytes(); + + assert_eq!(Signer::account_id(ð_signer), eth_signer.account_id()); + assert_eq!(Signer::sign(ð_signer, msg_as_bytes), eth_signer.sign(msg_as_bytes)); + } + + #[test] + fn check_account_id(keypair in keypair()) { + let account_id = { + let uncompressed = keypair.public_key().serialize_uncompressed(); + let hash = keccak(&uncompressed[1..]).0; + let hash20 = hash[12..].try_into().expect("should be 20 bytes"); + AccountId20(hash20) + }; + let eth_signer = EthereumSigner(keypair); + + assert_eq!(eth_signer.account_id(), account_id); + + } + + #[test] + fn check_account_id_eq_address(keypair in keypair()) { + let eth_signer = EthereumSigner(keypair); + assert_eq!(Signer::account_id(ð_signer), Signer::address(ð_signer)); + } + + #[test] + fn check_from_private_key_hex_matches(keypair in keypair()) { + let private_key = keypair.secret_key(); + let private_key_hex = private_key.as_ref().encode_hex::(); + let eth_signer = EthereumSigner::from_private_key_hex(&private_key_hex) + .expect("valid private key"); + assert_eq!(eth_signer, EthereumSigner(keypair)); + } + + #[test] + fn check_signing_and_verifying_matches(keypair in keypair(), msg in ".*") { + let eth_signer = EthereumSigner(keypair); + let sig = Signer::sign(ð_signer, msg.as_bytes()); + + assert!(verify( + &sig, + msg, + ð_signer.public_key()) + ); + } + } +} diff --git a/signer/src/lib.rs b/signer/src/lib.rs index db13732327..18abb9eb33 100644 --- a/signer/src/lib.rs +++ b/signer/src/lib.rs @@ -32,6 +32,11 @@ pub mod sr25519; #[cfg_attr(docsrs, doc(cfg(feature = "ecdsa")))] pub mod ecdsa; +// An ethereum signer implementation. +#[cfg(feature = "eth")] +#[cfg_attr(docsrs, doc(cfg(feature = "eth")))] +pub mod eth; + // Re-export useful bits and pieces for generating a Pair from a phrase, // namely the Mnemonic struct. pub use bip39; diff --git a/signer/wasm-tests/tests/wasm.rs b/signer/wasm-tests/tests/wasm.rs index d5cf4227ba..3121f04af4 100644 --- a/signer/wasm-tests/tests/wasm.rs +++ b/signer/wasm-tests/tests/wasm.rs @@ -1,6 +1,6 @@ #![cfg(target_arch = "wasm32")] -use subxt_signer::{ ecdsa, sr25519 }; +use subxt_signer::{ecdsa, eth, sr25519}; use wasm_bindgen_test::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -22,7 +22,11 @@ async fn wasm_sr25519_signing_works() { // There's some non-determinism in the signing, so this ensures that // the rand stuff is configured properly to run ok in wasm. let signature = alice.sign(b"Hello there"); - assert!(sr25519::verify(&signature, b"Hello there", &alice.public_key())); + assert!(sr25519::verify( + &signature, + b"Hello there", + &alice.public_key() + )); } #[wasm_bindgen_test] @@ -32,5 +36,19 @@ async fn wasm_ecdsa_signing_works() { // There's some non-determinism in the signing, so this ensures that // the rand stuff is configured properly to run ok in wasm. let signature = alice.sign(b"Hello there"); - assert!(ecdsa::verify(&signature, b"Hello there", &alice.public_key())); -} \ No newline at end of file + assert!(ecdsa::verify( + &signature, + b"Hello there", + &alice.public_key() + )); +} + +#[wasm_bindgen_test] +async fn wasm_eth_signing_works() { + let alice = ecdsa::eth::alice(); + + // There's some non-determinism in the signing, so this ensures that + // the rand stuff is configured properly to run ok in wasm. + let signature = alice.sign(b"Hello there"); + assert!(eth::verify(&signature, b"Hello there", &alice.public_key())); +} From e500d75367df2a7298bf49d4ad12c7e5db771ed0 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Mon, 25 Mar 2024 23:55:34 -0400 Subject: [PATCH 02/16] fix: wasm tests --- signer/wasm-tests/Cargo.toml | 8 +++++++- signer/wasm-tests/tests/wasm.rs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/signer/wasm-tests/Cargo.toml b/signer/wasm-tests/Cargo.toml index 5bf218a4c2..8763ae3297 100644 --- a/signer/wasm-tests/Cargo.toml +++ b/signer/wasm-tests/Cargo.toml @@ -13,7 +13,13 @@ console_error_panic_hook = "0.1.7" # enable the "web" feature here but don't want it enabled as part # of workspace builds. Also disable the "subxt" feature here because # we want to ensure it works in isolation of that. -subxt-signer = { path = "..", default-features = false, features = ["web", "sr25519", "ecdsa", "std"] } +subxt-signer = { path = "..", default-features = false, features = [ + "web", + "sr25519", + "ecdsa", + "eth", + "std", +] } # this shouldn't be needed, it's in workspace.exclude, but still # I get the complaint unless I add it... diff --git a/signer/wasm-tests/tests/wasm.rs b/signer/wasm-tests/tests/wasm.rs index 3121f04af4..53f08ab292 100644 --- a/signer/wasm-tests/tests/wasm.rs +++ b/signer/wasm-tests/tests/wasm.rs @@ -45,7 +45,7 @@ async fn wasm_ecdsa_signing_works() { #[wasm_bindgen_test] async fn wasm_eth_signing_works() { - let alice = ecdsa::eth::alice(); + let alice = eth::dev::alice(); // There's some non-determinism in the signing, so this ensures that // the rand stuff is configured properly to run ok in wasm. From 5bb6b176efbca48f4a0ac817eadf92c2d75451f2 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Tue, 26 Mar 2024 13:19:05 -0400 Subject: [PATCH 03/16] updates --- Cargo.toml | 1 + signer/Cargo.toml | 2 +- signer/src/ecdsa.rs | 2 +- signer/src/eth.rs | 225 +++++++++++++++++++++++++++++--------------- 4 files changed, 154 insertions(+), 76 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c0e8c00cf1..a9703a56af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ which = "5.0.0" scale-typegen-description = "0.2.0" scale-typegen = "0.2.0" strip-ansi-escapes = "0.2.0" +proptest = "1.4.0" # Light client support: smoldot = { version = "0.16.0", default-features = false } diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 4b282310fa..072a1312d0 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -59,7 +59,7 @@ getrandom = { workspace = true, optional = true } [dev-dependencies] sp-core = { workspace = true } sp-keyring = { workspace = true } -proptest = "1.4.0" +proptest = { workspace = true } [package.metadata.cargo-machete] ignored = ["getrandom"] diff --git a/signer/src/ecdsa.rs b/signer/src/ecdsa.rs index 12b76a12ac..4ab93228a0 100644 --- a/signer/src/ecdsa.rs +++ b/signer/src/ecdsa.rs @@ -39,7 +39,7 @@ impl AsRef<[u8]> for PublicKey { } /// An ecdsa keypair implementation. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Keypair(pub secp256k1::Keypair); impl Keypair { diff --git a/signer/src/eth.rs b/signer/src/eth.rs index c5642a242c..def133e495 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -1,62 +1,142 @@ -// Copyright 2019-2023 Parity Technologies (UK) Ltd. +// Copyright 2019-2024 Parity Technologies (UK) Ltd. // This file is dual-licensed as Apache-2.0 or GPL-3.0. // see LICENSE for license details. -//! An ethereum signer implementation. +//! An ethereum keypair implementation. + use derive_more::{Display, From}; use hex::FromHex; use keccak_hash::keccak; -use secp256k1::{Keypair, Message, Secp256k1, SecretKey}; +use secp256k1::{Message, Secp256k1, SecretKey}; + +use crate::crypto::{DeriveJunction, SecretUri}; +use crate::ecdsa; + +const SEED_LENGTH: usize = 32; + +/// Seed bytes used to generate a key pair. +pub type Seed = [u8; SEED_LENGTH]; -/// An ethereum signer implementation. +/// An ethereum keypair implementation. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct EthereumSigner(Keypair); +pub struct Keypair(ecdsa::Keypair); -impl From for EthereumSigner { - fn from(kp: crate::ecdsa::Keypair) -> Self { - EthereumSigner(kp.0) +impl From for Keypair { + fn from(kp: ecdsa::Keypair) -> Self { + Self(kp) } } -impl EthereumSigner { - /// Construct an ethereum signer from a hex-encoded private key. - pub fn from_private_key_hex(hex: &str) -> Result { +impl Keypair { + /// Construct a keypair from a hex-encoded private key. + pub fn from_private_key_hex(hex: &str) -> Result { let seed = <[u8; 32]>::from_hex(hex).map_err(Error::Hex)?; let secret = SecretKey::from_slice(&seed).map_err(|_| Error::InvalidPrivateKey)?; - Ok(EthereumSigner(secp256k1::Keypair::from_secret_key( - &Secp256k1::signing_only(), - &secret, - ))) + let kp = secp256k1::Keypair::from_secret_key(&Secp256k1::signing_only(), &secret); + Ok(Self(ecdsa::Keypair(kp))) + } + + /// Create a keypair from a [`SecretUri`]. See the [`SecretUri`] docs for more. + /// + /// # Example + /// + /// ```rust + /// use subxt_signer::{ SecretUri, eth::Keypair }; + /// use std::str::FromStr; + /// + /// let uri = SecretUri::from_str("//Alice").unwrap(); + /// let keypair = Keypair::from_uri(&uri).unwrap(); + /// + /// keypair.sign(b"Hello world!"); + /// ``` + pub fn from_uri(uri: &SecretUri) -> Result { + ecdsa::Keypair::from_uri(uri) + .map(Self) + .map_err(Error::Inner) + } + + /// Create a keypair from a BIP-39 mnemonic phrase and optional password. + /// + /// # Example + /// + /// ```rust + /// use subxt_signer::{ bip39::Mnemonic, eth::Keypair }; + /// + /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; + /// let mnemonic = Mnemonic::parse(phrase).unwrap(); + /// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap(); + /// + /// keypair.sign(b"Hello world!"); + /// ``` + pub fn from_phrase(mnemonic: &bip39::Mnemonic, password: Option<&str>) -> Result { + ecdsa::Keypair::from_phrase(mnemonic, password) + .map(Self) + .map_err(Error::Inner) } - /// Obtain the [`secp256k1::PublicKey`] of this signer. - pub fn public_key(&self) -> secp256k1::PublicKey { + /// Turn a 32 byte seed into a keypair. + /// + /// # Warning + /// + /// This will only be secure if the seed is secure! + pub fn from_seed(seed: Seed) -> Result { + ecdsa::Keypair::from_seed(seed) + .map(Self) + .map_err(Error::Inner) + } + + /// Derive a child key from this one given a series of junctions. + /// + /// # Example + /// + /// ```rust + /// use subxt_signer::{ bip39::Mnemonic, eth::Keypair, DeriveJunction }; + /// + /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; + /// let mnemonic = Mnemonic::parse(phrase).unwrap(); + /// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap(); + /// + /// // Equivalent to the URI path '//Alice//stash': + /// let new_keypair = keypair.derive([ + /// DeriveJunction::hard("Alice"), + /// DeriveJunction::hard("stash") + /// ]); + /// ``` + pub fn derive>( + &self, + junctions: Js, + ) -> Result { + self.0.derive(junctions).map(Self).map_err(Error::Inner) + } + + /// Obtain the [`ecdsa::PublicKey`] of this keypair. + pub fn public_key(&self) -> ecdsa::PublicKey { self.0.public_key() } /// Obtains the public address of the account by taking the last 20 bytes /// of the Keccak-256 hash of the public key. pub fn account_id(&self) -> AccountId20 { - let uncompressed = self.0.public_key().serialize_uncompressed(); + let uncompressed = self.0 .0.public_key().serialize_uncompressed(); let hash = keccak(&uncompressed[1..]).0; let hash20 = hash[12..].try_into().expect("should be 20 bytes"); AccountId20(hash20) } - /// Sign any arbitrary message. - pub fn sign(&self, signer_payload: &[u8]) -> EthereumSignature { + /// Signs an arbitrary message payload. + pub fn sign(&self, signer_payload: &[u8]) -> Signature { let message_hash = keccak(signer_payload); let wrapped = Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed"); - EthereumSignature(crate::ecdsa::sign(&self.0.secret_key(), &wrapped)) + Signature(crate::ecdsa::sign(&self.0 .0.secret_key(), &wrapped)) } } -/// A signature generated by [`EthereumSigner::sign()`]. +/// A signature generated by [`Keypair::sign()`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode)] -pub struct EthereumSignature(pub [u8; 65]); +pub struct Signature(pub [u8; 65]); -impl AsRef<[u8; 65]> for EthereumSignature { +impl AsRef<[u8; 65]> for Signature { fn as_ref(&self) -> &[u8; 65] { &self.0 } @@ -72,36 +152,33 @@ impl AsRef<[u8]> for AccountId20 { } } -/// Verify that some signature for a message was created by the owner of the [`secp256k1::PublicKey`]. +/// Verify that some signature for a message was created by the owner of the [`ecdsa::PublicKey`]. /// /// ```rust /// use subxt_signer::eth; /// -/// let signer = eth::dev::alice(); +/// let keypair = eth::dev::alice(); /// let message = b"Hello!"; /// -/// let signature = signer.sign(message); -/// let public_key = signer.public_key(); +/// let signature = keypair.sign(message); +/// let public_key = keypair.public_key(); /// assert!(eth::verify(&signature, message, &public_key)); /// ``` -pub fn verify>( - sig: &EthereumSignature, - message: M, - pub_key: &secp256k1::PublicKey, -) -> bool { +pub fn verify>(sig: &Signature, message: M, pub_key: &ecdsa::PublicKey) -> bool { let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig.0[..64]) else { return false; }; let message_hash = keccak(message.as_ref()); let wrapped = Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed"); + let pub_key = secp256k1::PublicKey::from_slice(&pub_key.0).expect("valid public key"); Secp256k1::verification_only() - .verify_ecdsa(&wrapped, &signature, pub_key) + .verify_ecdsa(&wrapped, &signature, &pub_key) .is_ok() } -/// An error handed back if creating the ethereum signer fails. +/// An error handed back if creating the keypair fails. #[derive(Debug, PartialEq, Display, From)] pub enum Error { /// Invalid private key. @@ -111,6 +188,9 @@ pub enum Error { /// Invalid hex. #[display(fmt = "Cannot parse hex string: {_0}")] Hex(hex::FromHexError), + /// Inner, + #[display(fmt = "{_0}")] + Inner(ecdsa::Error), } #[cfg(feature = "std")] @@ -120,40 +200,40 @@ impl std::error::Error for Error {} /// since the secret keys are known. pub mod dev { use super::*; - use crate::ecdsa::dev; + use core::str::FromStr; once_static_cloned! { /// Equivalent to `{DEV_PHRASE}//Alice`. - pub fn alice() -> EthereumSigner { - dev::alice().into() + pub fn alice() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Alice").unwrap()).unwrap() } /// Equivalent to `{DEV_PHRASE}//Bob`. - pub fn bob() -> EthereumSigner { - dev::bob().into() + pub fn bob() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Bob").unwrap()).unwrap() } /// Equivalent to `{DEV_PHRASE}//Charlie`. - pub fn charlie() -> EthereumSigner { - dev::charlie().into() + pub fn charlie() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Charlie").unwrap()).unwrap() } /// Equivalent to `{DEV_PHRASE}//Dave`. - pub fn dave() -> EthereumSigner { - dev::dave().into() + pub fn dave() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Dave").unwrap()).unwrap() } /// Equivalent to `{DEV_PHRASE}//Eve`. - pub fn eve() -> EthereumSigner { - dev::eve().into() + pub fn eve() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Eve").unwrap()).unwrap() } /// Equivalent to `{DEV_PHRASE}//Ferdie`. - pub fn ferdie() -> EthereumSigner { - dev::ferdie().into() + pub fn ferdie() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Ferdie").unwrap()).unwrap() } /// Equivalent to `{DEV_PHRASE}//One`. - pub fn one() -> EthereumSigner { - dev::one().into() + pub fn one() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//One").unwrap()).unwrap() } /// Equivalent to `{DEV_PHRASE}//Two`. - pub fn two() -> EthereumSigner { - dev::two().into() + pub fn two() -> Keypair { + Keypair::from_uri(&SecretUri::from_str("//Two").unwrap()).unwrap() } } } @@ -162,11 +242,11 @@ pub mod dev { mod subxt_compat { use super::*; - impl subxt::tx::Signer for EthereumSigner + impl subxt::tx::Signer for Keypair where T::AccountId: From, T::Address: From, - T::Signature: From, + T::Signature: From, { fn account_id(&self) -> T::AccountId { self.account_id().into() @@ -188,14 +268,13 @@ mod test { use proptest::prelude::*; use hex::ToHex; - enum StubEthRuntimeConfig {} impl subxt::Config for StubEthRuntimeConfig { type Hash = subxt::utils::H256; type AccountId = super::AccountId20; type Address = super::AccountId20; - type Signature = super::EthereumSignature; + type Signature = super::Signature; type Hasher = subxt::config::substrate::BlakeTwo256; type Header = subxt::config::substrate::SubstrateHeader; @@ -206,63 +285,61 @@ mod test { type Signer = dyn subxt::tx::Signer; prop_compose! { - fn keypair()(seed in any::<[u8; 32]>()) -> secp256k1::Keypair { + fn keypair()(seed in any::<[u8; 32]>()) -> Keypair { let secret = SecretKey::from_slice(&seed).expect("valid secret key"); - secp256k1::Keypair::from_secret_key( + let inner = secp256k1::Keypair::from_secret_key( &Secp256k1::new(), &secret, - ) + ); + + Keypair(ecdsa::Keypair(inner)) } } proptest! { #[test] fn check_subxt_signer_implementation_matches(keypair in keypair(), msg in ".*") { - let eth_signer = EthereumSigner(keypair); let msg_as_bytes = msg.as_bytes(); - assert_eq!(Signer::account_id(ð_signer), eth_signer.account_id()); - assert_eq!(Signer::sign(ð_signer, msg_as_bytes), eth_signer.sign(msg_as_bytes)); + assert_eq!(Signer::account_id(&keypair), keypair.account_id()); + assert_eq!(Signer::sign(&keypair, msg_as_bytes), keypair.sign(msg_as_bytes)); } #[test] fn check_account_id(keypair in keypair()) { let account_id = { - let uncompressed = keypair.public_key().serialize_uncompressed(); + let uncompressed = keypair.0.0.public_key().serialize_uncompressed(); let hash = keccak(&uncompressed[1..]).0; let hash20 = hash[12..].try_into().expect("should be 20 bytes"); AccountId20(hash20) }; - let eth_signer = EthereumSigner(keypair); - assert_eq!(eth_signer.account_id(), account_id); + assert_eq!(keypair.account_id(), account_id); } #[test] fn check_account_id_eq_address(keypair in keypair()) { - let eth_signer = EthereumSigner(keypair); - assert_eq!(Signer::account_id(ð_signer), Signer::address(ð_signer)); + assert_eq!(Signer::account_id(&keypair), Signer::address(&keypair)); } #[test] fn check_from_private_key_hex_matches(keypair in keypair()) { - let private_key = keypair.secret_key(); + let private_key = keypair.0.0.secret_key(); let private_key_hex = private_key.as_ref().encode_hex::(); - let eth_signer = EthereumSigner::from_private_key_hex(&private_key_hex) - .expect("valid private key"); - assert_eq!(eth_signer, EthereumSigner(keypair)); + + assert_eq!(keypair, + Keypair::from_private_key_hex(&private_key_hex).expect("valid private key")); } #[test] fn check_signing_and_verifying_matches(keypair in keypair(), msg in ".*") { - let eth_signer = EthereumSigner(keypair); - let sig = Signer::sign(ð_signer, msg.as_bytes()); + let sig = Signer::sign(&keypair, msg.as_bytes()); assert!(verify( &sig, msg, - ð_signer.public_key()) + &keypair.public_key()) ); } } From 3662da7afe86ecc939096d41e5b08f1d9fb0b3a2 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 27 Mar 2024 09:52:31 -0400 Subject: [PATCH 04/16] remove from private key hex --- signer/src/eth.rs | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/signer/src/eth.rs b/signer/src/eth.rs index def133e495..b3ec938026 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -5,9 +5,8 @@ //! An ethereum keypair implementation. use derive_more::{Display, From}; -use hex::FromHex; use keccak_hash::keccak; -use secp256k1::{Message, Secp256k1, SecretKey}; +use secp256k1::{Message, Secp256k1}; use crate::crypto::{DeriveJunction, SecretUri}; use crate::ecdsa; @@ -28,14 +27,6 @@ impl From for Keypair { } impl Keypair { - /// Construct a keypair from a hex-encoded private key. - pub fn from_private_key_hex(hex: &str) -> Result { - let seed = <[u8; 32]>::from_hex(hex).map_err(Error::Hex)?; - let secret = SecretKey::from_slice(&seed).map_err(|_| Error::InvalidPrivateKey)?; - let kp = secp256k1::Keypair::from_secret_key(&Secp256k1::signing_only(), &secret); - Ok(Self(ecdsa::Keypair(kp))) - } - /// Create a keypair from a [`SecretUri`]. See the [`SecretUri`] docs for more. /// /// # Example @@ -267,7 +258,6 @@ mod test { use super::*; use proptest::prelude::*; - use hex::ToHex; enum StubEthRuntimeConfig {} impl subxt::Config for StubEthRuntimeConfig { @@ -286,7 +276,7 @@ mod test { prop_compose! { fn keypair()(seed in any::<[u8; 32]>()) -> Keypair { - let secret = SecretKey::from_slice(&seed).expect("valid secret key"); + let secret = secp256k1::SecretKey::from_slice(&seed).expect("valid secret key"); let inner = secp256k1::Keypair::from_secret_key( &Secp256k1::new(), &secret, @@ -323,15 +313,6 @@ mod test { assert_eq!(Signer::account_id(&keypair), Signer::address(&keypair)); } - #[test] - fn check_from_private_key_hex_matches(keypair in keypair()) { - let private_key = keypair.0.0.secret_key(); - let private_key_hex = private_key.as_ref().encode_hex::(); - - assert_eq!(keypair, - Keypair::from_private_key_hex(&private_key_hex).expect("valid private key")); - } - #[test] fn check_signing_and_verifying_matches(keypair in keypair(), msg in ".*") { let sig = Signer::sign(&keypair, msg.as_bytes()); From 2590e510e2c77f127b3b89187c82e1aefb96d738 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 27 Mar 2024 09:53:48 -0400 Subject: [PATCH 05/16] rename signer to subxt signer --- signer/src/eth.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/signer/src/eth.rs b/signer/src/eth.rs index b3ec938026..c8b28d4ba6 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -272,7 +272,7 @@ mod test { type AssetId = u32; } - type Signer = dyn subxt::tx::Signer; + type SubxtSigner = dyn subxt::tx::Signer; prop_compose! { fn keypair()(seed in any::<[u8; 32]>()) -> Keypair { @@ -291,8 +291,8 @@ mod test { fn check_subxt_signer_implementation_matches(keypair in keypair(), msg in ".*") { let msg_as_bytes = msg.as_bytes(); - assert_eq!(Signer::account_id(&keypair), keypair.account_id()); - assert_eq!(Signer::sign(&keypair, msg_as_bytes), keypair.sign(msg_as_bytes)); + assert_eq!(SubxtSigner::account_id(&keypair), keypair.account_id()); + assert_eq!(SubxtSigner::sign(&keypair, msg_as_bytes), keypair.sign(msg_as_bytes)); } #[test] @@ -310,12 +310,12 @@ mod test { #[test] fn check_account_id_eq_address(keypair in keypair()) { - assert_eq!(Signer::account_id(&keypair), Signer::address(&keypair)); + assert_eq!(SubxtSigner::account_id(&keypair), SubxtSigner::address(&keypair)); } #[test] fn check_signing_and_verifying_matches(keypair in keypair(), msg in ".*") { - let sig = Signer::sign(&keypair, msg.as_bytes()); + let sig = SubxtSigner::sign(&keypair, msg.as_bytes()); assert!(verify( &sig, From 337c19242591cd8f15bdc7f3c934b2ef9f79e410 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 27 Mar 2024 09:54:18 -0400 Subject: [PATCH 06/16] remove unnecessary super usage --- signer/src/eth.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/signer/src/eth.rs b/signer/src/eth.rs index c8b28d4ba6..f33f42dd6e 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -262,9 +262,9 @@ mod test { impl subxt::Config for StubEthRuntimeConfig { type Hash = subxt::utils::H256; - type AccountId = super::AccountId20; - type Address = super::AccountId20; - type Signature = super::Signature; + type AccountId = AccountId20; + type Address = AccountId20; + type Signature = Signature; type Hasher = subxt::config::substrate::BlakeTwo256; type Header = subxt::config::substrate::SubstrateHeader; From d9f8398aa2abf612b0a805293831e0303c3313b3 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 27 Mar 2024 10:28:19 -0400 Subject: [PATCH 07/16] create internal verify in ecdsa and move to separate submodule --- signer/src/ecdsa.rs | 49 +++++++++++++++++++++++++++------------------ signer/src/eth.rs | 18 ++++++++--------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/signer/src/ecdsa.rs b/signer/src/ecdsa.rs index 4ab93228a0..16eb70e735 100644 --- a/signer/src/ecdsa.rs +++ b/signer/src/ecdsa.rs @@ -162,20 +162,10 @@ impl Keypair { pub fn sign(&self, message: &[u8]) -> Signature { let message_hash = sp_crypto_hashing::blake2_256(message); let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed"); - Signature(sign(&self.0.secret_key(), &wrapped)) + Signature(internal::sign(&self.0.secret_key(), &wrapped)) } } -pub(crate) fn sign(secret_key: &secp256k1::SecretKey, message: &Message) -> [u8; 65] { - let recsig: RecoverableSignature = - Secp256k1::signing_only().sign_ecdsa_recoverable(message, secret_key); - let (recid, sig): (_, [u8; 64]) = recsig.serialize_compact(); - let mut signature_bytes: [u8; 65] = [0; 65]; - signature_bytes[..64].copy_from_slice(&sig); - signature_bytes[64] = (recid.to_i32() & 0xFF) as u8; - signature_bytes -} - /// Verify that some signature for a message was created by the owner of the [`PublicKey`]. /// /// ```rust @@ -189,18 +179,37 @@ pub(crate) fn sign(secret_key: &secp256k1::SecretKey, message: &Message) -> [u8; /// assert!(ecdsa::verify(&signature, message, &public_key)); /// ``` pub fn verify>(sig: &Signature, message: M, pubkey: &PublicKey) -> bool { - let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig.0[..64]) else { - return false; - }; - let Ok(public) = secp256k1::PublicKey::from_slice(&pubkey.0) else { - return false; - }; let message_hash = sp_crypto_hashing::blake2_256(message.as_ref()); let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed"); - Secp256k1::verification_only() - .verify_ecdsa(&wrapped, &signature, &public) - .is_ok() + internal::verify(&sig.0, &wrapped, pubkey) +} + +pub(crate) mod internal { + use super::*; + + pub fn sign(secret_key: &secp256k1::SecretKey, message: &Message) -> [u8; 65] { + let recsig: RecoverableSignature = + Secp256k1::signing_only().sign_ecdsa_recoverable(message, secret_key); + let (recid, sig): (_, [u8; 64]) = recsig.serialize_compact(); + let mut signature_bytes: [u8; 65] = [0; 65]; + signature_bytes[..64].copy_from_slice(&sig); + signature_bytes[64] = (recid.to_i32() & 0xFF) as u8; + signature_bytes + } + + pub fn verify(sig: &[u8; 65], message: &Message, pubkey: &PublicKey) -> bool { + let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig[..64]) else { + return false; + }; + let Ok(public) = secp256k1::PublicKey::from_slice(&pubkey.0) else { + return false; + }; + + Secp256k1::verification_only() + .verify_ecdsa(message, &signature, &public) + .is_ok() + } } /// An error handed back if creating a keypair fails. diff --git a/signer/src/eth.rs b/signer/src/eth.rs index f33f42dd6e..989edaedde 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -6,7 +6,7 @@ use derive_more::{Display, From}; use keccak_hash::keccak; -use secp256k1::{Message, Secp256k1}; +use secp256k1::Message; use crate::crypto::{DeriveJunction, SecretUri}; use crate::ecdsa; @@ -119,7 +119,10 @@ impl Keypair { let message_hash = keccak(signer_payload); let wrapped = Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed"); - Signature(crate::ecdsa::sign(&self.0 .0.secret_key(), &wrapped)) + Signature(crate::ecdsa::internal::sign( + &self.0 .0.secret_key(), + &wrapped, + )) } } @@ -155,18 +158,12 @@ impl AsRef<[u8]> for AccountId20 { /// let public_key = keypair.public_key(); /// assert!(eth::verify(&signature, message, &public_key)); /// ``` -pub fn verify>(sig: &Signature, message: M, pub_key: &ecdsa::PublicKey) -> bool { - let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig.0[..64]) else { - return false; - }; +pub fn verify>(sig: &Signature, message: M, pubkey: &ecdsa::PublicKey) -> bool { let message_hash = keccak(message.as_ref()); let wrapped = Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed"); - let pub_key = secp256k1::PublicKey::from_slice(&pub_key.0).expect("valid public key"); - Secp256k1::verification_only() - .verify_ecdsa(&wrapped, &signature, &pub_key) - .is_ok() + ecdsa::internal::verify(&sig.0, &wrapped, pubkey) } /// An error handed back if creating the keypair fails. @@ -257,6 +254,7 @@ mod subxt_compat { mod test { use super::*; use proptest::prelude::*; + use secp256k1::Secp256k1; enum StubEthRuntimeConfig {} From c9e916c9098ef4cf85d76e4cb3f80d3867f7c338 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 27 Mar 2024 10:32:17 -0400 Subject: [PATCH 08/16] remove eth error struct --- signer/src/eth.rs | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/signer/src/eth.rs b/signer/src/eth.rs index 989edaedde..1c5aa4b2cd 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -4,7 +4,6 @@ //! An ethereum keypair implementation. -use derive_more::{Display, From}; use keccak_hash::keccak; use secp256k1::Message; @@ -41,9 +40,7 @@ impl Keypair { /// keypair.sign(b"Hello world!"); /// ``` pub fn from_uri(uri: &SecretUri) -> Result { - ecdsa::Keypair::from_uri(uri) - .map(Self) - .map_err(Error::Inner) + ecdsa::Keypair::from_uri(uri).map(Self) } /// Create a keypair from a BIP-39 mnemonic phrase and optional password. @@ -60,9 +57,7 @@ impl Keypair { /// keypair.sign(b"Hello world!"); /// ``` pub fn from_phrase(mnemonic: &bip39::Mnemonic, password: Option<&str>) -> Result { - ecdsa::Keypair::from_phrase(mnemonic, password) - .map(Self) - .map_err(Error::Inner) + ecdsa::Keypair::from_phrase(mnemonic, password).map(Self) } /// Turn a 32 byte seed into a keypair. @@ -71,9 +66,7 @@ impl Keypair { /// /// This will only be secure if the seed is secure! pub fn from_seed(seed: Seed) -> Result { - ecdsa::Keypair::from_seed(seed) - .map(Self) - .map_err(Error::Inner) + ecdsa::Keypair::from_seed(seed).map(Self) } /// Derive a child key from this one given a series of junctions. @@ -97,7 +90,7 @@ impl Keypair { &self, junctions: Js, ) -> Result { - self.0.derive(junctions).map(Self).map_err(Error::Inner) + self.0.derive(junctions).map(Self) } /// Obtain the [`ecdsa::PublicKey`] of this keypair. @@ -167,22 +160,7 @@ pub fn verify>(sig: &Signature, message: M, pubkey: &ecdsa::Publi } /// An error handed back if creating the keypair fails. -#[derive(Debug, PartialEq, Display, From)] -pub enum Error { - /// Invalid private key. - #[display(fmt = "Invalid private key")] - #[from(ignore)] - InvalidPrivateKey, - /// Invalid hex. - #[display(fmt = "Cannot parse hex string: {_0}")] - Hex(hex::FromHexError), - /// Inner, - #[display(fmt = "{_0}")] - Inner(ecdsa::Error), -} - -#[cfg(feature = "std")] -impl std::error::Error for Error {} +pub type Error = ecdsa::Error; /// Dev accounts, helpful for testing but not to be used in production, /// since the secret keys are known. From bf514d8ce96644dd0e984c0ffee2628b41c67161 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 27 Mar 2024 23:21:27 -0400 Subject: [PATCH 09/16] fix dev accounts and account derivation --- Cargo.lock | 174 +++++++++++++++++++++++++++++++++++----- Cargo.toml | 2 + signer/Cargo.toml | 4 +- signer/src/eth.rs | 196 ++++++++++++++++++++++++---------------------- 4 files changed, 261 insertions(+), 115 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b96d7124f0..0b321e31e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -571,6 +571,22 @@ dependencies = [ "serde", ] +[[package]] +name = "bip32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e141fb0f8be1c7b45887af94c88b182472b57c96b56773250ae00cd6a14a164" +dependencies = [ + "bs58", + "hmac 0.12.1", + "k256", + "rand_core 0.6.4", + "ripemd", + "sha2 0.10.8", + "subtle", + "zeroize", +] + [[package]] name = "bip39" version = "2.0.0" @@ -725,6 +741,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" dependencies = [ + "sha2 0.10.8", "tinyvec", ] @@ -1501,6 +1518,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "eth_checksum" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0451da8f79bc717a33d8edd5f11b8fc7d510fd0a3b14f78361ff647f677da0d0" +dependencies = [ + "rust-crypto", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1607,7 +1633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -1659,6 +1685,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "2.0.0" @@ -1778,6 +1810,12 @@ dependencies = [ "slab", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "generate-custom-metadata" version = "0.35.0" @@ -1807,7 +1845,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1817,7 +1855,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" dependencies = [ - "rand", + "rand 0.8.5", "rand_core 0.6.4", ] @@ -2510,7 +2548,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand", + "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", @@ -2667,7 +2705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2841,8 +2879,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e69bf016dc406eff7d53a7d3f7cf1c2e72c82b9088aac1118591e36dd2cd3e9" dependencies = [ "bitcoin_hashes 0.13.0", - "rand", - "rand_core 0.5.1", + "rand 0.8.5", + "rand_core 0.6.4", "serde", "unicode-normalization", ] @@ -3229,7 +3267,7 @@ dependencies = [ "bitflags 2.4.2", "lazy_static", "num-traits", - "rand", + "rand 0.8.5", "rand_chacha", "rand_xorshift", "regex-syntax 0.8.2", @@ -3268,6 +3306,29 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -3289,6 +3350,21 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.5.1" @@ -3333,6 +3409,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "reconnecting-jsonrpsee-ws-client" version = "0.3.0" @@ -3446,6 +3531,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +dependencies = [ + "gcc", + "libc", + "rand 0.3.23", + "rustc-serialize", + "time", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3464,6 +3571,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc-serialize" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" + [[package]] name = "rustc_version" version = "0.4.0" @@ -3767,7 +3880,7 @@ dependencies = [ "peekmore", "proc-macro2", "quote", - "rand", + "rand 0.8.5", "rand_chacha", "scale-info", "scale-typegen", @@ -4149,7 +4262,7 @@ dependencies = [ "pbkdf2", "pin-project", "poly1305", - "rand", + "rand 0.8.5", "rand_chacha", "ruzstd", "schnorrkel", @@ -4192,7 +4305,7 @@ dependencies = [ "no-std-net", "parking_lot", "pin-project", - "rand", + "rand 0.8.5", "rand_chacha", "serde", "serde_json", @@ -4224,7 +4337,7 @@ dependencies = [ "futures", "httparse", "log", - "rand", + "rand 0.8.5", "sha-1", ] @@ -4284,7 +4397,7 @@ dependencies = [ "parking_lot", "paste", "primitive-types", - "rand", + "rand 0.8.5", "scale-info", "schnorrkel", "secp256k1", @@ -4415,7 +4528,7 @@ dependencies = [ "log", "parity-scale-codec", "paste", - "rand", + "rand 0.8.5", "scale-info", "serde", "simple-mermaid", @@ -4471,7 +4584,7 @@ dependencies = [ "log", "parity-scale-codec", "parking_lot", - "rand", + "rand 0.8.5", "smallvec", "sp-core", "sp-externalities", @@ -4529,7 +4642,7 @@ dependencies = [ "nohash-hasher", "parity-scale-codec", "parking_lot", - "rand", + "rand 0.8.5", "scale-info", "schnellru", "sp-core", @@ -4830,9 +4943,11 @@ dependencies = [ name = "subxt-signer" version = "0.35.0" dependencies = [ + "bip32", "bip39", "cfg-if", "derive_more", + "eth_checksum", "getrandom", "hex", "hmac 0.12.1", @@ -4959,6 +5074,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -5028,7 +5154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" dependencies = [ "pin-project", - "rand", + "rand 0.8.5", "tokio", ] @@ -5335,7 +5461,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", "digest 0.10.7", - "rand", + "rand 0.8.5", "static_assertions", ] @@ -5484,7 +5610,7 @@ dependencies = [ "arrayref", "constcat", "digest 0.10.7", - "rand", + "rand 0.8.5", "rand_chacha", "rand_core 0.6.4", "sha2 0.10.8", @@ -5544,6 +5670,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5778,7 +5910,7 @@ dependencies = [ "memfd", "memoffset", "paste", - "rand", + "rand 0.8.5", "rustix 0.36.17", "wasmtime-asm-macros", "wasmtime-environ", diff --git a/Cargo.toml b/Cargo.toml index a9703a56af..65886ab7f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,6 +147,7 @@ substrate-runner = { path = "testing/substrate-runner" } # subxt-signer deps that I expect aren't useful anywhere else: bip39 = { version = "2.0.0", default-features = false } +bip32 = { version = "0.5.1", default-features = false } hmac = { version = "0.12.1", default-features = false } pbkdf2 = { version = "0.12.2", default-features = false } schnorrkel = { version = "0.11.4", default-features = false } @@ -155,6 +156,7 @@ keccak-hash = { version = "0.10.0", default-features = false } secrecy = "0.8.0" sha2 = { version = "0.10.8", default-features = false } zeroize = { version = "1", default-features = false } +eth_checksum = { version = "0.1.2", default-features = false } [profile.dev.package.smoldot-light] opt-level = 2 diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 072a1312d0..079a527bc2 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -24,7 +24,7 @@ std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std # https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699 sr25519 = ["schnorrkel"] ecdsa = ["secp256k1"] -eth = ["keccak-hash", "secp256k1"] +eth = ["keccak-hash", "secp256k1", "bip32", "eth_checksum"] # Make the keypair algorithms here compatible with Subxt's Signer trait, # so that they can be used to sign transactions for compatible chains. @@ -48,10 +48,12 @@ sha2 = { workspace = true } hmac = { workspace = true } zeroize = { workspace = true } bip39 = { workspace = true } +bip32 = { workspace = true, features = ["alloc", "secp256k1"], optional = true } schnorrkel = { workspace = true, optional = true } secp256k1 = { workspace = true, optional = true, features = ["alloc", "recovery"] } secrecy = { workspace = true } keccak-hash = { workspace = true, optional = true } +eth_checksum = { workspace = true, optional = true } # We only pull this in to enable the JS flag for schnorrkel to use. getrandom = { workspace = true, optional = true } diff --git a/signer/src/eth.rs b/signer/src/eth.rs index 1c5aa4b2cd..d6a59dc43b 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -4,10 +4,12 @@ //! An ethereum keypair implementation. +use core::fmt::{Display, Formatter}; + +use derive_more::{Display, From}; use keccak_hash::keccak; use secp256k1::Message; -use crate::crypto::{DeriveJunction, SecretUri}; use crate::ecdsa; const SEED_LENGTH: usize = 32; @@ -26,24 +28,7 @@ impl From for Keypair { } impl Keypair { - /// Create a keypair from a [`SecretUri`]. See the [`SecretUri`] docs for more. - /// - /// # Example - /// - /// ```rust - /// use subxt_signer::{ SecretUri, eth::Keypair }; - /// use std::str::FromStr; - /// - /// let uri = SecretUri::from_str("//Alice").unwrap(); - /// let keypair = Keypair::from_uri(&uri).unwrap(); - /// - /// keypair.sign(b"Hello world!"); - /// ``` - pub fn from_uri(uri: &SecretUri) -> Result { - ecdsa::Keypair::from_uri(uri).map(Self) - } - - /// Create a keypair from a BIP-39 mnemonic phrase and optional password. + /// Create a keypair from a BIP-39 mnemonic phrase, optional password, and derivation index. /// /// # Example /// @@ -52,12 +37,25 @@ impl Keypair { /// /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; /// let mnemonic = Mnemonic::parse(phrase).unwrap(); - /// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap(); + /// let keypair = Keypair::from_phrase(&mnemonic, None, 0).unwrap(); /// /// keypair.sign(b"Hello world!"); /// ``` - pub fn from_phrase(mnemonic: &bip39::Mnemonic, password: Option<&str>) -> Result { - ecdsa::Keypair::from_phrase(mnemonic, password).map(Self) + pub fn from_phrase( + mnemonic: &bip39::Mnemonic, + password: Option<&str>, + index: u32, + ) -> Result { + let derivation_path: bip32::DerivationPath = format!("m/44'/60'/0'/0/{}", index) + .parse() + .map_err(Error::InvalidDerivationIndex)?; + let private = bip32::XPrv::derive_from_path( + mnemonic.to_seed(password.unwrap_or("")), + &derivation_path, + ) + .unwrap(); + + Keypair::from_seed(private.to_bytes()) } /// Turn a 32 byte seed into a keypair. @@ -66,31 +64,9 @@ impl Keypair { /// /// This will only be secure if the seed is secure! pub fn from_seed(seed: Seed) -> Result { - ecdsa::Keypair::from_seed(seed).map(Self) - } - - /// Derive a child key from this one given a series of junctions. - /// - /// # Example - /// - /// ```rust - /// use subxt_signer::{ bip39::Mnemonic, eth::Keypair, DeriveJunction }; - /// - /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; - /// let mnemonic = Mnemonic::parse(phrase).unwrap(); - /// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap(); - /// - /// // Equivalent to the URI path '//Alice//stash': - /// let new_keypair = keypair.derive([ - /// DeriveJunction::hard("Alice"), - /// DeriveJunction::hard("stash") - /// ]); - /// ``` - pub fn derive>( - &self, - junctions: Js, - ) -> Result { - self.0.derive(junctions).map(Self) + ecdsa::Keypair::from_seed(seed) + .map(Self) + .map_err(|_| Error::InvalidSeed) } /// Obtain the [`ecdsa::PublicKey`] of this keypair. @@ -112,10 +88,7 @@ impl Keypair { let message_hash = keccak(signer_payload); let wrapped = Message::from_digest_slice(message_hash.as_bytes()).expect("Message is 32 bytes; qed"); - Signature(crate::ecdsa::internal::sign( - &self.0 .0.secret_key(), - &wrapped, - )) + Signature(ecdsa::internal::sign(&self.0 .0.secret_key(), &wrapped)) } } @@ -139,18 +112,12 @@ impl AsRef<[u8]> for AccountId20 { } } -/// Verify that some signature for a message was created by the owner of the [`ecdsa::PublicKey`]. -/// -/// ```rust -/// use subxt_signer::eth; -/// -/// let keypair = eth::dev::alice(); -/// let message = b"Hello!"; -/// -/// let signature = keypair.sign(message); -/// let public_key = keypair.public_key(); -/// assert!(eth::verify(&signature, message, &public_key)); -/// ``` +impl Display for AccountId20 { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", eth_checksum::checksum(&hex::encode(&self))) + } +} + pub fn verify>(sig: &Signature, message: M, pubkey: &ecdsa::PublicKey) -> bool { let message_hash = keccak(message.as_ref()); let wrapped = @@ -159,47 +126,57 @@ pub fn verify>(sig: &Signature, message: M, pubkey: &ecdsa::Publi ecdsa::internal::verify(&sig.0, &wrapped, pubkey) } -/// An error handed back if creating the keypair fails. -pub type Error = ecdsa::Error; +/// An error handed back if creating a keypair fails. +#[derive(Debug, PartialEq, Display, From)] +pub enum Error { + /// Invalid seed. + #[display(fmt = "Invalid seed (was it the wrong length?)")] + #[from(ignore)] + InvalidSeed, + /// Invalid derivation index. + #[display(fmt = "Invalid derivation index: {_0}")] + InvalidDerivationIndex(bip32::Error), + /// Invalid phrase. + #[display(fmt = "Cannot parse phrase: {_0}")] + InvalidPhrase(bip39::Error), +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} /// Dev accounts, helpful for testing but not to be used in production, /// since the secret keys are known. pub mod dev { - use super::*; use core::str::FromStr; + use crate::DEV_PHRASE; + + use super::*; + once_static_cloned! { - /// Equivalent to `{DEV_PHRASE}//Alice`. - pub fn alice() -> Keypair { - Keypair::from_uri(&SecretUri::from_str("//Alice").unwrap()).unwrap() + pub fn alith() -> Keypair { + Keypair::from_phrase( + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 0).unwrap() } - /// Equivalent to `{DEV_PHRASE}//Bob`. - pub fn bob() -> Keypair { - Keypair::from_uri(&SecretUri::from_str("//Bob").unwrap()).unwrap() + pub fn baltathar() -> Keypair { + Keypair::from_phrase( + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 1).unwrap() } - /// Equivalent to `{DEV_PHRASE}//Charlie`. - pub fn charlie() -> Keypair { - Keypair::from_uri(&SecretUri::from_str("//Charlie").unwrap()).unwrap() + pub fn charleth() -> Keypair { + Keypair::from_phrase( + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 2).unwrap() } - /// Equivalent to `{DEV_PHRASE}//Dave`. - pub fn dave() -> Keypair { - Keypair::from_uri(&SecretUri::from_str("//Dave").unwrap()).unwrap() + pub fn dorothy() -> Keypair { + Keypair::from_phrase( + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 3).unwrap() } - /// Equivalent to `{DEV_PHRASE}//Eve`. - pub fn eve() -> Keypair { - Keypair::from_uri(&SecretUri::from_str("//Eve").unwrap()).unwrap() + pub fn ethan() -> Keypair { + Keypair::from_phrase( + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 4).unwrap() } - /// Equivalent to `{DEV_PHRASE}//Ferdie`. - pub fn ferdie() -> Keypair { - Keypair::from_uri(&SecretUri::from_str("//Ferdie").unwrap()).unwrap() - } - /// Equivalent to `{DEV_PHRASE}//One`. - pub fn one() -> Keypair { - Keypair::from_uri(&SecretUri::from_str("//One").unwrap()).unwrap() - } - /// Equivalent to `{DEV_PHRASE}//Two`. - pub fn two() -> Keypair { - Keypair::from_uri(&SecretUri::from_str("//Two").unwrap()).unwrap() + pub fn faith() -> Keypair { + Keypair::from_phrase( + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 5).unwrap() } } } @@ -230,10 +207,15 @@ mod subxt_compat { #[cfg(test)] mod test { - use super::*; + use core::str::FromStr; + use proptest::prelude::*; use secp256k1::Secp256k1; + use crate::DEV_PHRASE; + + use super::*; + enum StubEthRuntimeConfig {} impl subxt::Config for StubEthRuntimeConfig { @@ -300,4 +282,32 @@ mod test { ); } } + + #[test] + fn check_dev_accounts_match() { + assert_eq!( + dev::alith().account_id().to_string(), + eth_checksum::checksum("0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac") + ); + assert_eq!( + dev::baltathar().account_id().to_string(), + eth_checksum::checksum("0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0") + ); + assert_eq!( + dev::charleth().account_id().to_string(), + eth_checksum::checksum("0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc") + ); + assert_eq!( + dev::dorothy().account_id().to_string(), + eth_checksum::checksum("0x773539d4Ac0e786233D90A233654ccEE26a613D9") + ); + assert_eq!( + dev::ethan().account_id().to_string(), + eth_checksum::checksum("0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB") + ); + assert_eq!( + dev::faith().account_id().to_string(), + eth_checksum::checksum("0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d") + ); + } } From 15a6492ba8eb86a77c52dc9258d901f6d6b303e5 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Wed, 27 Mar 2024 23:49:06 -0400 Subject: [PATCH 10/16] fix clippy --- signer/src/eth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signer/src/eth.rs b/signer/src/eth.rs index 47ca648b91..82f968646c 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -114,7 +114,7 @@ impl AsRef<[u8]> for AccountId20 { impl Display for AccountId20 { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", eth_checksum::checksum(&hex::encode(&self))) + write!(f, "{}", eth_checksum::checksum(&hex::encode(self))) } } From d4018a65bf304ef2c47a3e3c4ccd9e7d5228226a Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 28 Mar 2024 01:24:35 -0400 Subject: [PATCH 11/16] moar tests --- Cargo.lock | 7 +++ Cargo.toml | 1 + signer/Cargo.toml | 1 + signer/src/eth.rs | 132 ++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 125 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ec720ff13..e4ac931cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2068,6 +2068,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hmac" version = "0.8.1" @@ -4991,6 +4997,7 @@ dependencies = [ "eth_checksum", "getrandom", "hex", + "hex-literal", "hmac 0.12.1", "keccak-hash", "parity-scale-codec", diff --git a/Cargo.toml b/Cargo.toml index 8c740439b6..83435bb729 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,7 @@ wasm-bindgen-test = "0.3.24" which = "5.0.0" strip-ansi-escapes = "0.2.0" proptest = "1.4.0" +hex-literal = "0.4.1" # Light client support: smoldot = { version = "0.16.0", default-features = false } diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 41e63e592b..1a703fe138 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -60,6 +60,7 @@ getrandom = { workspace = true, optional = true } [dev-dependencies] sp-keyring = { workspace = true } proptest = { workspace = true } +hex-literal = { workspace = true } sp-core = { workspace = true } [package.metadata.cargo-machete] diff --git a/signer/src/eth.rs b/signer/src/eth.rs index 82f968646c..0ad6d3b8ee 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -28,7 +28,8 @@ impl From for Keypair { } impl Keypair { - /// Create a keypair from a BIP-39 mnemonic phrase, optional password, and derivation index. + /// Create a keypair from a BIP-39 mnemonic phrase, optional password, account index, and + /// derivation type. /// /// # Example /// @@ -37,7 +38,7 @@ impl Keypair { /// /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; /// let mnemonic = Mnemonic::parse(phrase).unwrap(); - /// let keypair = Keypair::from_phrase(&mnemonic, None, 0).unwrap(); + /// let keypair = Keypair::from_phrase(&mnemonic, None, 0, Default::default()).unwrap(); /// /// keypair.sign(b"Hello world!"); /// ``` @@ -45,10 +46,15 @@ impl Keypair { mnemonic: &bip39::Mnemonic, password: Option<&str>, index: u32, + derivation: Derivation, ) -> Result { - let derivation_path: bip32::DerivationPath = format!("m/44'/60'/0'/0/{}", index) - .parse() - .map_err(Error::InvalidDerivationIndex)?; + let derivation_path = match derivation { + Derivation::Hard => format!("m/44'/60'/{}'/0/0", index), + Derivation::Soft => format!("m/44'/60'/0'/0/{}", index), + } + .parse() + .map_err(Error::InvalidDerivationIndex)?; + let private = bip32::XPrv::derive_from_path( mnemonic.to_seed(password.unwrap_or("")), &derivation_path, @@ -92,6 +98,18 @@ impl Keypair { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Derivation { + Hard, + Soft, +} + +impl Default for Derivation { + fn default() -> Self { + Self::Soft + } +} + /// A signature generated by [`Keypair::sign()`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, codec::Encode)] pub struct Signature(pub [u8; 65]); @@ -156,37 +174,38 @@ pub mod dev { once_static_cloned! { pub fn alith() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 0).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 0, Derivation::Soft).unwrap() } pub fn baltathar() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 1).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 1, Derivation::Soft).unwrap() } pub fn charleth() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 2).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 2, Derivation::Soft).unwrap() } pub fn dorothy() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 3).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 3, Derivation::Soft).unwrap() } pub fn ethan() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 4).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 4, Derivation::Soft).unwrap() } pub fn faith() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 5).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 5, Derivation::Soft).unwrap() } } } #[cfg(feature = "subxt")] mod subxt_compat { - use super::*; - use subxt_core::config::Config; use subxt_core::tx::Signer as SignerT; + + use super::*; + impl SignerT for Keypair where T::AccountId: From, @@ -209,12 +228,9 @@ mod subxt_compat { #[cfg(test)] mod test { - use core::str::FromStr; - use proptest::prelude::*; use secp256k1::Secp256k1; - use crate::DEV_PHRASE; use subxt_core::{config::*, tx::Signer as SignerT, utils::H256}; use super::*; @@ -234,6 +250,10 @@ mod test { type SubxtSigner = dyn SignerT; + fn derivation_strategy() -> impl Strategy { + prop_oneof![Just(Derivation::Hard), Just(Derivation::Soft),] + } + prop_compose! { fn keypair()(seed in any::<[u8; 32]>()) -> Keypair { let secret = secp256k1::SecretKey::from_slice(&seed).expect("valid secret key"); @@ -247,6 +267,44 @@ mod test { } proptest! { + #[test] + fn check_from_phrase( + entropy in any::<[u8; 32]>(), + password in any::>(), + index in 1..(i32::MAX as u32), + derivation in derivation_strategy() + ) { + let mnemonic = bip39::Mnemonic::from_entropy(&entropy).expect("valid mnemonic"); + let derivation_path = match derivation { + Derivation::Hard => format!("m/44'/60'/{}'/0/0", index), + Derivation::Soft => format!("m/44'/60'/0'/0/{}", index), + }.parse().expect("valid derivation path"); + let private = bip32::XPrv::derive_from_path( + mnemonic.to_seed(password.clone().unwrap_or("".to_string())), + &derivation_path, + ).expect("valid private"); + + assert_eq!( + Keypair::from_phrase(&mnemonic, password.as_deref(), index, derivation).expect("valid keypair"), + Keypair(ecdsa::Keypair::from_seed(private.to_bytes()).expect("valid ecdsa keypair")) + ); + } + + #[test] + fn check_from_phrase_bad_index( + entropy in any::<[u8; 32]>(), + password in any::>(), + index in (i32::MAX as u32)..=u32::MAX, + derivation in derivation_strategy() + ) { + let mnemonic = bip39::Mnemonic::from_entropy(&entropy).expect("valid mnemonic"); + + assert_eq!( + Keypair::from_phrase(&mnemonic, password.as_deref(), index, derivation).expect_err("bad index"), + Error::InvalidDerivationIndex(bip32::Error::ChildNumber) + ); + } + #[test] fn check_subxt_signer_implementation_matches(keypair in keypair(), msg in ".*") { let msg_as_bytes = msg.as_bytes(); @@ -312,4 +370,46 @@ mod test { eth_checksum::checksum("0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d") ); } + + /// Test the same accounts from moonbeam so we know for sure that this implementation is working + /// https://github.com/moonbeam-foundation/moonbeam/blob/e70ee0d427dfee8987d5a5671a66416ee6ec38aa/primitives/account/src/lib.rs#L217 + #[cfg(test)] + mod moonbeam_sanity_tests { + use hex_literal::hex; + + use super::*; + + const KEY_1: [u8; 32] = + hex!("502f97299c472b88754accd412b7c9a6062ef3186fba0c0388365e1edec24875"); + const KEY_2: [u8; 32] = + hex!("0f02ba4d7f83e59eaa32eae9c3c4d99b68ce76decade21cdab7ecce8f4aef81a"); + const KEY_3: [u8; 32] = + hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + #[test] + fn test_account_derivation_1() { + let kp = Keypair::from_seed(KEY_1).expect("valid keypair"); + assert_eq!( + kp.account_id().to_string(), + eth_checksum::checksum("0x976f8456e4e2034179b284a23c0e0c8f6d3da50c") + ); + } + + #[test] + fn test_account_derivation_2() { + let kp = Keypair::from_seed(KEY_2).expect("valid keypair"); + assert_eq!( + kp.account_id().to_string(), + eth_checksum::checksum("0x420e9f260b40af7e49440cead3069f8e82a5230f") + ); + } + + #[test] + fn test_account_derivation_3() { + let kp = Keypair::from_seed(KEY_3).expect("valid keypair"); + assert_eq!( + kp.account_id().to_string(), + eth_checksum::checksum("0x9cce34F7aB185c7ABA1b7C8140d620B4BDA941d6") + ); + } + } } From 22e2a21825f121b3d9c10e8bfa560e6ffeb81ab1 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 28 Mar 2024 01:41:18 -0400 Subject: [PATCH 12/16] fixes --- signer/src/eth.rs | 3 ++- signer/wasm-tests/tests/wasm.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/signer/src/eth.rs b/signer/src/eth.rs index 0ad6d3b8ee..7e6e8d3a08 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -59,7 +59,7 @@ impl Keypair { mnemonic.to_seed(password.unwrap_or("")), &derivation_path, ) - .unwrap(); + .expect("valid private key"); Keypair::from_seed(private.to_bytes()) } @@ -315,6 +315,7 @@ mod test { #[test] fn check_account_id(keypair in keypair()) { + // https://github.com/ethereumbook/ethereumbook/blob/develop/04keys-addresses.asciidoc#ethereum-addresses let account_id = { let uncompressed = keypair.0.0.public_key().serialize_uncompressed(); let hash = keccak(&uncompressed[1..]).0; diff --git a/signer/wasm-tests/tests/wasm.rs b/signer/wasm-tests/tests/wasm.rs index 53f08ab292..667f20a55c 100644 --- a/signer/wasm-tests/tests/wasm.rs +++ b/signer/wasm-tests/tests/wasm.rs @@ -45,7 +45,7 @@ async fn wasm_ecdsa_signing_works() { #[wasm_bindgen_test] async fn wasm_eth_signing_works() { - let alice = eth::dev::alice(); + let alice = eth::dev::alith(); // There's some non-determinism in the signing, so this ensures that // the rand stuff is configured properly to run ok in wasm. From 1852a5b30c9c64d54ac3873bb785d45210c4e1ea Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Thu, 28 Mar 2024 09:38:51 -0400 Subject: [PATCH 13/16] replace eth checksum depedency --- Cargo.lock | 146 ++++++++-------------------------------------- Cargo.toml | 2 +- signer/Cargo.toml | 4 +- signer/src/eth.rs | 20 +++---- 4 files changed, 37 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4ac931cbd..11dfbb9366 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand", ] [[package]] @@ -1530,13 +1530,10 @@ dependencies = [ ] [[package]] -name = "eth_checksum" -version = "0.1.2" +name = "ethaddr" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0451da8f79bc717a33d8edd5f11b8fc7d510fd0a3b14f78361ff647f677da0d0" -dependencies = [ - "rust-crypto", -] +checksum = "ecbcc1770d1b2e3fb83915c80c47a10efbe1964544a40e0211fe40274f8363c8" [[package]] name = "event-listener" @@ -1644,7 +1641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand 0.8.5", + "rand", "rustc-hex", "static_assertions", ] @@ -1696,12 +1693,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - [[package]] name = "funty" version = "2.0.0" @@ -1821,12 +1812,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gcc" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" - [[package]] name = "generate-custom-metadata" version = "0.35.0" @@ -1856,7 +1841,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -1866,7 +1851,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" dependencies = [ - "rand 0.8.5", + "rand", "rand_core 0.6.4", ] @@ -2565,7 +2550,7 @@ dependencies = [ "libsecp256k1-core", "libsecp256k1-gen-ecmult", "libsecp256k1-gen-genmult", - "rand 0.8.5", + "rand", "serde", "sha2 0.9.9", "typenum", @@ -2722,7 +2707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] @@ -2896,7 +2881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e69bf016dc406eff7d53a7d3f7cf1c2e72c82b9088aac1118591e36dd2cd3e9" dependencies = [ "bitcoin_hashes 0.13.0", - "rand 0.8.5", + "rand", "rand_core 0.6.4", "serde", "unicode-normalization", @@ -3284,7 +3269,7 @@ dependencies = [ "bitflags 2.4.2", "lazy_static", "num-traits", - "rand 0.8.5", + "rand", "rand_chacha", "rand_xorshift", "regex-syntax 0.8.2", @@ -3323,29 +3308,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" -dependencies = [ - "libc", - "rand 0.4.6", -] - -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.8.5" @@ -3367,21 +3329,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.5.1" @@ -3426,15 +3373,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "reconnecting-jsonrpsee-ws-client" version = "0.3.0" @@ -3557,19 +3495,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "rust-crypto" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" -dependencies = [ - "gcc", - "libc", - "rand 0.3.23", - "rustc-serialize", - "time", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3588,12 +3513,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" -[[package]] -name = "rustc-serialize" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" - [[package]] name = "rustc_version" version = "0.4.0" @@ -3898,7 +3817,7 @@ dependencies = [ "peekmore", "proc-macro2", "quote", - "rand 0.8.5", + "rand", "rand_chacha", "scale-info", "scale-typegen", @@ -4280,7 +4199,7 @@ dependencies = [ "pbkdf2", "pin-project", "poly1305", - "rand 0.8.5", + "rand", "rand_chacha", "ruzstd", "schnorrkel", @@ -4323,7 +4242,7 @@ dependencies = [ "no-std-net", "parking_lot", "pin-project", - "rand 0.8.5", + "rand", "rand_chacha", "serde", "serde_json", @@ -4355,7 +4274,7 @@ dependencies = [ "futures", "httparse", "log", - "rand 0.8.5", + "rand", "sha-1", ] @@ -4415,7 +4334,7 @@ dependencies = [ "parking_lot", "paste", "primitive-types", - "rand 0.8.5", + "rand", "scale-info", "schnorrkel", "secp256k1", @@ -4546,7 +4465,7 @@ dependencies = [ "log", "parity-scale-codec", "paste", - "rand 0.8.5", + "rand", "scale-info", "serde", "simple-mermaid", @@ -4602,7 +4521,7 @@ dependencies = [ "log", "parity-scale-codec", "parking_lot", - "rand 0.8.5", + "rand", "smallvec", "sp-core", "sp-externalities", @@ -4660,7 +4579,7 @@ dependencies = [ "nohash-hasher", "parity-scale-codec", "parking_lot", - "rand 0.8.5", + "rand", "scale-info", "schnellru", "sp-core", @@ -4994,7 +4913,7 @@ dependencies = [ "bip39", "cfg-if", "derive_more", - "eth_checksum", + "ethaddr", "getrandom", "hex", "hex-literal", @@ -5122,17 +5041,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "tiny-keccak" version = "2.0.2" @@ -5202,7 +5110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" dependencies = [ "pin-project", - "rand 0.8.5", + "rand", "tokio", ] @@ -5509,7 +5417,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", "digest 0.10.7", - "rand 0.8.5", + "rand", "static_assertions", ] @@ -5658,7 +5566,7 @@ dependencies = [ "arrayref", "constcat", "digest 0.10.7", - "rand 0.8.5", + "rand", "rand_chacha", "rand_core 0.6.4", "sha2 0.10.8", @@ -5718,12 +5626,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5958,7 +5860,7 @@ dependencies = [ "memfd", "memoffset", "paste", - "rand 0.8.5", + "rand", "rustix 0.36.17", "wasmtime-asm-macros", "wasmtime-environ", diff --git a/Cargo.toml b/Cargo.toml index 83435bb729..5ee07dea6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,7 +159,7 @@ keccak-hash = { version = "0.10.0", default-features = false } secrecy = "0.8.0" sha2 = { version = "0.10.8", default-features = false } zeroize = { version = "1", default-features = false } -eth_checksum = { version = "0.1.2", default-features = false } +ethaddr = { version = "0.2.2", default-features = false } [profile.dev.package.smoldot-light] opt-level = 2 diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 1a703fe138..625c8b96bf 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -24,7 +24,7 @@ std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std # https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699 sr25519 = ["schnorrkel"] ecdsa = ["secp256k1"] -eth = ["keccak-hash", "secp256k1", "bip32", "eth_checksum"] +eth = ["keccak-hash", "secp256k1", "bip32", "ethaddr"] # Make the keypair algorithms here compatible with Subxt's Signer trait, # so that they can be used to sign transactions for compatible chains. @@ -52,7 +52,7 @@ bip32 = { workspace = true, features = ["alloc", "secp256k1"], optional = true } schnorrkel = { workspace = true, optional = true } secp256k1 = { workspace = true, optional = true, features = ["alloc", "recovery"] } keccak-hash = { workspace = true, optional = true } -eth_checksum = { workspace = true, optional = true } +ethaddr = { workspace = true, optional = true } # We only pull this in to enable the JS flag for schnorrkel to use. getrandom = { workspace = true, optional = true } diff --git a/signer/src/eth.rs b/signer/src/eth.rs index 7e6e8d3a08..5488031852 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -132,7 +132,7 @@ impl AsRef<[u8]> for AccountId20 { impl Display for AccountId20 { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", eth_checksum::checksum(&hex::encode(self))) + write!(f, "{}", ethaddr::Address::from_slice(&self.0)) } } @@ -348,27 +348,27 @@ mod test { fn check_dev_accounts_match() { assert_eq!( dev::alith().account_id().to_string(), - eth_checksum::checksum("0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac") + "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac" ); assert_eq!( dev::baltathar().account_id().to_string(), - eth_checksum::checksum("0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0") + "0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0" ); assert_eq!( dev::charleth().account_id().to_string(), - eth_checksum::checksum("0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc") + "0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc" ); assert_eq!( dev::dorothy().account_id().to_string(), - eth_checksum::checksum("0x773539d4Ac0e786233D90A233654ccEE26a613D9") + "0x773539d4Ac0e786233D90A233654ccEE26a613D9" ); assert_eq!( dev::ethan().account_id().to_string(), - eth_checksum::checksum("0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB") + "0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB" ); assert_eq!( dev::faith().account_id().to_string(), - eth_checksum::checksum("0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d") + "0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d" ); } @@ -391,7 +391,7 @@ mod test { let kp = Keypair::from_seed(KEY_1).expect("valid keypair"); assert_eq!( kp.account_id().to_string(), - eth_checksum::checksum("0x976f8456e4e2034179b284a23c0e0c8f6d3da50c") + "0x976f8456E4e2034179B284A23C0e0c8f6d3da50c" ); } @@ -400,7 +400,7 @@ mod test { let kp = Keypair::from_seed(KEY_2).expect("valid keypair"); assert_eq!( kp.account_id().to_string(), - eth_checksum::checksum("0x420e9f260b40af7e49440cead3069f8e82a5230f") + "0x420e9F260B40aF7E49440ceAd3069f8e82A5230f" ); } @@ -409,7 +409,7 @@ mod test { let kp = Keypair::from_seed(KEY_3).expect("valid keypair"); assert_eq!( kp.account_id().to_string(), - eth_checksum::checksum("0x9cce34F7aB185c7ABA1b7C8140d620B4BDA941d6") + "0x9cce34F7aB185c7ABA1b7C8140d620B4BDA941d6" ); } } From 89931d53ea9b46042534ef5dfe65c27c87c4c33c Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Sun, 31 Mar 2024 22:44:02 -0400 Subject: [PATCH 14/16] add checksum implementation --- Cargo.lock | 7 ------- Cargo.toml | 1 - signer/Cargo.toml | 3 +-- signer/src/eth.rs | 26 +++++++++++++++++++++++++- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11dfbb9366..54cbafebd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1529,12 +1529,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ethaddr" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecbcc1770d1b2e3fb83915c80c47a10efbe1964544a40e0211fe40274f8363c8" - [[package]] name = "event-listener" version = "2.5.3" @@ -4913,7 +4907,6 @@ dependencies = [ "bip39", "cfg-if", "derive_more", - "ethaddr", "getrandom", "hex", "hex-literal", diff --git a/Cargo.toml b/Cargo.toml index 5ee07dea6d..207fde4f36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,7 +159,6 @@ keccak-hash = { version = "0.10.0", default-features = false } secrecy = "0.8.0" sha2 = { version = "0.10.8", default-features = false } zeroize = { version = "1", default-features = false } -ethaddr = { version = "0.2.2", default-features = false } [profile.dev.package.smoldot-light] opt-level = 2 diff --git a/signer/Cargo.toml b/signer/Cargo.toml index 625c8b96bf..dca475008a 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -24,7 +24,7 @@ std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std # https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699 sr25519 = ["schnorrkel"] ecdsa = ["secp256k1"] -eth = ["keccak-hash", "secp256k1", "bip32", "ethaddr"] +eth = ["keccak-hash", "secp256k1", "bip32"] # Make the keypair algorithms here compatible with Subxt's Signer trait, # so that they can be used to sign transactions for compatible chains. @@ -52,7 +52,6 @@ bip32 = { workspace = true, features = ["alloc", "secp256k1"], optional = true } schnorrkel = { workspace = true, optional = true } secp256k1 = { workspace = true, optional = true, features = ["alloc", "recovery"] } keccak-hash = { workspace = true, optional = true } -ethaddr = { workspace = true, optional = true } # We only pull this in to enable the JS flag for schnorrkel to use. getrandom = { workspace = true, optional = true } diff --git a/signer/src/eth.rs b/signer/src/eth.rs index 5488031852..cb80c9f03c 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -124,6 +124,29 @@ impl AsRef<[u8; 65]> for Signature { #[derive(Debug, Copy, Clone, PartialEq, Eq, codec::Encode)] pub struct AccountId20(pub [u8; 20]); +impl AccountId20 { + fn checksum(&self) -> String { + let hex_address = hex::encode(self.0); + let hash = keccak(hex_address.as_bytes()); + + let mut checksum_address = String::with_capacity(42); + checksum_address.push_str("0x"); + + for (i, ch) in hex_address.chars().enumerate() { + // Get the corresponding nibble from the hash + let nibble = hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 }) & 0xf; + + if nibble >= 8 { + checksum_address.push(ch.to_ascii_uppercase()); + } else { + checksum_address.push(ch); + } + } + + checksum_address + } +} + impl AsRef<[u8]> for AccountId20 { fn as_ref(&self) -> &[u8] { &self.0 @@ -132,7 +155,7 @@ impl AsRef<[u8]> for AccountId20 { impl Display for AccountId20 { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", ethaddr::Address::from_slice(&self.0)) + write!(f, "{}", self.checksum()) } } @@ -386,6 +409,7 @@ mod test { hex!("0f02ba4d7f83e59eaa32eae9c3c4d99b68ce76decade21cdab7ecce8f4aef81a"); const KEY_3: [u8; 32] = hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + #[test] fn test_account_derivation_1() { let kp = Keypair::from_seed(KEY_1).expect("valid keypair"); From 92a6e8a0afcf5595207af5f6e94cefcd5970982f Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Sun, 31 Mar 2024 23:12:00 -0400 Subject: [PATCH 15/16] fix: wasm-tests --- signer/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signer/Cargo.toml b/signer/Cargo.toml index dca475008a..fe1c5065cb 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -38,7 +38,7 @@ web = ["getrandom/js"] subxt-core = { workspace = true, optional = true, default-features = false } secrecy = { workspace = true } regex = { workspace = true, features = ["unicode"] } -hex = { workspace = true } +hex = { workspace = true, features = ["alloc"] } cfg-if = { workspace = true } codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] } sp-crypto-hashing = { workspace = true } From 430b001bb74502e355edd279fdd3b6f6ad8c5a34 Mon Sep 17 00:00:00 2001 From: James Wilson Date: Fri, 12 Apr 2024 17:54:33 +0100 Subject: [PATCH 16/16] Tweaks to eth signer (#1526) * Tidy DerivationPath, make no-std compatible, misc bits * fmt * Improve comments and hide bip32 lib from pub interface * remove unstable-eth from defaults again * from_seed => from_secret_key, and check bip39 compliance eg no derivation path mnemonics vs seeds * from_seed to from_Secret_key in sr25519 one; all of them are actually taking secret key bytes --- .github/workflows/rust.yml | 1 + signer/Cargo.toml | 16 +- signer/src/ecdsa.rs | 18 +- signer/src/eth.rs | 365 ++++++++++++++++++++++++++--------- signer/src/lib.rs | 4 +- signer/src/sr25519.rs | 18 +- signer/wasm-tests/Cargo.toml | 2 +- 7 files changed, 304 insertions(+), 120 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0f1843ca69..b9f64ba34d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -189,6 +189,7 @@ jobs: cargo check -p subxt-signer cargo check -p subxt-signer --no-default-features --features sr25519 cargo check -p subxt-signer --no-default-features --features ecdsa + cargo check -p subxt-signer --no-default-features --features unstable-eth # We can't enable web features here, so no cargo hack. - name: Cargo check subxt-lightclient diff --git a/signer/Cargo.toml b/signer/Cargo.toml index fe1c5065cb..ddd6517d9d 100644 --- a/signer/Cargo.toml +++ b/signer/Cargo.toml @@ -15,8 +15,18 @@ description = "Sign extrinsics to be submitted by Subxt" keywords = ["parity", "subxt", "extrinsic", "signer"] [features] -default = ["sr25519", "ecdsa", "eth", "subxt", "std"] -std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std", "bip39/std", "schnorrkel/std", "secp256k1/std", "sp-core/std"] +default = ["sr25519", "ecdsa", "subxt", "std"] +std = [ + "regex/std", + "sp-crypto-hashing/std", + "pbkdf2/std", + "sha2/std", + "hmac/std", + "bip39/std", + "schnorrkel/std", + "secp256k1/std", + "sp-core/std" +] # Pick the signer implementation(s) you need by enabling the # corresponding features. Note: I had more difficulties getting @@ -24,7 +34,7 @@ std = ["regex/std", "sp-crypto-hashing/std", "pbkdf2/std", "sha2/std", "hmac/std # https://github.com/rust-bitcoin/rust-bitcoin/issues/930#issuecomment-1215538699 sr25519 = ["schnorrkel"] ecdsa = ["secp256k1"] -eth = ["keccak-hash", "secp256k1", "bip32"] +unstable-eth = ["keccak-hash", "ecdsa", "secp256k1", "bip32"] # Make the keypair algorithms here compatible with Subxt's Signer trait, # so that they can be used to sign transactions for compatible chains. diff --git a/signer/src/ecdsa.rs b/signer/src/ecdsa.rs index 43833462e5..4b7fb120ef 100644 --- a/signer/src/ecdsa.rs +++ b/signer/src/ecdsa.rs @@ -12,10 +12,10 @@ use hex::FromHex; use secp256k1::{ecdsa::RecoverableSignature, Message, Secp256k1, SecretKey}; use secrecy::ExposeSecret; -const SEED_LENGTH: usize = 32; +const SECRET_KEY_LENGTH: usize = 32; /// Seed bytes used to generate a key pair. -pub type Seed = [u8; SEED_LENGTH]; +pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH]; /// A signature generated by [`Keypair::sign()`]. These bytes are equivalent /// to a Substrate `MultiSignature::Ecdsa(bytes)`. @@ -67,8 +67,8 @@ impl Keypair { // Else, parse the phrase string taking the password into account. This is // the same approach taken in sp_core::crypto::Pair::from_string_with_seed. let key = if let Some(hex_str) = phrase.expose_secret().strip_prefix("0x") { - let seed = Seed::from_hex(hex_str)?; - Self::from_seed(seed)? + let seed = SecretKeyBytes::from_hex(hex_str)?; + Self::from_secret_key(seed)? } else { let phrase = bip39::Mnemonic::from_str(phrase.expose_secret().as_str())?; let pass_str = password.as_ref().map(|p| p.expose_secret().as_str()); @@ -97,11 +97,11 @@ impl Keypair { let big_seed = seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?; - let seed: Seed = big_seed[..SEED_LENGTH] + let secret_key_bytes: SecretKeyBytes = big_seed[..SECRET_KEY_LENGTH] .try_into() .expect("should be valid Seed"); - Self::from_seed(seed) + Self::from_secret_key(secret_key_bytes) } /// Turn a 32 byte seed into a keypair. @@ -109,8 +109,8 @@ impl Keypair { /// # Warning /// /// This will only be secure if the seed is secure! - pub fn from_seed(seed: Seed) -> Result { - let secret = SecretKey::from_slice(&seed).map_err(|_| Error::InvalidSeed)?; + pub fn from_secret_key(secret_key: SecretKeyBytes) -> Result { + let secret = SecretKey::from_slice(&secret_key).map_err(|_| Error::InvalidSeed)?; Ok(Self(secp256k1::Keypair::from_secret_key( &Secp256k1::signing_only(), &secret, @@ -148,7 +148,7 @@ impl Keypair { } } } - Self::from_seed(acc) + Self::from_secret_key(acc) } /// Obtain the [`PublicKey`] part of this key pair, which can be used in calls to [`verify()`]. diff --git a/signer/src/eth.rs b/signer/src/eth.rs index cb80c9f03c..0c4d80b0d3 100644 --- a/signer/src/eth.rs +++ b/signer/src/eth.rs @@ -4,18 +4,19 @@ //! An ethereum keypair implementation. +use crate::ecdsa; +use alloc::format; +use alloc::string::String; use core::fmt::{Display, Formatter}; - -use derive_more::{Display, From}; +use core::str::FromStr; +use derive_more::Display; use keccak_hash::keccak; use secp256k1::Message; -use crate::ecdsa; - -const SEED_LENGTH: usize = 32; +const SECRET_KEY_LENGTH: usize = 32; -/// Seed bytes used to generate a key pair. -pub type Seed = [u8; SEED_LENGTH]; +/// Bytes representing a private key. +pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH]; /// An ethereum keypair implementation. #[derive(Debug, Clone, PartialEq, Eq)] @@ -31,46 +32,60 @@ impl Keypair { /// Create a keypair from a BIP-39 mnemonic phrase, optional password, account index, and /// derivation type. /// + /// **Note:** if the `std` feature is not enabled, we won't attempt to normalize the provided password + /// to NFKD first, and so this is your responsibility. This is not a concern if only ASCII + /// characters are used in the password. + /// /// # Example /// /// ```rust - /// use subxt_signer::{ bip39::Mnemonic, eth::Keypair }; + /// use subxt_signer::{ bip39::Mnemonic, eth::{ Keypair, DerivationPath } }; /// /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; /// let mnemonic = Mnemonic::parse(phrase).unwrap(); - /// let keypair = Keypair::from_phrase(&mnemonic, None, 0, Default::default()).unwrap(); + /// let keypair = Keypair::from_phrase(&mnemonic, None, DerivationPath::eth(0,0)).unwrap(); /// /// keypair.sign(b"Hello world!"); /// ``` pub fn from_phrase( mnemonic: &bip39::Mnemonic, password: Option<&str>, - index: u32, - derivation: Derivation, + derivation_path: DerivationPath, ) -> Result { - let derivation_path = match derivation { - Derivation::Hard => format!("m/44'/60'/{}'/0/0", index), - Derivation::Soft => format!("m/44'/60'/0'/0/{}", index), - } - .parse() - .map_err(Error::InvalidDerivationIndex)?; - - let private = bip32::XPrv::derive_from_path( - mnemonic.to_seed(password.unwrap_or("")), - &derivation_path, - ) - .expect("valid private key"); - - Keypair::from_seed(private.to_bytes()) + // `to_seed` isn't available unless std is enabled in bip39. + #[cfg(feature = "std")] + let seed = mnemonic.to_seed(password.unwrap_or("")); + #[cfg(not(feature = "std"))] + let seed = mnemonic.to_seed_normalized(password.unwrap_or("")); + + // TODO: Currently, we use bip32 to derive private keys which under the hood uses + // the Rust k256 crate. We _also_ use the secp256k1 crate (which is very similar). + // It'd be great if we could 100% use just one of the two crypto libs. bip32 has + // a feature flag to use secp256k1, but it's unfortunately a different version (older) + // than ours. + let private = bip32::XPrv::derive_from_path(seed, &derivation_path.inner) + .map_err(|_| Error::DeriveFromPath)?; + + Keypair::from_secret_key(private.to_bytes()) } - /// Turn a 32 byte seed into a keypair. + /// Turn a 16, 32 or 64 byte seed into a keypair. /// /// # Warning /// /// This will only be secure if the seed is secure! - pub fn from_seed(seed: Seed) -> Result { - ecdsa::Keypair::from_seed(seed) + pub fn from_seed(seed: &[u8]) -> Result { + let private = bip32::XPrv::new(seed).map_err(|_| Error::InvalidSeed)?; + Keypair::from_secret_key(private.to_bytes()) + } + + /// Turn a 32 byte secret key into a keypair. + /// + /// # Warning + /// + /// This will only be secure if the secret key is secure! + pub fn from_secret_key(secret_key: SecretKeyBytes) -> Result { + ecdsa::Keypair::from_secret_key(secret_key) .map(Self) .map_err(|_| Error::InvalidSeed) } @@ -98,15 +113,47 @@ impl Keypair { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Derivation { - Hard, - Soft, +/// A derivation path. This can be parsed from a valid derivation path string like +/// `"m/44'/60'/0'/0/0"`, or we can construct one using the helpers [`DerivationPath::empty()`] +/// and [`DerivationPath::eth()`]. +#[derive(Clone, Debug)] +pub struct DerivationPath { + inner: bip32::DerivationPath, +} + +impl DerivationPath { + /// An empty derivation path (in other words, just use the master-key as is). + pub fn empty() -> Self { + let inner = bip32::DerivationPath::from_str("m").unwrap(); + DerivationPath { inner } + } + + /// A BIP44 Ethereum compatible derivation using the path "m/44'/60'/account'/0/address_index". + /// + /// # Panics + /// + /// Panics if the `account` or `address_index` provided are >= 2^31. + pub fn eth(account: u32, address_index: u32) -> Self { + assert!( + account < bip32::ChildNumber::HARDENED_FLAG, + "account must be less than 2^31" + ); + assert!( + address_index < bip32::ChildNumber::HARDENED_FLAG, + "address_index must be less than 2^31" + ); + + let derivation_string = format!("m/44'/60'/{account}'/0/{address_index}"); + let inner = bip32::DerivationPath::from_str(&derivation_string).unwrap(); + DerivationPath { inner } + } } -impl Default for Derivation { - fn default() -> Self { - Self::Soft +impl FromStr for DerivationPath { + type Err = Error; + fn from_str(s: &str) -> Result { + let inner = bip32::DerivationPath::from_str(s).map_err(|_| Error::DeriveFromPath)?; + Ok(DerivationPath { inner }) } } @@ -168,18 +215,14 @@ pub fn verify>(sig: &Signature, message: M, pubkey: &ecdsa::Publi } /// An error handed back if creating a keypair fails. -#[derive(Debug, PartialEq, Display, From)] +#[derive(Debug, PartialEq, Display)] pub enum Error { /// Invalid seed. #[display(fmt = "Invalid seed (was it the wrong length?)")] - #[from(ignore)] InvalidSeed, - /// Invalid derivation index. - #[display(fmt = "Invalid derivation index: {_0}")] - InvalidDerivationIndex(bip32::Error), - /// Invalid phrase. - #[display(fmt = "Cannot parse phrase: {_0}")] - InvalidPhrase(bip39::Error), + /// Invalid derivation path. + #[display(fmt = "Could not derive from path; some valeus in the path may have been >= 2^31?")] + DeriveFromPath, } #[cfg(feature = "std")] @@ -197,27 +240,27 @@ pub mod dev { once_static_cloned! { pub fn alith() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 0, Derivation::Soft).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 0)).unwrap() } pub fn baltathar() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 1, Derivation::Soft).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 1)).unwrap() } pub fn charleth() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 2, Derivation::Soft).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 2)).unwrap() } pub fn dorothy() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 3, Derivation::Soft).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 3)).unwrap() } pub fn ethan() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 4, Derivation::Soft).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 4)).unwrap() } pub fn faith() -> Keypair { Keypair::from_phrase( - &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, 5, Derivation::Soft).unwrap() + &bip39::Mnemonic::from_str(DEV_PHRASE).unwrap(), None, DerivationPath::eth(0, 5)).unwrap() } } } @@ -251,6 +294,7 @@ mod subxt_compat { #[cfg(test)] mod test { + use bip39::Mnemonic; use proptest::prelude::*; use secp256k1::Secp256k1; @@ -273,10 +317,6 @@ mod test { type SubxtSigner = dyn SignerT; - fn derivation_strategy() -> impl Strategy { - prop_oneof![Just(Derivation::Hard), Just(Derivation::Soft),] - } - prop_compose! { fn keypair()(seed in any::<[u8; 32]>()) -> Keypair { let secret = secp256k1::SecretKey::from_slice(&seed).expect("valid secret key"); @@ -294,37 +334,34 @@ mod test { fn check_from_phrase( entropy in any::<[u8; 32]>(), password in any::>(), - index in 1..(i32::MAX as u32), - derivation in derivation_strategy() + address in 1..(i32::MAX as u32), + account_idx in 1..(i32::MAX as u32), ) { let mnemonic = bip39::Mnemonic::from_entropy(&entropy).expect("valid mnemonic"); - let derivation_path = match derivation { - Derivation::Hard => format!("m/44'/60'/{}'/0/0", index), - Derivation::Soft => format!("m/44'/60'/0'/0/{}", index), - }.parse().expect("valid derivation path"); + let derivation_path = format!("m/44'/60'/{address}'/0/{account_idx}").parse().expect("valid derivation path"); let private = bip32::XPrv::derive_from_path( mnemonic.to_seed(password.clone().unwrap_or("".to_string())), &derivation_path, ).expect("valid private"); + // Creating our own keypairs should be equivalent to using bip32 crate to do it: assert_eq!( - Keypair::from_phrase(&mnemonic, password.as_deref(), index, derivation).expect("valid keypair"), - Keypair(ecdsa::Keypair::from_seed(private.to_bytes()).expect("valid ecdsa keypair")) + Keypair::from_phrase(&mnemonic, password.as_deref(), DerivationPath::eth(address, account_idx)).expect("valid keypair"), + Keypair(ecdsa::Keypair::from_secret_key(private.to_bytes()).expect("valid ecdsa keypair")) ); } #[test] fn check_from_phrase_bad_index( - entropy in any::<[u8; 32]>(), - password in any::>(), - index in (i32::MAX as u32)..=u32::MAX, - derivation in derivation_strategy() + address in (i32::MAX as u32)..=u32::MAX, + account_idx in (i32::MAX as u32)..=u32::MAX, ) { - let mnemonic = bip39::Mnemonic::from_entropy(&entropy).expect("valid mnemonic"); + let derivation_path_err = format!("m/44'/60'/{address}'/0/{account_idx}").parse::().expect_err("bad path expected"); + // Creating invalid derivation paths (ie values too large) will result in an error. assert_eq!( - Keypair::from_phrase(&mnemonic, password.as_deref(), index, derivation).expect_err("bad index"), - Error::InvalidDerivationIndex(bip32::Error::ChildNumber) + derivation_path_err, + Error::DeriveFromPath ); } @@ -367,37 +404,173 @@ mod test { } } + /// Test that the dev accounts match those listed in the moonbeam README. + /// https://github.com/moonbeam-foundation/moonbeam/blob/96cf8898874509d529b03c4da0e07b2787bacb18/README.md #[test] fn check_dev_accounts_match() { - assert_eq!( - dev::alith().account_id().to_string(), - "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac" - ); - assert_eq!( - dev::baltathar().account_id().to_string(), - "0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0" - ); - assert_eq!( - dev::charleth().account_id().to_string(), - "0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc" - ); - assert_eq!( - dev::dorothy().account_id().to_string(), - "0x773539d4Ac0e786233D90A233654ccEE26a613D9" - ); - assert_eq!( - dev::ethan().account_id().to_string(), - "0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB" - ); - assert_eq!( - dev::faith().account_id().to_string(), - "0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d" - ); + let cases = [ + ( + dev::alith(), + "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac", + "0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133", + ), + ( + dev::baltathar(), + "0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0", + "0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b", + ), + ( + dev::charleth(), + "0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc", + "0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b", + ), + ( + dev::dorothy(), + "0x773539d4Ac0e786233D90A233654ccEE26a613D9", + "0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68", + ), + ( + dev::ethan(), + "0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB", + "0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4", + ), + ( + dev::faith(), + "0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d", + "0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df", + ), + ]; + + for (case_idx, (keypair, exp_account_id, exp_priv_key)) in cases.into_iter().enumerate() { + let act_account_id = keypair.account_id().to_string(); + let act_priv_key = format!("0x{}", &keypair.0 .0.display_secret()); + + assert_eq!( + exp_account_id, act_account_id, + "account ID mismatch in {case_idx}" + ); + assert_eq!( + exp_priv_key, act_priv_key, + "private key mismatch in {case_idx}" + ); + } + } + + // This is a part of the test set linked in BIP39 and copied from https://github.com/trezor/python-mnemonic/blob/f5a975ab10c035596d65d854d21164266ffed284/vectors.json. + // The passphrase is always TREZOR. We check that keys generated with the mnemonic (and no derivation path) line up with the seeds given. + #[test] + fn check_basic_bip39_compliance() { + let mnemonics_and_seeds = [ + ( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", + ), + ( + "legal winner thank year wave sausage worth useful legal winner thank yellow", + "2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607", + ), + ( + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above", + "d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8", + ), + ( + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", + "ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069", + ), + ( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", + "035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa", + ), + ( + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", + "f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd", + ), + ( + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always", + "107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65", + ), + ( + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when", + "0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528", + ), + ( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8", + ), + ( + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", + "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87", + ), + ( + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", + "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f", + ), + ( + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", + "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad", + ), + ( + "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic", + "274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028", + ), + ( + "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog", + "628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac", + ), + ( + "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length", + "64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440", + ), + ( + "scheme spot photo card baby mountain device kick cradle pact join borrow", + "ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612", + ), + ( + "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave", + "fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d", + ), + ( + "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", + "72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d", + ), + ( + "cat swing flag economy stadium alone churn speed unique patch report train", + "deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5", + ), + ( + "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access", + "4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02", + ), + ( + "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", + "26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d", + ), + ( + "vessel ladder alter error federal sibling chat ability sun glass valve picture", + "2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f", + ), + ( + "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump", + "7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88", + ), + ( + "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", + "01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998", + ) + ]; + + for (idx, (m, s)) in mnemonics_and_seeds.into_iter().enumerate() { + let m = Mnemonic::parse(m).expect("mnemonic should be valid"); + let pair1 = Keypair::from_phrase(&m, Some("TREZOR"), DerivationPath::empty()).unwrap(); + let s = hex::decode(s).expect("seed hex should be valid"); + let pair2 = Keypair::from_seed(&s).unwrap(); + + assert_eq!(pair1, pair2, "pair1 and pair2 at index {idx} don't match"); + } } /// Test the same accounts from moonbeam so we know for sure that this implementation is working /// https://github.com/moonbeam-foundation/moonbeam/blob/e70ee0d427dfee8987d5a5671a66416ee6ec38aa/primitives/account/src/lib.rs#L217 - #[cfg(test)] mod moonbeam_sanity_tests { use hex_literal::hex; @@ -412,7 +585,7 @@ mod test { #[test] fn test_account_derivation_1() { - let kp = Keypair::from_seed(KEY_1).expect("valid keypair"); + let kp = Keypair::from_secret_key(KEY_1).expect("valid keypair"); assert_eq!( kp.account_id().to_string(), "0x976f8456E4e2034179B284A23C0e0c8f6d3da50c" @@ -421,7 +594,7 @@ mod test { #[test] fn test_account_derivation_2() { - let kp = Keypair::from_seed(KEY_2).expect("valid keypair"); + let kp = Keypair::from_secret_key(KEY_2).expect("valid keypair"); assert_eq!( kp.account_id().to_string(), "0x420e9F260B40aF7E49440ceAd3069f8e82A5230f" @@ -430,7 +603,7 @@ mod test { #[test] fn test_account_derivation_3() { - let kp = Keypair::from_seed(KEY_3).expect("valid keypair"); + let kp = Keypair::from_secret_key(KEY_3).expect("valid keypair"); assert_eq!( kp.account_id().to_string(), "0x9cce34F7aB185c7ABA1b7C8140d620B4BDA941d6" diff --git a/signer/src/lib.rs b/signer/src/lib.rs index 18abb9eb33..23865bf81c 100644 --- a/signer/src/lib.rs +++ b/signer/src/lib.rs @@ -33,8 +33,8 @@ pub mod sr25519; pub mod ecdsa; // An ethereum signer implementation. -#[cfg(feature = "eth")] -#[cfg_attr(docsrs, doc(cfg(feature = "eth")))] +#[cfg(feature = "unstable-eth")] +#[cfg_attr(docsrs, doc(cfg(feature = "unstable-eth")))] pub mod eth; // Re-export useful bits and pieces for generating a Pair from a phrase, diff --git a/signer/src/sr25519.rs b/signer/src/sr25519.rs index 83e011fc26..25db213a0d 100644 --- a/signer/src/sr25519.rs +++ b/signer/src/sr25519.rs @@ -16,11 +16,11 @@ use schnorrkel::{ }; use secrecy::ExposeSecret; -const SEED_LENGTH: usize = schnorrkel::keys::MINI_SECRET_KEY_LENGTH; +const SECRET_KEY_LENGTH: usize = schnorrkel::keys::MINI_SECRET_KEY_LENGTH; const SIGNING_CTX: &[u8] = b"substrate"; /// Seed bytes used to generate a key pair. -pub type Seed = [u8; SEED_LENGTH]; +pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH]; /// A signature generated by [`Keypair::sign()`]. These bytes are equivalent /// to a Substrate `MultiSignature::sr25519(bytes)`. @@ -73,8 +73,8 @@ impl Keypair { // Else, parse the phrase string taking the password into account. This is // the same approach taken in sp_core::crypto::Pair::from_string_with_seed. let key = if let Some(hex_str) = phrase.expose_secret().strip_prefix("0x") { - let seed = Seed::from_hex(hex_str)?; - Self::from_seed(seed)? + let seed = SecretKeyBytes::from_hex(hex_str)?; + Self::from_secret_key(seed)? } else { let phrase = bip39::Mnemonic::from_str(phrase.expose_secret().as_str())?; let pass_str = password.as_ref().map(|p| p.expose_secret().as_str()); @@ -103,20 +103,20 @@ impl Keypair { let big_seed = seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?; - let seed: Seed = big_seed[..SEED_LENGTH] + let seed: SecretKeyBytes = big_seed[..SECRET_KEY_LENGTH] .try_into() .expect("should be valid Seed"); - Self::from_seed(seed) + Self::from_secret_key(seed) } - /// Turn a 32 byte seed into a keypair. + /// Turn a 32 byte secret key into a keypair. /// /// # Warning /// /// This will only be secure if the seed is secure! - pub fn from_seed(seed: Seed) -> Result { - let keypair = MiniSecretKey::from_bytes(&seed) + pub fn from_secret_key(secret_key_bytes: SecretKeyBytes) -> Result { + let keypair = MiniSecretKey::from_bytes(&secret_key_bytes) .map_err(|_| Error::InvalidSeed)? .expand_to_keypair(ExpansionMode::Ed25519); diff --git a/signer/wasm-tests/Cargo.toml b/signer/wasm-tests/Cargo.toml index 8763ae3297..f807f06b3f 100644 --- a/signer/wasm-tests/Cargo.toml +++ b/signer/wasm-tests/Cargo.toml @@ -17,7 +17,7 @@ subxt-signer = { path = "..", default-features = false, features = [ "web", "sr25519", "ecdsa", - "eth", + "unstable-eth", "std", ] }