From eb445789080b33d78b40d5b2601eb382ad88fcf6 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 22 Nov 2023 22:06:28 +0100 Subject: [PATCH 01/42] init --- Cargo.toml | 11 ++++++----- crates/signers/Cargo.toml | 14 ++++++++++++++ crates/signers/src/lib.rs | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 crates/signers/Cargo.toml create mode 100644 crates/signers/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index a332db33199..00e28995164 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,13 +18,14 @@ rustdoc-args = ["--cfg", "docsrs"] [workspace.dependencies] alloy-json-rpc = { version = "0.1.0", path = "crates/json-rpc" } -alloy-transport = { version = "0.1.0", path = "crates/transport" } +alloy-networks = { version = "0.1.0", path = "crates/networks" } alloy-pubsub = { version = "0.1.0", path = "crates/pubsub" } +alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" } +alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } +alloy-signers = { version = "0.1.0", path = "crates/signers" } +alloy-transport = { version = "0.1.0", path = "crates/transport" } alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" } alloy-transport-ws = { version = "0.1.0", path = "crates/transport-ws" } -alloy-networks = { version = "0.1.0", path = "crates/networks" } -alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } -alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" } alloy-primitives = { version = "0.4.2", features = ["serde"] } alloy-rlp = "0.3" @@ -34,7 +35,7 @@ base64 = "0.21" bimap = "0.6" futures = "0.3.29" hyper = "0.14.27" -itertools = "0.11" +itertools = "0.12" pin-project = "1.1" reqwest = "0.11.18" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/signers/Cargo.toml b/crates/signers/Cargo.toml new file mode 100644 index 00000000000..b487dffa42e --- /dev/null +++ b/crates/signers/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "alloy-signers" +description = "Ethereum signer abstraction" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] diff --git a/crates/signers/src/lib.rs b/crates/signers/src/lib.rs new file mode 100644 index 00000000000..7d12d9af819 --- /dev/null +++ b/crates/signers/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 809a4d3bcf7647578bcfe5f7be4a7d51b96274ee Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 23 Nov 2023 02:25:38 +0100 Subject: [PATCH 02/42] setup --- crates/signers/README.md | 3 +++ crates/signers/src/lib.rs | 30 ++++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 crates/signers/README.md diff --git a/crates/signers/README.md b/crates/signers/README.md new file mode 100644 index 00000000000..aa7b8db08aa --- /dev/null +++ b/crates/signers/README.md @@ -0,0 +1,3 @@ +# alloy-signers + +Ethereum signer abstraction. diff --git a/crates/signers/src/lib.rs b/crates/signers/src/lib.rs index 7d12d9af819..8708f600aac 100644 --- a/crates/signers/src/lib.rs +++ b/crates/signers/src/lib.rs @@ -1,14 +1,16 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] From ed1d79781e125060b9ea8d26beebb28390f6d4bc Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 23 Nov 2023 06:00:44 +0100 Subject: [PATCH 03/42] wip --- Cargo.toml | 31 +- crates/signers/Cargo.toml | 55 +++ crates/signers/README.md | 63 ++++ crates/signers/src/aws/mod.rs | 294 +++++++++++++++ crates/signers/src/aws/utils.rs | 49 +++ crates/signers/src/ledger/app.rs | 347 ++++++++++++++++++ crates/signers/src/ledger/mod.rs | 51 +++ crates/signers/src/ledger/types.rs | 98 +++++ crates/signers/src/lib.rs | 50 ++- crates/signers/src/signature.rs | 165 +++++++++ crates/signers/src/signer.rs | 44 +++ crates/signers/src/trezor/app.rs | 397 ++++++++++++++++++++ crates/signers/src/trezor/mod.rs | 51 +++ crates/signers/src/trezor/types.rs | 155 ++++++++ crates/signers/src/utils.rs | 85 +++++ crates/signers/src/wallet/mnemonic.rs | 262 +++++++++++++ crates/signers/src/wallet/mod.rs | 141 +++++++ crates/signers/src/wallet/private_key.rs | 448 +++++++++++++++++++++++ crates/signers/src/wallet/yubi.rs | 116 ++++++ 19 files changed, 2896 insertions(+), 6 deletions(-) create mode 100644 crates/signers/src/aws/mod.rs create mode 100644 crates/signers/src/aws/utils.rs create mode 100644 crates/signers/src/ledger/app.rs create mode 100644 crates/signers/src/ledger/mod.rs create mode 100644 crates/signers/src/ledger/types.rs create mode 100644 crates/signers/src/signature.rs create mode 100644 crates/signers/src/signer.rs create mode 100644 crates/signers/src/trezor/app.rs create mode 100644 crates/signers/src/trezor/mod.rs create mode 100644 crates/signers/src/trezor/types.rs create mode 100644 crates/signers/src/utils.rs create mode 100644 crates/signers/src/wallet/mnemonic.rs create mode 100644 crates/signers/src/wallet/mod.rs create mode 100644 crates/signers/src/wallet/private_key.rs create mode 100644 crates/signers/src/wallet/yubi.rs diff --git a/Cargo.toml b/Cargo.toml index 00e28995164..b1615f2e19f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,19 +30,40 @@ alloy-transport-ws = { version = "0.1.0", path = "crates/transport-ws" } alloy-primitives = { version = "0.4.2", features = ["serde"] } alloy-rlp = "0.3" +# crypto +elliptic-curve = { version = "0.13.5", default-features = false } +generic-array = { version = "0.14.7", default-features = false } +k256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "std"] } +sha2 = { version = "0.10.8", default-features = false } +spki = { version = "0.7.2", default-features = false } + +# async async-trait = "0.1.74" -base64 = "0.21" -bimap = "0.6" futures = "0.3.29" +futures-util = "0.3.29" + hyper = "0.14.27" +tokio = { version = "1.33", features = ["sync", "macros"] } +tower = { version = "0.4.13", features = ["util"] } + +tracing = "0.1.40" +tracing-subscriber = "0.3.18" + +tempfile = "3.8" + +base64 = "0.21" +bimap = "0.6" itertools = "0.12" pin-project = "1.1" +rand = "0.8.5" reqwest = "0.11.18" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.4" +home = "0.5" +semver = "1.0" thiserror = "1.0" -tokio = { version = "1.33", features = ["sync", "macros"] } -tower = { version = "0.4.13", features = ["util"] } -tracing = "0.1.40" url = "2.4" + +[patch.crates-io] +alloy-primitives = { path = "../core/crates/primitives" } diff --git a/crates/signers/Cargo.toml b/crates/signers/Cargo.toml index b487dffa42e..f0bb81817ca 100644 --- a/crates/signers/Cargo.toml +++ b/crates/signers/Cargo.toml @@ -12,3 +12,58 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-primitives.workspace = true + +# crypto +coins-bip32 = "0.8.7" +coins-bip39 = "0.8.7" +elliptic-curve.workspace = true +k256.workspace = true +rand.workspace = true +sha2.workspace = true + +# misc +thiserror.workspace = true +tracing.workspace = true +async-trait.workspace = true + +# futures +futures-util = { workspace = true, optional = true } + +# aws +aws-config = { version = "1.0", default-features = false, optional = true } +aws-sdk-kms = { version = "0.39", default-features = false, optional = true } +spki = { workspace = true, optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +eth-keystore = "0.5.0" +home = { workspace = true, optional = true } + +# ledger +coins-ledger = { version = "0.8.3", default-features = false, optional = true } +semver = { workspace = true, optional = true } + +# trezor +# TODO: bump this and remove protobuf pin +trezor-client = { version = "=0.1.0", default-features = false, features = [ + "ethereum", +], optional = true } +protobuf = { version = "=3.2.0", optional = true } + +# yubi +yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional = true } + +[dev-dependencies] +serde_json.workspace = true +tempfile.workspace = true +tracing-subscriber.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +yubihsm = { version = "0.42", features = ["secp256k1", "usb", "mockhsm"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + +[features] +ledger = ["dep:coins-ledger", "dep:semver", "dep:futures-util"] +trezor = ["dep:trezor-client", "dep:semver", "dep:home", "dep:protobuf", "dep:futures-util"] +aws = ["dep:aws-config", "dep:aws-sdk-kms", "dep:spki"] +yubi = ["dep:yubihsm"] diff --git a/crates/signers/README.md b/crates/signers/README.md index aa7b8db08aa..03f041e9c29 100644 --- a/crates/signers/README.md +++ b/crates/signers/README.md @@ -1,3 +1,66 @@ # alloy-signers Ethereum signer abstraction. + +You can implement the `Signer` trait to extend functionality to other signers +such as Hardware Security Modules, KMS etc. + +The exposed interfaces return a recoverable signature. In order to convert the +signature and the [`TransactionRequest`] to a [`Transaction`], look at the +signing middleware. + +Supported signers: +- [Private key](./src/wallet) +- [Ledger](./src/ledger) +- [Trezor](./src/trezor) +- [YubiHSM2](./src/wallet/yubi.rs) +- [AWS KMS](./src/aws) + +[`transaction`]: ethers_core::types::Transaction +[`transactionrequest`]: ethers_core::types::TransactionRequest + +## Examples + + + +```rust,no_run +# use ethers_signers::{LocalWallet, Signer}; +# use ethers_core::{k256::ecdsa::SigningKey, types::TransactionRequest}; +# async fn foo() -> Result<(), Box> { +// instantiate the wallet +let wallet = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" + .parse::()?; + +// create a transaction +let tx = TransactionRequest::new() + .to("vitalik.eth") // this will use ENS + .value(10000).into(); + +// sign it +let signature = wallet.sign_transaction(&tx).await?; + +// can also sign a message +let signature = wallet.sign_message("hello world").await?; +signature.verify("hello world", wallet.address()).unwrap(); +# Ok(()) +# } +``` + +Sign an Ethereum prefixed message ([eip-712](https://eips.ethereum.org/EIPS/eip-712)): + +```rust,no_run +# use ethers_signers::{Signer, LocalWallet}; +# async fn foo() -> Result<(), Box> { +let message = "Some data"; +let wallet = LocalWallet::new(&mut rand::thread_rng()); + +// Sign the message +let signature = wallet.sign_message(message).await?; + +// Recover the signer from the message +let recovered = signature.recover(message)?; + +assert_eq!(recovered, wallet.address()); +# Ok(()) +# } +``` diff --git a/crates/signers/src/aws/mod.rs b/crates/signers/src/aws/mod.rs new file mode 100644 index 00000000000..62a676d1115 --- /dev/null +++ b/crates/signers/src/aws/mod.rs @@ -0,0 +1,294 @@ +//! AWS KMS-based signer. + +use super::Signer; +use alloy_primitives::utils::eip191_hash_message; +use aws_sdk_kms::{ + error::SdkError, + operation::{ + get_public_key::{GetPublicKeyError, GetPublicKeyOutput}, + sign::{SignError, SignOutput}, + }, + primitives::Blob, + types::{MessageType, SigningAlgorithmSpec}, + Client, +}; +use debug; +use ethers_core::types::{ + transaction::{eip2718::TypedTransaction, eip712::Eip712}, + Address, Signature as EthSig, B256, +}; +use instrument; +use k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey}; +use std::fmt; +use trace; + +mod utils; + +/// An Ethers signer that uses keys held in Amazon Web Services Key Management Service (AWS KMS). +/// +/// The AWS Signer passes signing requests to the cloud service. AWS KMS keys +/// are identified by a UUID, the `key_id`. +/// +/// Because the public key is unknown, we retrieve it on instantiation of the +/// signer. This means that the new function is `async` and must be called +/// within some runtime. +/// +/// # Examples +/// +/// ```no_run +/// # async fn test() { +/// use aws_config::BehaviorVersion; +/// use ethers_signers::{AwsSigner, Signer}; +/// +/// let config = aws_config::load_defaults(BehaviorVersion::latest()).await; +/// let client = aws_sdk_kms::Client::new(&config); +/// +/// let key_id = "..."; +/// let chain_id = 1; +/// let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); +/// +/// let message = vec![0, 1, 2, 3]; +/// +/// let sig = signer.sign_message(&message).await.unwrap(); +/// sig.verify(message, signer.address()).expect("valid sig"); +/// # } +/// ``` +#[derive(Clone)] +pub struct AwsSigner { + kms: Client, + chain_id: u64, + key_id: String, + pubkey: VerifyingKey, + address: Address, +} + +impl fmt::Debug for AwsSigner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AwsSigner") + .field("key_id", &self.key_id) + .field("chain_id", &self.chain_id) + .field("pubkey", &hex::encode(self.pubkey.to_sec1_bytes())) + .field("address", &self.address) + .finish() + } +} + +impl fmt::Display for AwsSigner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +/// Errors thrown by [`AwsSigner`]. +#[derive(thiserror::Error, Debug)] +pub enum AwsSignerError { + #[error(transparent)] + SignError(#[from] SdkError), + #[error(transparent)] + GetPublicKeyError(#[from] SdkError), + #[error(transparent)] + K256(#[from] K256Error), + #[error(transparent)] + Spki(#[from] spki::Error), + /// Error when converting from a hex string + #[error(transparent)] + HexError(#[from] hex::FromHexError), + /// Error type from Eip712Error message + #[error("failed encoding eip712 struct: {0:?}")] + Eip712Error(String), + #[error("{0}")] + Other(String), +} + +impl From for AwsSignerError { + fn from(value: String) -> Self { + Self::Other(value) + } +} + +impl AwsSigner { + /// Instantiate a new signer from an existing `Client` and key ID. + /// + /// This function retrieves the public key from AWS and calculates the + /// Etheruem address. It is therefore `async`. + #[instrument(err, skip_all, fields(key_id = %key_id.as_ref()))] + pub async fn new>( + kms: Client, + key_id: T, + chain_id: u64, + ) -> Result { + let key_id = key_id.as_ref(); + let resp = request_get_pubkey(&kms, key_id).await?; + let pubkey = decode_pubkey(resp)?; + let address = ethers_core::utils::public_key_to_address(&pubkey); + + debug!( + "Instantiated AWS signer with pubkey 0x{} and address {address:?}", + hex::encode(pubkey.to_sec1_bytes()), + ); + + Ok(Self { kms, chain_id, key_id: key_id.into(), pubkey, address }) + } + + /// Fetch the pubkey associated with a key ID. + pub async fn get_pubkey_for_key(&self, key_id: T) -> Result + where + T: AsRef, + { + request_get_pubkey(&self.kms, key_id.as_ref()).await.and_then(decode_pubkey) + } + + /// Fetch the pubkey associated with this signer's key ID. + pub async fn get_pubkey(&self) -> Result { + self.get_pubkey_for_key(&self.key_id).await + } + + /// Sign a digest with the key associated with a key ID. + pub async fn sign_digest_with_key>( + &self, + key_id: T, + digest: [u8; 32], + ) -> Result { + request_sign_digest(&self.kms, key_id.as_ref(), digest).await.and_then(decode_signature) + } + + /// Sign a digest with this signer's key + pub async fn sign_digest(&self, digest: [u8; 32]) -> Result { + self.sign_digest_with_key(self.key_id.clone(), digest).await + } + + /// Sign a digest with this signer's key and add the eip155 `v` value + /// corresponding to the input chain_id + #[instrument(err, skip(digest), fields(digest = %hex::encode(digest)))] + async fn sign_digest_with_eip155( + &self, + digest: B256, + chain_id: u64, + ) -> Result { + let sig = self.sign_digest(digest.into()).await?; + let mut sig = + utils::sig_from_digest_bytes_trial_recovery(&sig, digest.into(), &self.pubkey); + utils::apply_eip155(&mut sig, chain_id); + Ok(sig) + } +} + +#[async_trait::async_trait] +impl Signer for AwsSigner { + type Error = AwsSignerError; + + #[instrument(err, skip(message))] + async fn sign_message(&self, message: &[u8]) -> Result { + let message = message.as_ref(); + let message_hash = eip191_hash_message(message); + trace!(?message_hash, ?message); + + self.sign_digest_with_eip155(message_hash, self.chain_id).await + } + + #[instrument(err)] + async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { + let mut tx_with_chain = tx.clone(); + let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + tx_with_chain.set_chain_id(chain_id); + + let sighash = tx_with_chain.sighash(); + self.sign_digest_with_eip155(sighash, chain_id).await + } + + #[cfg(TODO)] + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result { + let digest = + payload.encode_eip712().map_err(|e| Self::Error::Eip712Error(e.to_string()))?; + + let sig = self.sign_digest(digest).await?; + let sig = utils::sig_from_digest_bytes_trial_recovery(&sig, digest, &self.pubkey); + + Ok(sig) + } + + fn address(&self) -> Address { + self.address + } + + fn chain_id(&self) -> u64 { + self.chain_id + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } +} + +#[instrument(err, skip(kms))] +async fn request_get_pubkey( + kms: &Client, + key_id: &str, +) -> Result { + kms.get_public_key().key_id(key_id).send().await.map_err(Into::into) +} + +#[instrument(err, skip(kms, digest), fields(digest = %hex::encode(digest)))] +async fn request_sign_digest( + kms: &Client, + key_id: &str, + digest: [u8; 32], +) -> Result { + kms.sign() + .key_id(key_id) + .message(Blob::new(digest)) + .message_type(MessageType::Digest) + .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) + .send() + .await + .map_err(Into::into) +} + +/// Decode an AWS KMS Pubkey response. +fn decode_pubkey(resp: GetPublicKeyOutput) -> Result { + let raw = resp + .public_key + .as_ref() + .ok_or_else(|| AwsSignerError::from("Pubkey not found in response".to_owned()))?; + + let spki = spki::SubjectPublicKeyInfoRef::try_from(raw.as_ref())?; + let key = VerifyingKey::from_sec1_bytes(spki.subject_public_key.raw_bytes())?; + + Ok(key) +} + +/// Decode an AWS KMS Signature response. +fn decode_signature(resp: SignOutput) -> Result { + let raw = resp + .signature + .as_ref() + .ok_or_else(|| AwsSignerError::from("Signature not found in response".to_owned()))?; + + let sig = KSig::from_der(raw.as_ref())?; + Ok(sig.normalize_s().unwrap_or(sig)) +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_config::BehaviorVersion; + + #[tokio::test] + async fn sign_message() { + let Ok(key_id) = std::env::var("AWS_KEY_ID") else { return }; + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let client = aws_sdk_kms::Client::new(&config); + + let chain_id = 1; + let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); + + let message = vec![0, 1, 2, 3]; + + let sig = signer.sign_message(&message).await.unwrap(); + sig.verify(message, signer.address()).expect("valid sig"); + } +} diff --git a/crates/signers/src/aws/utils.rs b/crates/signers/src/aws/utils.rs new file mode 100644 index 00000000000..b649ab8118e --- /dev/null +++ b/crates/signers/src/aws/utils.rs @@ -0,0 +1,49 @@ +//! These utils are NOT meant for general usage. They are ONLY meant for use +//! within this module. They DO NOT perform basic safety checks and may panic +//! if used incorrectly. + +use ethers_core::{ + k256::{ + ecdsa::{RecoveryId, Signature as RSig, Signature as KSig, VerifyingKey}, + FieldBytes, + }, + types::{Signature as EthSig, U256}, +}; + +/// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. +fn check_candidate( + sig: &RSig, + recovery_id: RecoveryId, + digest: [u8; 32], + vk: &VerifyingKey, +) -> bool { + VerifyingKey::recover_from_prehash(digest.as_slice(), sig, recovery_id) + .map(|key| key == *vk) + .unwrap_or(false) +} + +/// Recover an rsig from a signature under a known key by trial/error. +pub(super) fn sig_from_digest_bytes_trial_recovery( + sig: &KSig, + digest: [u8; 32], + vk: &VerifyingKey, +) -> EthSig { + let r_bytes: FieldBytes = sig.r().into(); + let s_bytes: FieldBytes = sig.s().into(); + let r = U256::from_big_endian(r_bytes.as_slice()); + let s = U256::from_big_endian(s_bytes.as_slice()); + + if check_candidate(sig, RecoveryId::from_byte(0).unwrap(), digest, vk) { + EthSig { r, s, v: 0 } + } else if check_candidate(sig, RecoveryId::from_byte(1).unwrap(), digest, vk) { + EthSig { r, s, v: 1 } + } else { + panic!("bad sig"); + } +} + +/// Modify the `v` value of a signature to conform to EIP-155. +pub(super) fn apply_eip155(sig: &mut EthSig, chain_id: u64) { + let v = (chain_id * 2 + 35) + sig.v; + sig.v = v; +} diff --git a/crates/signers/src/ledger/app.rs b/crates/signers/src/ledger/app.rs new file mode 100644 index 00000000000..e100ebbf3a8 --- /dev/null +++ b/crates/signers/src/ledger/app.rs @@ -0,0 +1,347 @@ +use super::types::*; +use crate::{Signature, Transaction, TransactionRequest}; +use alloy_primitives::{hex, keccak256, Address, TxHash, B256, U256}; +use coins_ledger::{ + common::{APDUAnswer, APDUCommand, APDUData}, + transports::{Ledger, LedgerAsync}, +}; +use futures_util::lock::Mutex; +use thiserror::Error; + +/// A Ledger Ethereum App. +/// +/// This is a simple wrapper around the [Ledger transport](Ledger) +#[derive(Debug)] +pub struct LedgerEthereum { + transport: Mutex, + derivation: DerivationType, + pub(crate) chain_id: u64, + pub(crate) address: Address, +} + +impl std::fmt::Display for LedgerEthereum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LedgerApp. Key at index {} with address {:?} on chain_id {}", + self.derivation, self.address, self.chain_id + ) + } +} + +const EIP712_MIN_VERSION: &str = ">=1.6.0"; + +impl LedgerEthereum { + /// Instantiate the application by acquiring a lock on the ledger device. + /// + /// + /// ``` + /// # async fn foo() -> Result<(), Box> { + /// use ethers_signers::{HDPath, Ledger}; + /// + /// let ledger = Ledger::new(HDPath::LedgerLive(0), 1).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { + let transport = Ledger::init().await?; + let address = Self::get_address_with_path_transport(&transport, &derivation).await?; + + Ok(Self { transport: Mutex::new(transport), derivation, chain_id, address }) + } + + /// Consume self and drop the ledger mutex + pub fn close(self) {} + + /// Get the account which corresponds to our derivation path + pub async fn get_address(&self) -> Result { + self.get_address_with_path(&self.derivation).await + } + + /// Gets the account which corresponds to the provided derivation path + pub async fn get_address_with_path( + &self, + derivation: &DerivationType, + ) -> Result { + let data = APDUData::new(&Self::path_to_bytes(derivation)); + let transport = self.transport.lock().await; + Self::get_address_with_path_transport(&transport, derivation).await + } + + #[instrument(skip(transport))] + async fn get_address_with_path_transport( + transport: &Ledger, + derivation: &DerivationType, + ) -> Result { + let data = APDUData::new(&Self::path_to_bytes(derivation)); + + let command = APDUCommand { + ins: INS::GET_PUBLIC_KEY as u8, + p1: P1::NON_CONFIRM as u8, + p2: P2::NO_CHAINCODE as u8, + data, + response_len: None, + }; + + debug!("Dispatching get_address request to ethereum app"); + let answer = transport.exchange(&command).await?; + let result = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?; + + let address = { + // extract the address from the response + let offset = 1 + result[0] as usize; + let address_str = &result[offset + 1..offset + 1 + result[offset] as usize]; + let mut address = [0; 20]; + address.copy_from_slice(&hex::decode(address_str)?); + Address::from(address) + }; + debug!(?address, "Received address from device"); + Ok(address) + } + + /// Returns the semver of the Ethereum ledger app + pub async fn version(&self) -> Result { + let transport = self.transport.lock().await; + + let command = APDUCommand { + ins: INS::GET_APP_CONFIGURATION as u8, + p1: P1::NON_CONFIRM as u8, + p2: P2::NO_CHAINCODE as u8, + data: APDUData::new(&[]), + response_len: None, + }; + + debug!("Dispatching get_version"); + let answer = transport.exchange(&command).await?; + let result = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?; + if result.len() < 4 { + return Err(LedgerError::ShortResponse { got: result.len(), at_least: 4 }); + } + let version = format!("{}.{}.{}", result[1], result[2], result[3]); + debug!(version, "Retrieved version from device"); + Ok(version) + } + + /// Signs an Ethereum transaction (requires confirmation on the ledger) + pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { + let mut tx_with_chain = tx.clone(); + if tx_with_chain.chain_id().is_none() { + // in the case we don't have a chain_id, let's use the signer chain id instead + tx_with_chain.set_chain_id(self.chain_id); + } + let mut payload = Self::path_to_bytes(&self.derivation); + payload.extend_from_slice(tx_with_chain.rlp().as_ref()); + + let mut signature = self.sign_payload(INS::SIGN, &payload).await?; + + // modify `v` value of signature to match EIP-155 for chains with large chain ID + // The logic is derived from Ledger's library + // https://github.com/LedgerHQ/ledgerjs/blob/e78aac4327e78301b82ba58d63a72476ecb842fc/packages/hw-app-eth/src/Eth.ts#L300 + let eip155_chain_id = self.chain_id * 2 + 35; + if eip155_chain_id + 1 > 255 { + let one_byte_chain_id = eip155_chain_id % 256; + let ecc_parity = if signature.v > one_byte_chain_id { + signature.v - one_byte_chain_id + } else { + one_byte_chain_id - signature.v + }; + + signature.v = match tx { + TypedTransaction::Eip2930(_) | TypedTransaction::Eip1559(_) => { + (ecc_parity % 2 != 1) as u64 + } + TypedTransaction::Legacy(_) => eip155_chain_id + ecc_parity, + #[cfg(feature = "optimism")] + TypedTransaction::DepositTransaction(_) => 0, + }; + } + + Ok(signature) + } + + /// Signs an ethereum personal message + pub async fn sign_message>( + &self, + message: &[u8], + ) -> Result { + let message = message.as_ref(); + + let mut payload = Self::path_to_bytes(&self.derivation); + payload.extend_from_slice(&(message.len() as u32).to_be_bytes()); + payload.extend_from_slice(message); + + self.sign_payload(INS::SIGN_PERSONAL_MESSAGE, &payload).await + } + + /// Signs an EIP712 encoded domain separator and message + pub async fn sign_typed_struct(&self, payload: &T) -> Result + where + T: Eip712, + { + // See comment for v1.6.0 requirement + // https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999 + let req = semver::VersionReq::parse(EIP712_MIN_VERSION)?; + let version = semver::Version::parse(&self.version().await?)?; + + // Enforce app version is greater than EIP712_MIN_VERSION + if !req.matches(&version) { + return Err(LedgerError::UnsupportedAppVersion(EIP712_MIN_VERSION)); + } + + let domain_separator = + payload.domain_separator().map_err(|e| LedgerError::Eip712Error(e.to_string()))?; + let struct_hash = + payload.struct_hash().map_err(|e| LedgerError::Eip712Error(e.to_string()))?; + + let mut payload = Self::path_to_bytes(&self.derivation); + payload.extend_from_slice(&domain_separator); + payload.extend_from_slice(&struct_hash); + + self.sign_payload(INS::SIGN_ETH_EIP_712, &payload).await + } + + // Helper function for signing either transaction data, personal messages or EIP712 derived + // structs + #[instrument(err, skip_all, fields(command = %command, payload = hex::encode(payload)))] + pub async fn sign_payload( + &self, + command: INS, + payload: &Vec, + ) -> Result { + if payload.is_empty() { + return Err(LedgerError::EmptyPayload); + } + let transport = self.transport.lock().await; + let mut command = APDUCommand { + ins: command as u8, + p1: P1_FIRST, + p2: P2::NO_CHAINCODE as u8, + data: APDUData::new(&[]), + response_len: None, + }; + + let mut answer = None; + // workaround for https://github.com/LedgerHQ/app-ethereum/issues/409 + // TODO: remove in future version + let chunk_size = + (0..=255).rev().find(|i| payload.len() % i != 3).expect("true for any length"); + + // Iterate in 255 byte chunks + let span = debug_span!("send_loop", index = 0, chunk = ""); + let guard = span.entered(); + for (index, chunk) in payload.chunks(chunk_size).enumerate() { + guard.record("index", index); + guard.record("chunk", hex::encode(chunk)); + command.data = APDUData::new(chunk); + + debug!("Dispatching packet to device"); + answer = Some(transport.exchange(&command).await?); + + let data = answer.as_ref().expect("just assigned").data(); + if data.is_none() { + return Err(LedgerError::UnexpectedNullResponse); + } + debug!( + response = hex::encode(data.expect("just checked")), + "Received response from device" + ); + + // We need more data + command.p1 = P1::MORE as u8; + } + drop(guard); + let answer = answer.expect("payload is non-empty, therefore loop ran"); + let result = answer.data().expect("check in loop"); + if result.len() < 65 { + return Err(LedgerError::ShortResponse { got: result.len(), at_least: 65 }); + } + let v = result[0] as u64; + let r = U256::from_big_endian(&result[1..33]); + let s = U256::from_big_endian(&result[33..]); + let sig = Signature { r, s, v }; + debug!(sig = %sig, "Received signature from device"); + Ok(sig) + } + + // helper which converts a derivation path to bytes + fn path_to_bytes(derivation: &DerivationType) -> Vec { + let derivation = derivation.to_string(); + let elements = derivation.split('/').skip(1).collect::>(); + let depth = elements.len(); + + let mut bytes = vec![depth as u8]; + for derivation_index in elements { + let hardened = derivation_index.contains('\''); + let mut index = derivation_index.replace('\'', "").parse::().unwrap(); + if hardened { + index |= 0x80000000; + } + + bytes.extend(index.to_be_bytes()); + } + + bytes + } +} + +#[cfg(all(test, feature = "ledger"))] +mod tests { + use super::*; + use crate::Signer; + use alloy_primitives::{hex, Address, I256, U256}; + use std::str::FromStr; + + #[tokio::test] + #[ignore] + // Replace this with your ETH addresses. + async fn test_get_address() { + // Instantiate it with the default ledger derivation path + let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).await.unwrap(); + assert_eq!( + ledger.get_address().await.unwrap(), + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + ); + assert_eq!( + ledger.get_address_with_path(&DerivationType::Legacy(0)).await.unwrap(), + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + ); + } + + #[tokio::test] + #[ignore] + async fn test_sign_tx() { + let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).await.unwrap(); + + // approve uni v2 router 0xff + let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); + + let tx_req = TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .gas(1000000) + .gas_price(400e9 as u64) + .nonce(5) + .data(data) + .value(alloy_primitives::utils::parse_ether(100).unwrap()) + .into(); + let tx = ledger.sign_transaction(&tx_req).await.unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_version() { + let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).await.unwrap(); + + let version = ledger.version().await.unwrap(); + assert_eq!(version, "1.3.7"); + } + + #[tokio::test] + #[ignore] + async fn test_sign_message() { + let ledger = LedgerEthereum::new(DerivationType::Legacy(0), 1).await.unwrap(); + let message = "hello world"; + let sig = ledger.sign_message(message).await.unwrap(); + let addr = ledger.get_address().await.unwrap(); + sig.verify(message, addr).unwrap(); + } +} diff --git a/crates/signers/src/ledger/mod.rs b/crates/signers/src/ledger/mod.rs new file mode 100644 index 00000000000..0e8816a76ac --- /dev/null +++ b/crates/signers/src/ledger/mod.rs @@ -0,0 +1,51 @@ +pub mod app; +pub mod types; + +use crate::Signer; +use app::LedgerEthereum; +use async_trait::async_trait; +use ethers_core::types::{ + transaction::{eip2718::TypedTransaction, eip712::Eip712}, + Address, Signature, +}; +use types::LedgerError; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Signer for LedgerEthereum { + type Error = LedgerError; + + async fn sign_message(&self, message: &[u8]) -> Result { + self.sign_message(message).await + } + + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + let mut tx_with_chain = message.clone(); + if tx_with_chain.chain_id().is_none() { + // in the case we don't have a chain_id, let's use the signer chain id instead + tx_with_chain.set_chain_id(self.chain_id); + } + self.sign_tx(&tx_with_chain).await + } + + #[cfg(TODO)] + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result { + self.sign_typed_struct(payload).await + } + + fn address(&self) -> Address { + self.address + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } + + fn chain_id(&self) -> u64 { + self.chain_id + } +} diff --git a/crates/signers/src/ledger/types.rs b/crates/signers/src/ledger/types.rs new file mode 100644 index 00000000000..cb3af7026c4 --- /dev/null +++ b/crates/signers/src/ledger/types.rs @@ -0,0 +1,98 @@ +//! Helpers for interacting with the Ethereum Ledger App. +//! +//! [Official Docs](https://github.com/LedgerHQ/app-ethereum/blob/master/doc/ethapp.adoc) + +#![allow(clippy::upper_case_acronyms)] + +use std::fmt; +use thiserror::Error; + +#[derive(Clone, Debug)] +/// Ledger wallet type +pub enum DerivationType { + /// Ledger Live-generated HD path + LedgerLive(usize), + /// Legacy generated HD Path + Legacy(usize), + /// Any other path + Other(String), +} + +impl fmt::Display for DerivationType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + DerivationType::Legacy(index) => write!(f, "m/44'/60'/0'/{index}"), + DerivationType::LedgerLive(index) => write!(f, "m/44'/60'/{index}'/0/0"), + DerivationType::Other(inner) => f.write_str(inner), + } + } +} + +#[derive(Error, Debug)] +/// Error when using the Ledger transport +pub enum LedgerError { + /// Underlying ledger transport error + #[error(transparent)] + LedgerError(#[from] coins_ledger::errors::LedgerError), + /// Device response was unexpectedly none + #[error("Received unexpected response from device. Expected data in response, found none.")] + UnexpectedNullResponse, + #[error(transparent)] + /// Error when converting from a hex string + HexError(#[from] hex::FromHexError), + #[error(transparent)] + /// Error when converting a semver requirement + SemVerError(#[from] semver::Error), + /// Error type from Eip712Error message + #[error("error encoding eip712 struct: {0:?}")] + Eip712Error(String), + /// Error when signing EIP712 struct with not compatible Ledger ETH app + #[error("Ledger ethereum app requires at least version {0}")] + UnsupportedAppVersion(&'static str), + /// Got a response, but it didn't contain as much data as expected + #[error("Cannot deserialize ledger response, insufficient bytes. Got {got} expected at least {at_least}")] + ShortResponse { got: usize, at_least: usize }, + /// Payload is empty + #[error("Payload must not be empty")] + EmptyPayload, +} + +pub const P1_FIRST: u8 = 0x00; + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[allow(non_camel_case_types)] +pub enum INS { + GET_PUBLIC_KEY = 0x02, + SIGN = 0x04, + GET_APP_CONFIGURATION = 0x06, + SIGN_PERSONAL_MESSAGE = 0x08, + SIGN_ETH_EIP_712 = 0x0C, +} + +impl std::fmt::Display for INS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + INS::GET_PUBLIC_KEY => write!(f, "GET_PUBLIC_KEY"), + INS::SIGN => write!(f, "SIGN"), + INS::GET_APP_CONFIGURATION => write!(f, "GET_APP_CONFIGURATION"), + INS::SIGN_PERSONAL_MESSAGE => write!(f, "SIGN_PERSONAL_MESSAGE"), + INS::SIGN_ETH_EIP_712 => write!(f, "SIGN_ETH_EIP_712"), + } + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[allow(non_camel_case_types)] +pub enum P1 { + NON_CONFIRM = 0x00, + MORE = 0x80, +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[allow(non_camel_case_types)] +pub enum P2 { + NO_CHAINCODE = 0x00, +} diff --git a/crates/signers/src/lib.rs b/crates/signers/src/lib.rs index 8708f600aac..120982e3523 100644 --- a/crates/signers/src/lib.rs +++ b/crates/signers/src/lib.rs @@ -11,6 +11,54 @@ clippy::missing_const_for_fn, rustdoc::all )] -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +// #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +// #[macro_use] +// extern crate tracing; + +mod signature; +pub use signature::Signature; + +mod signer; +pub use signer::Signer; + +mod wallet; +pub use wallet::{MnemonicBuilder, Wallet, WalletError}; + +// #[cfg(all(feature = "ledger", not(target_arch = "wasm32")))] +// mod ledger; +// #[cfg(all(feature = "ledger", not(target_arch = "wasm32")))] +// pub use ledger::{ +// app::LedgerEthereum as Ledger, +// types::{DerivationType as HDPath, LedgerError}, +// }; + +// #[cfg(all(feature = "trezor", not(target_arch = "wasm32")))] +// mod trezor; +// #[cfg(all(feature = "trezor", not(target_arch = "wasm32")))] +// pub use trezor::{ +// app::TrezorEthereum as Trezor, +// types::{DerivationType as TrezorHDPath, TrezorError}, +// }; + +// #[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] +// pub use yubihsm; + +// #[cfg(feature = "aws")] +// mod aws; +// #[cfg(feature = "aws")] +// pub use aws::{AwsSigner, AwsSignerError}; + +pub mod utils; + +/// Re-export the BIP-32 crate so that wordlists can be accessed conveniently. +pub use coins_bip39; + +/// A wallet instantiated with a locally stored private key +pub type LocalWallet = Wallet; + +#[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] +/// A wallet instantiated with a YubiHSM +pub type YubiWallet = Wallet>; diff --git a/crates/signers/src/signature.rs b/crates/signers/src/signature.rs new file mode 100644 index 00000000000..940e8f0917f --- /dev/null +++ b/crates/signers/src/signature.rs @@ -0,0 +1,165 @@ +use alloy_primitives::{keccak256, Address, B256}; +use elliptic_curve::NonZeroScalar; +use k256::{ + ecdsa::{self, RecoveryId, VerifyingKey}, + Secp256k1, +}; + +use crate::utils::public_key_to_address; + +/// An Ethereum ECDSA signature. +/// +/// This is a wrapper around [`ecdsa::Signature`] and a [`RecoveryId`] to provide public key +/// recovery functionality. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Signature { + /// The inner ECDSA signature. + inner: ecdsa::Signature, + /// The recovery ID. + recid: RecoveryId, +} + +impl Signature { + /// Creates a new signature from the given inner signature and recovery ID. + pub fn new(inner: ecdsa::Signature, recid: RecoveryId) -> Self { + Self { inner, recid } + } + + /// Parses a signature from a byte slice. + #[inline] + pub fn from_bytes(bytes: &[u8], v: u64) -> Result { + let inner = ecdsa::Signature::from_slice(bytes)?; + let recid = normalize_v(v); + Ok(Self { inner, recid }) + } + + /// Creates a [`Signature`] from the serialized `r` and `s` scalar values, which comprise the + /// ECDSA signature, alongside a `v` value, used to determine the recovery ID. + /// + /// See [`ecdsa::Signature::from_scalars`] for more details. + #[inline] + pub fn from_scalars(r: B256, s: B256, v: u64) -> Result { + let inner = ecdsa::Signature::from_scalars(r.0, s.0)?; + let recid = normalize_v(v); + Ok(Self { inner, recid }) + } + + /// Returns the inner ECDSA signature. + #[inline] + pub fn inner(&self) -> &ecdsa::Signature { + &self.inner + } + + /// Returns the inner ECDSA signature. + #[inline] + pub fn inner_mut(&mut self) -> &mut ecdsa::Signature { + &mut self.inner + } + + /// Returns the inner ECDSA signature. + #[inline] + pub fn into_inner(self) -> ecdsa::Signature { + self.inner + } + + /// Returns the recovery ID. + #[inline] + pub fn recid(&self) -> RecoveryId { + self.recid + } + + #[doc(hidden)] + #[deprecated(note = "use `Signature::recid` instead")] + pub fn recovery_id(&self) -> RecoveryId { + self.recid + } + + /// Returns the `r` component of this signature. + #[inline] + pub fn r(&self) -> NonZeroScalar { + self.inner.r() + } + + /// Returns the `s` component of this signature. + #[inline] + pub fn s(&self) -> NonZeroScalar { + self.inner.s() + } + + /// Returns the recovery ID as a `u8`. + #[inline] + pub fn v(&self) -> u8 { + self.recid.to_byte() + } + + /// Sets the recovery ID. + #[inline] + pub fn set_recid(&mut self, recid: RecoveryId) { + self.recid = recid; + } + + /// Sets the recovery ID by normalizing a `v` value. + #[inline] + pub fn set_v(&mut self, v: u64) { + self.recid = normalize_v(v); + } + + /// Recovers a [`VerifyingKey`] from this signature and the given message by first hashing the + /// message with Keccak-256. + #[inline] + pub fn recover_address_from_msg>( + &self, + msg: T, + ) -> Result { + self.recover_from_msg(msg).map(|pubkey| public_key_to_address(&pubkey)) + } + + /// Recovers a [`VerifyingKey`] from this signature and the given prehashed message. + #[inline] + pub fn recover_address_from_prehash(&self, prehash: &B256) -> Result { + self.recover_from_prehash(prehash).map(|pubkey| public_key_to_address(&pubkey)) + } + + /// Recovers a [`VerifyingKey`] from this signature and the given message by first hashing the + /// message with Keccak-256. + #[inline] + pub fn recover_from_msg>(&self, msg: T) -> Result { + self.recover_from_prehash(&keccak256(msg)) + } + + /// Recovers a [`VerifyingKey`] from this signature and the given prehashed message. + #[inline] + pub fn recover_from_prehash(&self, prehash: &B256) -> Result { + VerifyingKey::recover_from_prehash(prehash.as_slice(), &self.inner, self.recid) + } +} + +/// Normalizes a `v` value, respecting raw, legacy, and EIP-155 values. +/// +/// This function covers the entire u64 range, producing v-values as follows: +/// - 0-26 - raw/bare. 0-3 are legal. In order to ensure that all values are covered, we also handle +/// 4-26 here by returning v % 4. +/// - 27-34 - legacy. 27-30 are legal. By legacy bitcoin convention range 27-30 signals uncompressed +/// pubkeys, while 31-34 signals compressed pubkeys. We do not respect the compression convention. +/// All Ethereum keys are uncompressed. +/// - 35+ - EIP-155. By EIP-155 convention, `v = 35 + CHAIN_ID * 2 + 0/1` We return (v-1 % 2) here. +/// +/// NB: raw and legacy support values 2, and 3, while EIP-155 does not. +/// Recovery values of 2 and 3 are unlikely to occur in practice. In the vanishingly unlikely event +/// that you encounter an EIP-155 signature with a recovery value of 2 or 3, you should normalize +/// out of band. +#[inline] +const fn normalize_v(v: u64) -> RecoveryId { + let byte = match v { + // Case 0: raw/bare + v @ 0..=26 => (v % 4) as u8, + // Case 2: non-eip155 v value + v @ 27..=34 => ((v - 27) % 4) as u8, + // Case 3: eip155 V value + v @ 35.. => ((v - 1) % 2) as u8, + }; + match RecoveryId::from_byte(byte) { + Some(recid) => recid, + None => unsafe { core::hint::unreachable_unchecked() }, + } +} diff --git a/crates/signers/src/signer.rs b/crates/signers/src/signer.rs new file mode 100644 index 00000000000..fab2ca6ba8b --- /dev/null +++ b/crates/signers/src/signer.rs @@ -0,0 +1,44 @@ +use crate::Signature; +use alloy_primitives::Address; +use async_trait::async_trait; +use std::error::Error; + +/// Trait for signing transactions and messages. +/// +/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait Signer: std::fmt::Debug + Send + Sync { + /// The error type returned by the signer. + type Error: Error + Send + Sync; + + /// Signs the hash of the provided message after prefixing it. + async fn sign_message(&self, message: &[u8]) -> Result; + + /// Signs the transaction. + async fn sign_transaction(&self, message: &TypedTransaction) -> Result; + + /// Encodes and signs the typed data according [EIP-712]. + /// + /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 + #[cfg(TODO)] + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result; + + /// Returns the signer's Ethereum Address. + fn address(&self) -> Address; + + /// Returns the signer's chain ID. + fn chain_id(&self) -> u64; + + /// Sets the signer's chain ID. + #[must_use] + fn with_chain_id>(self, chain_id: T) -> Self + where + Self: Sized; +} + +#[cfg(test)] +struct _ObjectSafe(dyn Signer); diff --git a/crates/signers/src/trezor/app.rs b/crates/signers/src/trezor/app.rs new file mode 100644 index 00000000000..9011bbd58b7 --- /dev/null +++ b/crates/signers/src/trezor/app.rs @@ -0,0 +1,397 @@ +use super::types::*; +use ethers_core::{ + types::{ + transaction::{eip2718::TypedTransaction, eip712::Eip712}, + Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, B256, U256, + }, + utils::keccak256, +}; +use futures_executor::block_on; +use futures_util::lock::Mutex; +use std::{ + env, fs, + io::{Read, Write}, + path, + path::PathBuf, +}; +use thiserror::Error; +use trezor_client::client::{AccessListItem as Trezor_AccessListItem, Trezor}; + +/// A Trezor Ethereum App. +/// +/// This is a simple wrapper around the [Trezor transport](Trezor) +#[derive(Debug)] +pub struct TrezorEthereum { + derivation: DerivationType, + session_id: Vec, + cache_dir: PathBuf, + pub(crate) chain_id: u64, + pub(crate) address: Address, +} + +// we need firmware that supports EIP-1559 and EIP-712 +const FIRMWARE_1_MIN_VERSION: &str = ">=1.11.1"; +const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; + +// https://docs.trezor.io/trezor-firmware/common/communication/sessions.html +const SESSION_ID_LENGTH: usize = 32; +const SESSION_FILE_NAME: &str = "trezor.session"; + +impl TrezorEthereum { + pub async fn new( + derivation: DerivationType, + chain_id: u64, + cache_dir: Option, + ) -> Result { + let cache_dir = (match cache_dir.or_else(home::home_dir) { + Some(path) => path, + None => match env::current_dir() { + Ok(path) => path, + Err(e) => return Err(TrezorError::CacheError(e.to_string())), + }, + }) + .join(".ethers-rs") + .join("trezor") + .join("cache"); + + let mut blank = Self { + derivation: derivation.clone(), + chain_id, + cache_dir, + address: Address::from([0_u8; 20]), + session_id: vec![], + }; + + // Check if reachable + blank.initate_session()?; + blank.address = blank.get_address_with_path(&derivation).await?; + Ok(blank) + } + + fn check_version(version: String) -> Result<(), TrezorError> { + let version = semver::Version::parse(&version)?; + + let min_version = match version.major { + 1 => FIRMWARE_1_MIN_VERSION, + 2 => FIRMWARE_2_MIN_VERSION, + // unknown major version, possibly newer models that we don't know about yet + // it's probably safe to assume they support EIP-1559 and EIP-712 + _ => return Ok(()), + }; + + let req = semver::VersionReq::parse(min_version)?; + // Enforce firmware version is greater than "min_version" + if !req.matches(&version) { + return Err(TrezorError::UnsupportedFirmwareVersion(min_version.to_string())); + } + + Ok(()) + } + + fn get_cached_session(&self) -> Result>, TrezorError> { + let mut session = [0; SESSION_ID_LENGTH]; + + if let Ok(mut file) = fs::File::open(self.cache_dir.join(SESSION_FILE_NAME)) { + file.read_exact(&mut session).map_err(|e| TrezorError::CacheError(e.to_string()))?; + Ok(Some(session.to_vec())) + } else { + Ok(None) + } + } + + fn save_session(&mut self, session_id: Vec) -> Result<(), TrezorError> { + fs::create_dir_all(&self.cache_dir).map_err(|e| TrezorError::CacheError(e.to_string()))?; + + let mut file = fs::File::create(self.cache_dir.join(SESSION_FILE_NAME)) + .map_err(|e| TrezorError::CacheError(e.to_string()))?; + + file.write_all(&session_id).map_err(|e| TrezorError::CacheError(e.to_string()))?; + + self.session_id = session_id; + Ok(()) + } + + fn initate_session(&mut self) -> Result<(), TrezorError> { + let mut client = trezor_client::unique(false)?; + client.init_device(self.get_cached_session()?)?; + + let features = client.features().ok_or(TrezorError::FeaturesError)?; + + Self::check_version(format!( + "{}.{}.{}", + features.major_version(), + features.minor_version(), + features.patch_version() + ))?; + + self.save_session(features.session_id().to_vec())?; + + Ok(()) + } + + /// You need to drop(client) once you're done with it + fn get_client(&self, session_id: Vec) -> Result { + let mut client = trezor_client::unique(false)?; + client.init_device(Some(session_id))?; + Ok(client) + } + + /// Get the account which corresponds to our derivation path + pub async fn get_address(&self) -> Result { + self.get_address_with_path(&self.derivation).await + } + + /// Gets the account which corresponds to the provided derivation path + pub async fn get_address_with_path( + &self, + derivation: &DerivationType, + ) -> Result { + let mut client = self.get_client(self.session_id.clone())?; + let address_str = client.ethereum_get_address(Self::convert_path(derivation))?; + let mut address_bytes = [0; 20]; + hex::decode_to_slice(address_str, &mut address_bytes)?; + Ok(address_bytes.into()) + } + + /// Signs an Ethereum transaction (requires confirmation on the Trezor) + pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { + let mut client = self.get_client(self.session_id.clone())?; + + let arr_path = Self::convert_path(&self.derivation); + + let transaction = TrezorTransaction::load(tx)?; + + let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + + let signature = match tx { + TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx( + arr_path, + transaction.nonce, + transaction.gas_price, + transaction.gas, + transaction.to, + transaction.value, + transaction.data, + chain_id, + )?, + TypedTransaction::Eip1559(eip1559_tx) => client.ethereum_sign_eip1559_tx( + arr_path, + transaction.nonce, + transaction.gas, + transaction.to, + transaction.value, + transaction.data, + chain_id, + transaction.max_fee_per_gas, + transaction.max_priority_fee_per_gas, + transaction.access_list, + )?, + #[cfg(feature = "optimism")] + TypedTransaction::DepositTransaction(tx) => { + trezor_client::client::Signature { r: 0.into(), s: 0.into(), v: 0 } + } + }; + + Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) + } + + /// Signs an ethereum personal message + pub async fn sign_message>( + &self, + message: &[u8], + ) -> Result { + let message = message.as_ref(); + let mut client = self.get_client(self.session_id.clone())?; + let apath = Self::convert_path(&self.derivation); + + let signature = client.ethereum_sign_message(message.into(), apath)?; + + Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) + } + + /// Signs an EIP712 encoded domain separator and message + pub async fn sign_typed_struct(&self, payload: &T) -> Result + where + T: Eip712, + { + unimplemented!() + } + + // helper which converts a derivation path to [u32] + fn convert_path(derivation: &DerivationType) -> Vec { + let derivation = derivation.to_string(); + let elements = derivation.split('/').skip(1).collect::>(); + let depth = elements.len(); + + let mut path = vec![]; + for derivation_index in elements { + let hardened = derivation_index.contains('\''); + let mut index = derivation_index.replace('\'', "").parse::().unwrap(); + if hardened { + index |= 0x80000000; + } + path.push(index); + } + + path + } +} + +#[cfg(all(test, feature = "trezor"))] +mod tests { + use super::*; + use crate::Signer; + use ethers_core::types::{ + transaction::eip2930::{AccessList, AccessListItem}, + Address, Eip1559TransactionRequest, TransactionRequest, I256, U256, + }; + use std::str::FromStr; + + #[tokio::test] + #[ignore] + // Replace this with your ETH addresses. + async fn test_get_address() { + // Instantiate it with the default trezor derivation path + let trezor = + TrezorEthereum::new(DerivationType::TrezorLive(1), 1, Some(PathBuf::from("randomdir"))) + .await + .unwrap(); + assert_eq!( + trezor.get_address().await.unwrap(), + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + ); + assert_eq!( + trezor.get_address_with_path(&DerivationType::TrezorLive(0)).await.unwrap(), + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + ); + } + + #[tokio::test] + #[ignore] + async fn test_sign_tx() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + + // approve uni v2 router 0xff + let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); + + let tx_req = TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .gas(1000000) + .gas_price(400e9 as u64) + .nonce(5) + .data(data) + .value(ethers_core::utils::parse_ether(100).unwrap()) + .into(); + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_sign_big_data_tx() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + + // invalid data + let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + "aa").unwrap(); + let tx_req = TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .gas(1000000) + .gas_price(400e9 as u64) + .nonce(5) + .data(big_data) + .value(ethers_core::utils::parse_ether(100).unwrap()) + .into(); + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_sign_empty_txes() { + // Contract creation (empty `to`), requires data. + // To test without the data field, we need to specify a `to` address. + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + { + let tx_req = Eip1559TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .into(); + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + { + let tx_req = TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .into(); + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + + let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); + + // Contract creation (empty `to`, with data) should show on the trezor device as: + // ` "0 Wei ETH + // ` new contract?" + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + { + let tx_req = Eip1559TransactionRequest::new().data(data.clone()).into(); + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + { + let tx_req = TransactionRequest::new().data(data.clone()).into(); + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + } + + #[tokio::test] + #[ignore] + async fn test_sign_eip1559_tx() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + + // approve uni v2 router 0xff + let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); + + let lst = AccessList(vec![ + AccessListItem { + address: "0x8ba1f109551bd432803012645ac136ddd64dba72".parse().unwrap(), + storage_keys: vec![ + "0x0000000000000000000000000000000000000000000000000000000000000000" + .parse() + .unwrap(), + "0x0000000000000000000000000000000000000000000000000000000000000042" + .parse() + .unwrap(), + ], + }, + AccessListItem { + address: "0x2ed7afa17473e17ac59908f088b4371d28585476".parse().unwrap(), + storage_keys: vec![ + "0x0000000000000000000000000000000000000000000000000000000000000000" + .parse() + .unwrap(), + "0x0000000000000000000000000000000000000000000000000000000000000042" + .parse() + .unwrap(), + ], + }, + ]); + + let tx_req = Eip1559TransactionRequest::new() + .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) + .gas(1000000) + .max_fee_per_gas(400e9 as u64) + .max_priority_fee_per_gas(400e9 as u64) + .nonce(5) + .data(data) + .access_list(lst) + .value(ethers_core::utils::parse_ether(100).unwrap()) + .into(); + + let tx = trezor.sign_transaction(&tx_req).await.unwrap(); + } + + #[tokio::test] + #[ignore] + async fn test_sign_message() { + let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let message = "hello world"; + let sig = trezor.sign_message(message).await.unwrap(); + let addr = trezor.get_address().await.unwrap(); + sig.verify(message, addr).unwrap(); + } +} diff --git a/crates/signers/src/trezor/mod.rs b/crates/signers/src/trezor/mod.rs new file mode 100644 index 00000000000..f4012d1597e --- /dev/null +++ b/crates/signers/src/trezor/mod.rs @@ -0,0 +1,51 @@ +pub mod app; +pub mod types; + +use crate::Signer; +use app::TrezorEthereum; +use async_trait::async_trait; +use ethers_core::types::{ + transaction::{eip2718::TypedTransaction, eip712::Eip712}, + Address, Signature, +}; +use types::TrezorError; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Signer for TrezorEthereum { + type Error = TrezorError; + + async fn sign_message(&self, message: &[u8]) -> Result { + self.sign_message(message).await + } + + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + let mut tx_with_chain = message.clone(); + if tx_with_chain.chain_id().is_none() { + // in the case we don't have a chain_id, let's use the signer chain id instead + tx_with_chain.set_chain_id(self.chain_id); + } + self.sign_tx(&tx_with_chain).await + } + + #[cfg(TODO)] + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result { + self.sign_typed_struct(payload).await + } + + fn address(&self) -> Address { + self.address + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } + + fn chain_id(&self) -> u64 { + self.chain_id + } +} diff --git a/crates/signers/src/trezor/types.rs b/crates/signers/src/trezor/types.rs new file mode 100644 index 00000000000..045a29ea1f4 --- /dev/null +++ b/crates/signers/src/trezor/types.rs @@ -0,0 +1,155 @@ +//! Helpers for interacting with the Ethereum Trezor App. +//! +//! [Official Docs](https://github.com/TrezorHQ/app-ethereum/blob/master/doc/ethapp.asc) + +#![allow(clippy::upper_case_acronyms)] + +use std::fmt; +use thiserror::Error; + +use ethers_core::types::{transaction::eip2718::TypedTransaction, NameOrAddress, U256}; +use trezor_client::client::AccessListItem as Trezor_AccessListItem; + +#[derive(Clone, Debug)] +/// Trezor wallet type +pub enum DerivationType { + /// Trezor Live-generated HD path + TrezorLive(usize), + /// Any other path. Attention! Trezor by default forbids custom derivation paths + /// Run trezorctl set safety-checks prompt, to allow it + Other(String), +} + +impl fmt::Display for DerivationType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "{}", + match self { + DerivationType::TrezorLive(index) => format!("m/44'/60'/{index}'/0/0"), + DerivationType::Other(inner) => inner.to_owned(), + } + ) + } +} + +#[derive(Error, Debug)] +/// Error when using the Trezor transport +pub enum TrezorError { + /// Underlying Trezor transport error + #[error(transparent)] + TrezorError(#[from] trezor_client::error::Error), + #[error("Trezor was not able to retrieve device features")] + FeaturesError, + #[error("Not able to unpack value for TrezorTransaction.")] + DataError, + /// Error when converting from a hex string + #[error(transparent)] + HexError(#[from] hex::FromHexError), + /// Error when converting a semver requirement + #[error(transparent)] + SemVerError(#[from] semver::Error), + /// Error when signing EIP712 struct with not compatible Trezor ETH app + #[error("Trezor ethereum app requires at least version: {0:?}")] + UnsupportedFirmwareVersion(String), + #[error("Does not support ENS.")] + NoENSSupport, + #[error("Unable to access trezor cached session.")] + CacheError(String), +} + +/// Trezor Transaction Struct +pub struct TrezorTransaction { + pub nonce: Vec, + pub gas: Vec, + pub gas_price: Vec, + pub value: Vec, + pub to: String, + pub data: Vec, + pub max_fee_per_gas: Vec, + pub max_priority_fee_per_gas: Vec, + pub access_list: Vec, +} + +impl TrezorTransaction { + fn to_trimmed_big_endian(_value: &U256) -> Vec { + let mut trimmed_value = [0_u8; 32]; + _value.to_big_endian(&mut trimmed_value); + trimmed_value[_value.leading_zeros() as usize / 8..].to_vec() + } + + pub fn load(tx: &TypedTransaction) -> Result { + let to: String = match tx.to() { + Some(v) => match v { + NameOrAddress::Name(_) => return Err(TrezorError::NoENSSupport), + NameOrAddress::Address(value) => hex::encode_prefixed(value), + }, + // Contract Creation + None => "".to_string(), + }; + + let nonce = tx.nonce().map_or(vec![], Self::to_trimmed_big_endian); + let gas = tx.gas().map_or(vec![], Self::to_trimmed_big_endian); + let gas_price = tx.gas_price().map_or(vec![], |v| Self::to_trimmed_big_endian(&v)); + let value = tx.value().map_or(vec![], Self::to_trimmed_big_endian); + let data = tx.data().map_or(vec![], |v| v.to_vec()); + + match tx { + TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => Ok(Self { + nonce, + gas, + gas_price, + value, + to, + data, + max_fee_per_gas: vec![], + max_priority_fee_per_gas: vec![], + access_list: vec![], + }), + TypedTransaction::Eip1559(eip1559_tx) => { + let max_fee_per_gas = + eip1559_tx.max_fee_per_gas.map_or(vec![], |v| Self::to_trimmed_big_endian(&v)); + + let max_priority_fee_per_gas = eip1559_tx + .max_priority_fee_per_gas + .map_or(vec![], |v| Self::to_trimmed_big_endian(&v)); + + let mut access_list: Vec = Vec::new(); + for item in &eip1559_tx.access_list.0 { + let address: String = hex::encode_prefixed(item.address); + let mut storage_keys: Vec> = Vec::new(); + + for key in &item.storage_keys { + storage_keys.push(key.as_bytes().to_vec()) + } + + access_list.push(Trezor_AccessListItem { address, storage_keys }) + } + + Ok(Self { + nonce, + gas, + gas_price, + value, + to, + data, + max_fee_per_gas, + max_priority_fee_per_gas, + access_list, + }) + } + #[cfg(feature = "optimism")] + TypedTransaction::DepositTransaction(_) => Ok(Self { + nonce, + gas, + gas_price, + value, + to, + data, + max_fee_per_gas: vec![], + max_priority_fee_per_gas: vec![], + access_list: vec![], + }), + } + } +} diff --git a/crates/signers/src/utils.rs b/crates/signers/src/utils.rs new file mode 100644 index 00000000000..0c8f915af5b --- /dev/null +++ b/crates/signers/src/utils.rs @@ -0,0 +1,85 @@ +//! Utility functions for working with Ethereum signatures. + +use alloy_primitives::{keccak256, Address}; +use elliptic_curve::sec1::ToEncodedPoint; +use k256::{ + ecdsa::{SigningKey, VerifyingKey}, + AffinePoint, +}; + +/// Applies [EIP-155](https://eips.ethereum.org/EIPS/eip-155). +pub fn to_eip155_v(recovery_id: u8, chain_id: u64) -> u64 { + (recovery_id as u64) + 35 + chain_id * 2 +} + +/// Converts an ECDSA private key to its corresponding Ethereum Address. +#[inline] +pub fn secret_key_to_address(secret_key: &SigningKey) -> Address { + public_key_to_address(secret_key.verifying_key()) +} + +/// Converts an ECDSA public key to its corresponding Ethereum address. +#[inline] +pub fn public_key_to_address(pubkey: &VerifyingKey) -> Address { + let affine: &AffinePoint = pubkey.as_ref(); + let encoded = affine.to_encoded_point(false); + raw_public_key_to_address(&encoded.as_bytes()[1..]) +} + +/// Convert a raw, uncompressed public key to its corresponding Ethereum address. +/// +/// ### Warning +/// +/// This method **does not** verify that the public key is valid. It is the +/// caller's responsibility to pass a valid public key. Passing an invalid +/// public key will produce an unspendable output. +/// +/// # Panics +/// +/// This function panics if the input is not **exactly** 64 bytes. +#[inline] +#[track_caller] +pub fn raw_public_key_to_address(pubkey: &[u8]) -> Address { + assert_eq!(pubkey.len(), 64, "raw public key must be 64 bytes"); + let digest = keccak256(pubkey); + Address::from_slice(&digest[12..]) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::hex; + + // Only tests for correctness, no edge cases. Uses examples from https://docs.ethers.org/v5/api/utils/address/#utils-computeAddress + #[test] + fn test_public_key_to_address() { + let addr = "0Ac1dF02185025F65202660F8167210A80dD5086".parse::
().unwrap(); + + // Compressed + let pubkey = VerifyingKey::from_sec1_bytes( + &hex::decode("0376698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762") + .unwrap(), + ) + .unwrap(); + assert_eq!(public_key_to_address(&pubkey), addr); + + // Uncompressed + let pubkey= VerifyingKey::from_sec1_bytes(&hex::decode("0476698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762d46ca56d3dad2ce13213a6f42278dabbb53259f2d92681ea6a0b98197a719be3").unwrap()).unwrap(); + assert_eq!(public_key_to_address(&pubkey), addr); + } + + #[test] + fn test_raw_public_key_to_address() { + let addr = "0Ac1dF02185025F65202660F8167210A80dD5086".parse::
().unwrap(); + + let pubkey_bytes = hex::decode("76698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762d46ca56d3dad2ce13213a6f42278dabbb53259f2d92681ea6a0b98197a719be3").unwrap(); + + assert_eq!(raw_public_key_to_address(&pubkey_bytes), addr); + } + + #[test] + #[should_panic] + fn test_raw_public_key_to_address_panics() { + raw_public_key_to_address(&[]); + } +} diff --git a/crates/signers/src/wallet/mnemonic.rs b/crates/signers/src/wallet/mnemonic.rs new file mode 100644 index 00000000000..a752ebaa1e3 --- /dev/null +++ b/crates/signers/src/wallet/mnemonic.rs @@ -0,0 +1,262 @@ +//! Specific helper functions for creating/loading a mnemonic private key following BIP-39 +//! specifications. + +use crate::{utils::secret_key_to_address, Wallet, WalletError}; +use coins_bip32::path::DerivationPath; +use coins_bip39::{Mnemonic, Wordlist}; +use k256::ecdsa::SigningKey; +use rand::Rng; +use std::{fs::File, io::Write, marker::PhantomData, path::PathBuf, str::FromStr}; +use thiserror::Error; + +const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/"; +const DEFAULT_DERIVATION_PATH: &str = "m/44'/60'/0'/0/0"; + +/// Represents a structure that can resolve into a `Wallet`. +#[derive(Clone, Debug, PartialEq, Eq)] +#[must_use = "builders do nothing unless `build` is called"] +pub struct MnemonicBuilder { + /// The mnemonic phrase can be supplied to the builder as a string. A builder that has a valid + /// phrase should `build` the wallet. + phrase: Option, + /// The mnemonic builder can also be asked to generate a new random wallet by providing the + /// number of words in the phrase. By default this is set to 12. + word_count: usize, + /// The derivation path at which the extended private key child will be derived at. By default + /// the mnemonic builder uses the path: "m/44'/60'/0'/0/0". + derivation_path: DerivationPath, + /// Optional password for the mnemonic phrase. + password: Option, + /// Optional field that if enabled, writes the mnemonic phrase to disk storage at the provided + /// path. + write_to: Option, + /// PhantomData + _wordlist: PhantomData, +} + +/// Error produced by the mnemonic wallet module +#[derive(Error, Debug)] +pub enum MnemonicBuilderError { + /// Error suggests that a phrase (path or words) was expected but not found + #[error("Expected phrase not found")] + ExpectedPhraseNotFound, + /// Error suggests that a phrase (path or words) was not expected but found + #[error("Unexpected phrase found")] + UnexpectedPhraseFound, +} + +impl Default for MnemonicBuilder { + fn default() -> Self { + Self { + phrase: None, + word_count: 12usize, + derivation_path: DEFAULT_DERIVATION_PATH.parse().unwrap(), + password: None, + write_to: None, + _wordlist: PhantomData, + } + } +} + +impl MnemonicBuilder { + /// Sets the phrase in the mnemonic builder. The phrase can either be a string or a path to + /// the file that contains the phrase. Once a phrase is provided, the key will be generated + /// deterministically by calling the `build` method. + /// + /// # Examples + /// + /// ``` + /// use alloy_signers::{MnemonicBuilder, coins_bip39::English}; + /// # async fn foo() -> Result<(), Box> { + /// + /// let wallet = MnemonicBuilder::::default() + /// .phrase("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about") + /// .build()?; + /// + /// # Ok(()) + /// # } + /// ``` + pub fn phrase>(mut self, phrase: P) -> Self { + self.phrase = Some(phrase.into()); + self + } + + /// Sets the word count of a mnemonic phrase to be generated at random. If the `phrase` field + /// is set, then `word_count` will be ignored. + /// + /// # Examples + /// + /// ``` + /// use alloy_signers::{coins_bip39::English, MnemonicBuilder}; + /// # async fn foo() -> Result<(), Box> { + /// + /// let mut rng = rand::thread_rng(); + /// let wallet = MnemonicBuilder::::default().word_count(24).build_random(&mut rng)?; + /// + /// # Ok(()) + /// # } + /// ``` + pub fn word_count(mut self, count: usize) -> Self { + self.word_count = count; + self + } + + /// Sets the derivation path of the child key to be derived. The derivation path is calculated + /// using the default derivation path prefix used in Ethereum, i.e. "m/44'/60'/0'/0/{index}". + pub fn index(mut self, index: u32) -> Result { + self.derivation_path(&format!("{DEFAULT_DERIVATION_PATH_PREFIX}{index}")) + } + + /// Sets the derivation path of the child key to be derived. + pub fn derivation_path>(mut self, path: T) -> Result { + self.derivation_path = DerivationPath::from_str(path.as_ref())?; + Ok(self) + } + + /// Sets the password used to construct the seed from the mnemonic phrase. + pub fn password>(mut self, password: T) -> Self { + self.password = Some(password.into()); + self + } + + /// Sets the path to which the randomly generated phrase will be written to. This field is + /// ignored when building a wallet from the provided mnemonic phrase. + pub fn write_to>(mut self, path: P) -> Self { + self.write_to = Some(path.into()); + self + } + + /// Builds a `LocalWallet` using the parameters set in mnemonic builder. This method expects + /// the phrase field to be set. + pub fn build(&self) -> Result, WalletError> { + let mnemonic = match &self.phrase { + Some(phrase) => Mnemonic::::new_from_phrase(phrase)?, + None => return Err(MnemonicBuilderError::ExpectedPhraseNotFound.into()), + }; + self.mnemonic_to_wallet(&mnemonic) + } + + /// Builds a `LocalWallet` using the parameters set in the mnemonic builder and constructing + /// the phrase using the provided random number generator. + pub fn build_random(&self, rng: &mut R) -> Result, WalletError> { + let mnemonic = match &self.phrase { + None => Mnemonic::::new_with_count(rng, self.word_count)?, + _ => return Err(MnemonicBuilderError::UnexpectedPhraseFound.into()), + }; + let wallet = self.mnemonic_to_wallet(&mnemonic)?; + + // Write the mnemonic phrase to storage if a directory has been provided. + if let Some(dir) = &self.write_to { + let mut file = File::create(dir.as_path().join(wallet.address.to_string()))?; + file.write_all(mnemonic.to_phrase().as_bytes())?; + } + + Ok(wallet) + } + + fn mnemonic_to_wallet( + &self, + mnemonic: &Mnemonic, + ) -> Result, WalletError> { + let derived_priv_key = + mnemonic.derive_key(&self.derivation_path, self.password.as_deref())?; + let key: &coins_bip32::prelude::SigningKey = derived_priv_key.as_ref(); + let signer = SigningKey::from_bytes(&key.to_bytes())?; + let address = secret_key_to_address(&signer); + + Ok(Wallet:: { signer, address, chain_id: 1 }) + } +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use super::*; + + use crate::coins_bip39::English; + use tempfile::tempdir; + + const TEST_DERIVATION_PATH: &str = "m/44'/60'/0'/2/1"; + + #[tokio::test] + async fn mnemonic_deterministic() { + // Testcases have been taken from MyCryptoWallet + const TESTCASES: [(&str, u32, Option<&str>, &str); 4] = [ + ( + "work man father plunge mystery proud hollow address reunion sauce theory bonus", + 0u32, + Some("TREZOR123"), + "0x431a00DA1D54c281AeF638A73121B3D153e0b0F6", + ), + ( + "inject danger program federal spice bitter term garbage coyote breeze thought funny", + 1u32, + Some("LEDGER321"), + "0x231a3D0a05d13FAf93078C779FeeD3752ea1350C", + ), + ( + "fire evolve buddy tenant talent favorite ankle stem regret myth dream fresh", + 2u32, + None, + "0x1D86AD5eBb2380dAdEAF52f61f4F428C485460E9", + ), + ( + "thumb soda tape crunch maple fresh imitate cancel order blind denial giraffe", + 3u32, + None, + "0xFB78b25f69A8e941036fEE2A5EeAf349D81D4ccc", + ), + ]; + TESTCASES.iter().for_each(|&(phrase, index, password, expected_addr)| { + let wallet = match password { + Some(psswd) => MnemonicBuilder::::default() + .phrase(phrase) + .index(index) + .unwrap() + .password(psswd) + .build() + .unwrap(), + None => MnemonicBuilder::::default() + .phrase(phrase) + .index(index) + .unwrap() + .build() + .unwrap(), + }; + assert_eq!(&wallet.address.to_string(), expected_addr); + }) + } + + #[tokio::test] + async fn mnemonic_write_read() { + let dir = tempdir().unwrap(); + + // Construct a wallet from random mnemonic phrase and write it to the temp dir. + let mut rng = rand::thread_rng(); + let wallet1 = MnemonicBuilder::::default() + .word_count(24) + .derivation_path(TEST_DERIVATION_PATH) + .unwrap() + .write_to(dir.as_ref()) + .build_random(&mut rng) + .unwrap(); + + // Ensure that only one file has been created. + let paths = std::fs::read_dir(dir.as_ref()).unwrap(); + assert_eq!(paths.count(), 1); + + // Use the newly created file's path to instantiate wallet. + let phrase_path = dir.as_ref().join(wallet1.address.to_string()); + let wallet2 = MnemonicBuilder::::default() + .phrase(phrase_path.to_str().unwrap()) + .derivation_path(TEST_DERIVATION_PATH) + .unwrap() + .build() + .unwrap(); + + // Ensure that both wallets belong to the same address. + assert_eq!(wallet1.address, wallet2.address); + + dir.close().unwrap(); + } +} diff --git a/crates/signers/src/wallet/mod.rs b/crates/signers/src/wallet/mod.rs new file mode 100644 index 00000000000..2f0c7320e41 --- /dev/null +++ b/crates/signers/src/wallet/mod.rs @@ -0,0 +1,141 @@ +use crate::{utils::to_eip155_v, Signature, Signer}; +use alloy_primitives::{utils::eip191_hash_message, Address, B256}; +use async_trait::async_trait; +use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}; +use std::fmt; + +mod mnemonic; +pub use mnemonic::MnemonicBuilder; + +mod private_key; +pub use private_key::WalletError; + +#[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] +mod yubi; + +/// An Ethereum private-public key pair which can be used for signing messages. +/// +/// # Examples +/// +/// ## Signing and Verifying a message +/// +/// The wallet can be used to produce ECDSA [`Signature`] objects, which can be +/// then verified. Note that this uses [`eip191_hash_message`] under the hood which will +/// prefix the message being hashed with the `Ethereum Signed Message` domain separator. +/// +/// ``` +/// use ethers_core::rand::thread_rng; +/// use ethers_signers::{LocalWallet, Signer}; +/// +/// # async fn foo() -> Result<(), Box> { +/// let wallet = LocalWallet::new(&mut thread_rng()); +/// +/// // Optionally, the wallet's chain id can be set, in order to use EIP-155 +/// // replay protection with different chains +/// let wallet = wallet.with_chain_id(1337u64); +/// +/// // The wallet can be used to sign messages +/// let message = b"hello"; +/// let signature = wallet.sign_message(message).await?; +/// assert_eq!(signature.recover(&message[..]).unwrap(), wallet.address()); +/// +/// // LocalWallet is clonable: +/// let wallet_clone = wallet.clone(); +/// let signature2 = wallet_clone.sign_message(message).await?; +/// assert_eq!(signature, signature2); +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone)] +pub struct Wallet> { + /// The wallet's private key. + pub(crate) signer: D, + /// The wallet's address. + pub(crate) address: Address, + /// The wallet's chain ID (for EIP-155). + pub(crate) chain_id: u64, +} + +impl> Wallet { + /// Construct a new wallet with an external Signer + pub fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { + Wallet { signer, address, chain_id } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl> Signer for Wallet { + type Error = WalletError; + + async fn sign_message(&self, message: &[u8]) -> Result { + let message_hash = eip191_hash_message(message); + self.sign_hash(message_hash) + } + + async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { + self.sign_transaction_sync(tx) + } + + #[cfg(TODO)] + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result { + let encoded = + payload.encode_eip712().map_err(|e| Self::Error::Eip712Error(e.to_string()))?; + + self.sign_hash(B256::from(encoded)) + } + + fn address(&self) -> Address { + self.address + } + + fn chain_id(&self) -> u64 { + self.chain_id + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } +} + +impl> Wallet { + /// Synchronously signs the provided transaction, normalizing the signature `v` value with + /// EIP-155 using the transaction's `chain_id`, or the signer's `chain_id` if the transaction + /// does not specify one. + pub fn sign_transaction_sync(&self, tx: &TypedTransaction) -> Result { + // rlp (for sighash) must have the same chain id as v in the signature + let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + let mut tx = tx.clone(); + tx.set_chain_id(chain_id); + + let sighash = tx.sighash(); + let mut sig = self.sign_hash(sighash)?; + sig.set_v(to_eip155_v(sig.recid().to_byte(), chain_id)); + Ok(sig) + } + + /// Signs the provided hash. + pub fn sign_hash(&self, hash: B256) -> Result { + let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; + Ok(Signature::new(recoverable_sig, recovery_id)) + } + + /// Gets the wallet's signer + pub fn signer(&self) -> &D { + &self.signer + } +} + +// do not log the signer +impl> fmt::Debug for Wallet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Wallet") + .field("address", &self.address) + .field("chain_Id", &self.chain_id) + .finish() + } +} diff --git a/crates/signers/src/wallet/private_key.rs b/crates/signers/src/wallet/private_key.rs new file mode 100644 index 00000000000..0d16c74c319 --- /dev/null +++ b/crates/signers/src/wallet/private_key.rs @@ -0,0 +1,448 @@ +//! Specific helper functions for loading an offline K256 Private Key stored on disk + +use super::{mnemonic::MnemonicBuilderError, Wallet}; +use crate::utils::secret_key_to_address; +use alloy_primitives::hex; +use coins_bip32::Bip32Error; +use coins_bip39::MnemonicError; +use k256::{ + ecdsa::{self, SigningKey}, + SecretKey as K256SecretKey, +}; +use rand::{CryptoRng, Rng}; +use std::str::FromStr; +use thiserror::Error; + +#[cfg(not(target_arch = "wasm32"))] +use elliptic_curve::rand_core; +#[cfg(not(target_arch = "wasm32"))] +use eth_keystore::KeystoreError; +#[cfg(not(target_arch = "wasm32"))] +use std::path::Path; + +/// Error thrown by the Wallet module +#[derive(Debug, Error)] +pub enum WalletError { + /// Error propagated from the BIP-32 crate + #[error(transparent)] + Bip32Error(#[from] Bip32Error), + /// Error propagated from the BIP-39 crate + #[error(transparent)] + Bip39Error(#[from] MnemonicError), + /// Underlying eth keystore error + #[cfg(not(target_arch = "wasm32"))] + #[error(transparent)] + EthKeystoreError(#[from] KeystoreError), + /// Error propagated from k256's ECDSA module + #[error(transparent)] + EcdsaError(#[from] ecdsa::Error), + /// Error propagated from the hex crate. + #[error(transparent)] + HexError(#[from] hex::FromHexError), + /// Error propagated by IO operations + #[error(transparent)] + IoError(#[from] std::io::Error), + /// Error propagated from the mnemonic builder module. + #[error(transparent)] + MnemonicBuilderError(#[from] MnemonicBuilderError), + /// Error type from Eip712Error message + #[error("error encoding eip712 struct: {0:?}")] + Eip712Error(String), +} + +impl Wallet { + /// Creates a new random encrypted JSON with the provided password and stores it in the + /// provided directory. Returns a tuple (Wallet, String) of the wallet instance for the + /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`, + /// the keystore is stored as the stringified UUID. + #[cfg(not(target_arch = "wasm32"))] + pub fn new_keystore( + dir: P, + rng: &mut R, + password: S, + name: Option<&str>, + ) -> Result<(Self, String), WalletError> + where + P: AsRef, + R: Rng + CryptoRng + rand_core::CryptoRng, + S: AsRef<[u8]>, + { + let (secret, uuid) = eth_keystore::new(dir, rng, password, name)?; + let signer = SigningKey::from_bytes(secret.as_slice().into())?; + let address = secret_key_to_address(&signer); + Ok((Self { signer, address, chain_id: 1 }, uuid)) + } + + /// Decrypts an encrypted JSON from the provided path to construct a Wallet instance + #[cfg(not(target_arch = "wasm32"))] + pub fn decrypt_keystore(keypath: P, password: S) -> Result + where + P: AsRef, + S: AsRef<[u8]>, + { + let secret = eth_keystore::decrypt_key(keypath, password)?; + let signer = SigningKey::from_bytes(secret.as_slice().into())?; + let address = secret_key_to_address(&signer); + Ok(Self { signer, address, chain_id: 1 }) + } + + /// Creates a new encrypted JSON with the provided private key and password and stores it in the + /// provided directory. Returns a tuple (Wallet, String) of the wallet instance for the + /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`, + /// the keystore is stored as the stringified UUID. + #[cfg(not(target_arch = "wasm32"))] + pub fn encrypt_keystore( + keypath: P, + rng: &mut R, + pk: B, + password: S, + name: Option<&str>, + ) -> Result<(Self, String), WalletError> + where + P: AsRef, + R: Rng + CryptoRng, + B: AsRef<[u8]>, + S: AsRef<[u8]>, + { + let uuid = eth_keystore::encrypt_key(keypath, rng, &pk, password, name)?; + let signer = SigningKey::from_slice(pk.as_ref())?; + let address = secret_key_to_address(&signer); + Ok((Self { signer, address, chain_id: 1 }, uuid)) + } + + /// Creates a new random keypair seeded with the provided RNG + pub fn new(rng: &mut R) -> Self { + let signer = SigningKey::random(rng); + let address = secret_key_to_address(&signer); + Self { signer, address, chain_id: 1 } + } + + /// Creates a new Wallet instance from a raw scalar value (big endian). + pub fn from_bytes(bytes: &[u8]) -> Result { + let signer = SigningKey::from_bytes(bytes.into())?; + let address = secret_key_to_address(&signer); + Ok(Self { signer, address, chain_id: 1 }) + } +} + +impl PartialEq for Wallet { + fn eq(&self, other: &Self) -> bool { + self.signer.to_bytes().eq(&other.signer.to_bytes()) + && self.address == other.address + && self.chain_id == other.chain_id + } +} + +impl From for Wallet { + fn from(signer: SigningKey) -> Self { + let address = secret_key_to_address(&signer); + + Self { signer, address, chain_id: 1 } + } +} + +impl From for Wallet { + fn from(key: K256SecretKey) -> Self { + let signer = key.into(); + let address = secret_key_to_address(&signer); + + Self { signer, address, chain_id: 1 } + } +} + +impl FromStr for Wallet { + type Err = WalletError; + + fn from_str(src: &str) -> Result { + let src = hex::decode(src.strip_prefix("0X").unwrap_or(src))?; + + if src.len() != 32 { + return Err(WalletError::HexError(hex::FromHexError::InvalidStringLength)); + } + + let sk = SigningKey::from_bytes(src.as_slice().into())?; + Ok(sk.into()) + } +} + +impl TryFrom<&str> for Wallet { + type Error = WalletError; + + fn try_from(value: &str) -> Result { + value.parse() + } +} + +impl TryFrom for Wallet { + type Error = WalletError; + + fn try_from(value: String) -> Result { + value.parse() + } +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use super::*; + use crate::{LocalWallet, Signer, TransactionRequest, TypedTransaction}; + use alloy_primitives::Address; + use tempfile::tempdir; + + #[test] + fn parse_pk() { + let s = "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b"; + let _pk: Wallet = s.parse().unwrap(); + } + + #[test] + fn parse_short_key() { + let s = "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea3"; + assert!(s.len() < 64); + let pk = s.parse::().unwrap_err(); + match pk { + WalletError::HexError(hex::FromHexError::InvalidStringLength) => {} + _ => panic!("Unexpected error"), + } + } + + async fn test_encrypted_json_keystore(key: Wallet, uuid: &str, dir: &Path) { + // sign a message using the given key + let message = "Some data"; + let signature = key.sign_message(message.as_bytes()).await.unwrap(); + + // read from the encrypted JSON keystore and decrypt it, while validating that the + // signatures produced by both the keys should match + let path = Path::new(dir).join(uuid); + let key2 = Wallet::::decrypt_keystore(path.clone(), "randpsswd").unwrap(); + + let signature2 = key2.sign_message(message.as_bytes()).await.unwrap(); + assert_eq!(signature, signature2); + + std::fs::remove_file(&path).unwrap(); + } + + #[tokio::test] + async fn encrypted_json_keystore_new() { + // create and store an encrypted JSON keystore in this directory + let dir = tempdir().unwrap(); + let mut rng = rand::thread_rng(); + let (key, uuid) = + Wallet::::new_keystore(&dir, &mut rng, "randpsswd", None).unwrap(); + + test_encrypted_json_keystore(key, &uuid, dir.path()).await; + } + + #[tokio::test] + async fn encrypted_json_keystore_from_pk() { + // create and store an encrypted JSON keystore in this directory + let dir = tempdir().unwrap(); + let mut rng = rand::thread_rng(); + + let private_key = + hex::decode("6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea30b") + .unwrap(); + + let (key, uuid) = + Wallet::::encrypt_keystore(&dir, &mut rng, private_key, "randpsswd", None) + .unwrap(); + + test_encrypted_json_keystore(key, &uuid, dir.path()).await; + } + + #[tokio::test] + async fn signs_msg() { + let message = "Some data"; + let hash = alloy_primitives::utils::eip191_hash_message(message); + let key = Wallet::::new(&mut rand::thread_rng()); + let address = key.address; + + // sign a message + let signature = key.sign_message(message).await.unwrap(); + + // ecrecover via the message will hash internally + let recovered = signature.recover(message).unwrap(); + + // if provided with a hash, it will skip hashing + let recovered2 = signature.recover(hash).unwrap(); + + // verifies the signature is produced by `address` + signature.verify(message, address).unwrap(); + + assert_eq!(recovered, address); + assert_eq!(recovered2, address); + } + + #[tokio::test] + async fn signs_tx() { + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + let tx: TypedTransaction = TransactionRequest { + from: None, + to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::
().unwrap().into()), + value: Some(1_000_000_000.into()), + gas: Some(2_000_000.into()), + nonce: Some(0.into()), + gas_price: Some(21_000_000_000u128.into()), + data: None, + chain_id: Some(U64::one()), + } + .into(); + let wallet: Wallet = + "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); + let wallet = wallet.with_chain_id(tx.chain_id().unwrap().as_u64()); + + let sig = wallet.sign_transaction(&tx).await.unwrap(); + let sighash = tx.sighash(); + sig.verify(sighash, wallet.address).unwrap(); + } + + #[tokio::test] + async fn signs_tx_empty_chain_id() { + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + let tx: TypedTransaction = TransactionRequest { + from: None, + to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::
().unwrap().into()), + value: Some(1_000_000_000.into()), + gas: Some(2_000_000.into()), + nonce: Some(0.into()), + gas_price: Some(21_000_000_000u128.into()), + data: None, + chain_id: None, + } + .into(); + let wallet: Wallet = + "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); + let wallet = wallet.with_chain_id(1u64); + + // this should populate the tx chain_id as the signer's chain_id (1) before signing + let sig = wallet.sign_transaction(&tx).await.unwrap(); + + // since we initialize with None we need to re-set the chain_id for the sighash to be + // correct + let mut tx = tx; + tx.set_chain_id(1); + let sighash = tx.sighash(); + sig.verify(sighash, wallet.address).unwrap(); + } + + #[test] + #[cfg(not(feature = "celo"))] + fn signs_tx_empty_chain_id_sync() { + let chain_id = 1337u64; + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + let tx: TypedTransaction = TransactionRequest { + from: None, + to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::
().unwrap().into()), + value: Some(1_000_000_000u64.into()), + gas: Some(2_000_000u64.into()), + nonce: Some(0u64.into()), + gas_price: Some(21_000_000_000u128.into()), + data: None, + chain_id: None, + } + .into(); + let wallet: Wallet = + "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); + let wallet = wallet.with_chain_id(chain_id); + + // this should populate the tx chain_id as the signer's chain_id (1337) before signing and + // normalize the v + let sig = wallet.sign_transaction_sync(&tx).unwrap(); + + // ensure correct v given the chain - first extract recid + let recid = (sig.v - 35) % 2; + // eip155 check + assert_eq!(sig.v, chain_id * 2 + 35 + recid); + + // since we initialize with None we need to re-set the chain_id for the sighash to be + // correct + let mut tx = tx; + tx.set_chain_id(chain_id); + let sighash = tx.sighash(); + sig.verify(sighash, wallet.address).unwrap(); + } + + #[test] + fn key_to_address() { + let wallet: Wallet = + "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); + assert_eq!( + wallet.address, + Address::from_str("7E5F4552091A69125d5DfCb7b8C2659029395Bdf").expect("Decoding failed") + ); + + let wallet: Wallet = + "0000000000000000000000000000000000000000000000000000000000000002".parse().unwrap(); + assert_eq!( + wallet.address, + Address::from_str("2B5AD5c4795c026514f8317c7a215E218DcCD6cF").expect("Decoding failed") + ); + + let wallet: Wallet = + "0000000000000000000000000000000000000000000000000000000000000003".parse().unwrap(); + assert_eq!( + wallet.address, + Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed") + ); + } + + #[test] + fn key_from_bytes() { + let wallet: Wallet = + "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); + + let key_as_bytes = wallet.signer.to_bytes(); + let wallet_from_bytes = Wallet::from_bytes(&key_as_bytes).unwrap(); + + assert_eq!(wallet.address, wallet_from_bytes.address); + assert_eq!(wallet.chain_id, wallet_from_bytes.chain_id); + assert_eq!(wallet.signer, wallet_from_bytes.signer); + } + + #[test] + fn key_from_str() { + let wallet: Wallet = + "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); + + // Check FromStr and `0x` + let wallet_0x: Wallet = + "0x0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); + assert_eq!(wallet.address, wallet_0x.address); + assert_eq!(wallet.chain_id, wallet_0x.chain_id); + assert_eq!(wallet.signer, wallet_0x.signer); + + // Check FromStr and `0X` + let wallet_0x_cap: Wallet = + "0X0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); + assert_eq!(wallet.address, wallet_0x_cap.address); + assert_eq!(wallet.chain_id, wallet_0x_cap.chain_id); + assert_eq!(wallet.signer, wallet_0x_cap.signer); + + // Check TryFrom<&str> + let wallet_0x_tryfrom_str: Wallet = + "0x0000000000000000000000000000000000000000000000000000000000000001" + .try_into() + .unwrap(); + assert_eq!(wallet.address, wallet_0x_tryfrom_str.address); + assert_eq!(wallet.chain_id, wallet_0x_tryfrom_str.chain_id); + assert_eq!(wallet.signer, wallet_0x_tryfrom_str.signer); + + // Check TryFrom + let wallet_0x_tryfrom_string: Wallet = + "0x0000000000000000000000000000000000000000000000000000000000000001" + .to_string() + .try_into() + .unwrap(); + assert_eq!(wallet.address, wallet_0x_tryfrom_string.address); + assert_eq!(wallet.chain_id, wallet_0x_tryfrom_string.chain_id); + assert_eq!(wallet.signer, wallet_0x_tryfrom_string.signer); + + // Must fail because of `0z` + "0z0000000000000000000000000000000000000000000000000000000000000001" + .parse::>() + .unwrap_err(); + } +} diff --git a/crates/signers/src/wallet/yubi.rs b/crates/signers/src/wallet/yubi.rs new file mode 100644 index 00000000000..a69e0a2194e --- /dev/null +++ b/crates/signers/src/wallet/yubi.rs @@ -0,0 +1,116 @@ +//! Helpers for creating wallets for YubiHSM2 +use super::Wallet; +use elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; +use ethers_core::{ + k256::{PublicKey, Secp256k1}, + types::Address, + utils::keccak256, +}; +use yubihsm::{ + asymmetric::Algorithm::EcK256, ecdsa::Signer as YubiSigner, object, object::Label, Capability, + Client, Connector, Credentials, Domain, +}; + +impl Wallet> { + /// Connects to a yubi key's ECDSA account at the provided id + pub fn connect(connector: Connector, credentials: Credentials, id: object::Id) -> Self { + let client = Client::open(connector, credentials, true).unwrap(); + let signer = YubiSigner::create(client, id).unwrap(); + signer.into() + } + + /// Creates a new random ECDSA keypair on the yubi at the provided id + pub fn new( + connector: Connector, + credentials: Credentials, + id: object::Id, + label: Label, + domain: Domain, + ) -> Self { + let client = Client::open(connector, credentials, true).unwrap(); + let id = client + .generate_asymmetric_key(id, label, domain, Capability::SIGN_ECDSA, EcK256) + .unwrap(); + let signer = YubiSigner::create(client, id).unwrap(); + signer.into() + } + + /// Uploads the provided keypair on the yubi at the provided id + pub fn from_key( + connector: Connector, + credentials: Credentials, + id: object::Id, + label: Label, + domain: Domain, + key: impl Into>, + ) -> Self { + let client = Client::open(connector, credentials, true).unwrap(); + let id = client + .put_asymmetric_key(id, label, domain, Capability::SIGN_ECDSA, EcK256, key) + .unwrap(); + let signer = YubiSigner::create(client, id).unwrap(); + signer.into() + } +} + +impl From> for Wallet> { + fn from(signer: YubiSigner) -> Self { + // this will never fail + let public_key = PublicKey::from_encoded_point(signer.public_key()).unwrap(); + let public_key = public_key.to_encoded_point(/* compress = */ false); + let public_key = public_key.as_bytes(); + debug_assert_eq!(public_key[0], 0x04); + let hash = keccak256(&public_key[1..]); + let address = Address::from_slice(&hash[12..]); + + Self { signer, address, chain_id: 1 } + } +} + +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] +mod tests { + use super::*; + use crate::Signer; + use std::str::FromStr; + + #[tokio::test] + async fn from_key() { + let key = hex::decode("2d8c44dc2dd2f0bea410e342885379192381e82d855b1b112f9b55544f1e0900") + .unwrap(); + + let connector = yubihsm::Connector::mockhsm(); + let wallet = Wallet::from_key( + connector, + Credentials::default(), + 0, + Label::from_bytes(&[]).unwrap(), + Domain::at(1).unwrap(), + key, + ); + + let msg = "Some data"; + let sig = wallet.sign_message(msg).await.unwrap(); + assert_eq!(sig.recover(msg).unwrap(), wallet.address()); + assert_eq!( + wallet.address(), + Address::from_str("2DE2C386082Cff9b28D62E60983856CE1139eC49").unwrap() + ); + } + + #[tokio::test] + async fn new_key() { + let connector = yubihsm::Connector::mockhsm(); + let wallet = Wallet::>::new( + connector, + Credentials::default(), + 0, + Label::from_bytes(&[]).unwrap(), + Domain::at(1).unwrap(), + ); + + let msg = "Some data"; + let sig = wallet.sign_message(msg).await.unwrap(); + assert_eq!(sig.recover(msg).unwrap(), wallet.address()); + } +} From fd82f7b7d82380ab3c7fbfbb605606c011510026 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:00:35 +0100 Subject: [PATCH 04/42] cfg stuff --- crates/signers/Cargo.toml | 1 + crates/signers/src/signature.rs | 1 + crates/signers/src/signer.rs | 1 + crates/signers/src/wallet/mnemonic.rs | 3 ++- crates/signers/src/wallet/mod.rs | 4 +++- crates/signers/src/wallet/private_key.rs | 18 ++++++++---------- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/signers/Cargo.toml b/crates/signers/Cargo.toml index f0bb81817ca..55b1698dea1 100644 --- a/crates/signers/Cargo.toml +++ b/crates/signers/Cargo.toml @@ -13,6 +13,7 @@ exclude.workspace = true [dependencies] alloy-primitives.workspace = true +alloy-rpc-types.workspace = true # crypto coins-bip32 = "0.8.7" diff --git a/crates/signers/src/signature.rs b/crates/signers/src/signature.rs index 940e8f0917f..6da18a7581f 100644 --- a/crates/signers/src/signature.rs +++ b/crates/signers/src/signature.rs @@ -12,6 +12,7 @@ use crate::utils::public_key_to_address; /// This is a wrapper around [`ecdsa::Signature`] and a [`RecoveryId`] to provide public key /// recovery functionality. #[derive(Clone, Debug, PartialEq, Eq)] +#[allow(missing_copy_implementations)] pub struct Signature { /// The inner ECDSA signature. inner: ecdsa::Signature, diff --git a/crates/signers/src/signer.rs b/crates/signers/src/signer.rs index fab2ca6ba8b..648569afce9 100644 --- a/crates/signers/src/signer.rs +++ b/crates/signers/src/signer.rs @@ -16,6 +16,7 @@ pub trait Signer: std::fmt::Debug + Send + Sync { async fn sign_message(&self, message: &[u8]) -> Result; /// Signs the transaction. + #[cfg(TODO)] async fn sign_transaction(&self, message: &TypedTransaction) -> Result; /// Encodes and signs the typed data according [EIP-712]. diff --git a/crates/signers/src/wallet/mnemonic.rs b/crates/signers/src/wallet/mnemonic.rs index a752ebaa1e3..645476197bf 100644 --- a/crates/signers/src/wallet/mnemonic.rs +++ b/crates/signers/src/wallet/mnemonic.rs @@ -36,6 +36,7 @@ pub struct MnemonicBuilder { /// Error produced by the mnemonic wallet module #[derive(Error, Debug)] +#[allow(missing_copy_implementations)] pub enum MnemonicBuilderError { /// Error suggests that a phrase (path or words) was expected but not found #[error("Expected phrase not found")] @@ -103,7 +104,7 @@ impl MnemonicBuilder { /// Sets the derivation path of the child key to be derived. The derivation path is calculated /// using the default derivation path prefix used in Ethereum, i.e. "m/44'/60'/0'/0/{index}". - pub fn index(mut self, index: u32) -> Result { + pub fn index(self, index: u32) -> Result { self.derivation_path(&format!("{DEFAULT_DERIVATION_PATH_PREFIX}{index}")) } diff --git a/crates/signers/src/wallet/mod.rs b/crates/signers/src/wallet/mod.rs index 2f0c7320e41..776fd94455f 100644 --- a/crates/signers/src/wallet/mod.rs +++ b/crates/signers/src/wallet/mod.rs @@ -1,4 +1,4 @@ -use crate::{utils::to_eip155_v, Signature, Signer}; +use crate::{Signature, Signer}; use alloy_primitives::{utils::eip191_hash_message, Address, B256}; use async_trait::async_trait; use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}; @@ -73,6 +73,7 @@ impl> Signer for self.sign_hash(message_hash) } + #[cfg(TODO)] async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { self.sign_transaction_sync(tx) } @@ -106,6 +107,7 @@ impl> Wallet { /// Synchronously signs the provided transaction, normalizing the signature `v` value with /// EIP-155 using the transaction's `chain_id`, or the signer's `chain_id` if the transaction /// does not specify one. + #[cfg(TODO)] pub fn sign_transaction_sync(&self, tx: &TypedTransaction) -> Result { // rlp (for sighash) must have the same chain id as v in the signature let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); diff --git a/crates/signers/src/wallet/private_key.rs b/crates/signers/src/wallet/private_key.rs index 0d16c74c319..c2e45efdddb 100644 --- a/crates/signers/src/wallet/private_key.rs +++ b/crates/signers/src/wallet/private_key.rs @@ -185,7 +185,7 @@ impl TryFrom for Wallet { #[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; - use crate::{LocalWallet, Signer, TransactionRequest, TypedTransaction}; + use crate::{LocalWallet, Signer}; use alloy_primitives::Address; use tempfile::tempdir; @@ -258,22 +258,19 @@ mod tests { let address = key.address; // sign a message - let signature = key.sign_message(message).await.unwrap(); + let signature = key.sign_message(message.as_bytes()).await.unwrap(); // ecrecover via the message will hash internally - let recovered = signature.recover(message).unwrap(); + let recovered = signature.recover_address_from_msg(message).unwrap(); + assert_eq!(recovered, address); // if provided with a hash, it will skip hashing - let recovered2 = signature.recover(hash).unwrap(); - - // verifies the signature is produced by `address` - signature.verify(message, address).unwrap(); - - assert_eq!(recovered, address); + let recovered2 = signature.recover_address_from_prehash(&hash).unwrap(); assert_eq!(recovered2, address); } #[tokio::test] + #[cfg(TODO)] async fn signs_tx() { // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction @@ -298,6 +295,7 @@ mod tests { } #[tokio::test] + #[cfg(TODO)] async fn signs_tx_empty_chain_id() { // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction @@ -328,7 +326,7 @@ mod tests { } #[test] - #[cfg(not(feature = "celo"))] + #[cfg(TODO)] fn signs_tx_empty_chain_id_sync() { let chain_id = 1337u64; // retrieved test vector from: From f8c6554c583db174cb072469332b38aba7a03c1a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:05:13 +0100 Subject: [PATCH 05/42] chore: clippy --- crates/signers/src/signature.rs | 15 +++++++-------- crates/signers/src/utils.rs | 2 +- crates/signers/src/wallet/mnemonic.rs | 4 ++-- crates/signers/src/wallet/mod.rs | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/signers/src/signature.rs b/crates/signers/src/signature.rs index 6da18a7581f..1c0c7cc55ce 100644 --- a/crates/signers/src/signature.rs +++ b/crates/signers/src/signature.rs @@ -1,3 +1,4 @@ +use crate::utils::public_key_to_address; use alloy_primitives::{keccak256, Address, B256}; use elliptic_curve::NonZeroScalar; use k256::{ @@ -5,8 +6,6 @@ use k256::{ Secp256k1, }; -use crate::utils::public_key_to_address; - /// An Ethereum ECDSA signature. /// /// This is a wrapper around [`ecdsa::Signature`] and a [`RecoveryId`] to provide public key @@ -22,7 +21,7 @@ pub struct Signature { impl Signature { /// Creates a new signature from the given inner signature and recovery ID. - pub fn new(inner: ecdsa::Signature, recid: RecoveryId) -> Self { + pub const fn new(inner: ecdsa::Signature, recid: RecoveryId) -> Self { Self { inner, recid } } @@ -47,7 +46,7 @@ impl Signature { /// Returns the inner ECDSA signature. #[inline] - pub fn inner(&self) -> &ecdsa::Signature { + pub const fn inner(&self) -> &ecdsa::Signature { &self.inner } @@ -59,19 +58,19 @@ impl Signature { /// Returns the inner ECDSA signature. #[inline] - pub fn into_inner(self) -> ecdsa::Signature { + pub const fn into_inner(self) -> ecdsa::Signature { self.inner } /// Returns the recovery ID. #[inline] - pub fn recid(&self) -> RecoveryId { + pub const fn recid(&self) -> RecoveryId { self.recid } #[doc(hidden)] #[deprecated(note = "use `Signature::recid` instead")] - pub fn recovery_id(&self) -> RecoveryId { + pub const fn recovery_id(&self) -> RecoveryId { self.recid } @@ -89,7 +88,7 @@ impl Signature { /// Returns the recovery ID as a `u8`. #[inline] - pub fn v(&self) -> u8 { + pub const fn v(&self) -> u8 { self.recid.to_byte() } diff --git a/crates/signers/src/utils.rs b/crates/signers/src/utils.rs index 0c8f915af5b..a56b9d171d2 100644 --- a/crates/signers/src/utils.rs +++ b/crates/signers/src/utils.rs @@ -8,7 +8,7 @@ use k256::{ }; /// Applies [EIP-155](https://eips.ethereum.org/EIPS/eip-155). -pub fn to_eip155_v(recovery_id: u8, chain_id: u64) -> u64 { +pub const fn to_eip155_v(recovery_id: u8, chain_id: u64) -> u64 { (recovery_id as u64) + 35 + chain_id * 2 } diff --git a/crates/signers/src/wallet/mnemonic.rs b/crates/signers/src/wallet/mnemonic.rs index 645476197bf..1a408d4e1ee 100644 --- a/crates/signers/src/wallet/mnemonic.rs +++ b/crates/signers/src/wallet/mnemonic.rs @@ -97,7 +97,7 @@ impl MnemonicBuilder { /// # Ok(()) /// # } /// ``` - pub fn word_count(mut self, count: usize) -> Self { + pub const fn word_count(mut self, count: usize) -> Self { self.word_count = count; self } @@ -105,7 +105,7 @@ impl MnemonicBuilder { /// Sets the derivation path of the child key to be derived. The derivation path is calculated /// using the default derivation path prefix used in Ethereum, i.e. "m/44'/60'/0'/0/{index}". pub fn index(self, index: u32) -> Result { - self.derivation_path(&format!("{DEFAULT_DERIVATION_PATH_PREFIX}{index}")) + self.derivation_path(format!("{DEFAULT_DERIVATION_PATH_PREFIX}{index}")) } /// Sets the derivation path of the child key to be derived. diff --git a/crates/signers/src/wallet/mod.rs b/crates/signers/src/wallet/mod.rs index 776fd94455f..f7c1a6ba361 100644 --- a/crates/signers/src/wallet/mod.rs +++ b/crates/signers/src/wallet/mod.rs @@ -58,7 +58,7 @@ pub struct Wallet> { impl> Wallet { /// Construct a new wallet with an external Signer - pub fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { + pub const fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { Wallet { signer, address, chain_id } } } @@ -127,7 +127,7 @@ impl> Wallet { } /// Gets the wallet's signer - pub fn signer(&self) -> &D { + pub const fn signer(&self) -> &D { &self.signer } } From 15ddd9b28342d56bd380c719874beec9bca72f87 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 23 Nov 2023 23:15:36 +0100 Subject: [PATCH 06/42] signature methods, tests --- crates/signers/README.md | 7 -- crates/signers/src/signature.rs | 158 ++++++++++++++++++++++++++++++-- 2 files changed, 150 insertions(+), 15 deletions(-) diff --git a/crates/signers/README.md b/crates/signers/README.md index 03f041e9c29..8021192c744 100644 --- a/crates/signers/README.md +++ b/crates/signers/README.md @@ -5,10 +5,6 @@ Ethereum signer abstraction. You can implement the `Signer` trait to extend functionality to other signers such as Hardware Security Modules, KMS etc. -The exposed interfaces return a recoverable signature. In order to convert the -signature and the [`TransactionRequest`] to a [`Transaction`], look at the -signing middleware. - Supported signers: - [Private key](./src/wallet) - [Ledger](./src/ledger) @@ -16,9 +12,6 @@ Supported signers: - [YubiHSM2](./src/wallet/yubi.rs) - [AWS KMS](./src/aws) -[`transaction`]: ethers_core::types::Transaction -[`transactionrequest`]: ethers_core::types::TransactionRequest - ## Examples diff --git a/crates/signers/src/signature.rs b/crates/signers/src/signature.rs index 1c0c7cc55ce..15f68b28e78 100644 --- a/crates/signers/src/signature.rs +++ b/crates/signers/src/signature.rs @@ -1,10 +1,11 @@ use crate::utils::public_key_to_address; -use alloy_primitives::{keccak256, Address, B256}; +use alloy_primitives::{eip191_hash_message, hex, Address, B256}; use elliptic_curve::NonZeroScalar; use k256::{ ecdsa::{self, RecoveryId, VerifyingKey}, Secp256k1, }; +use std::str::FromStr; /// An Ethereum ECDSA signature. /// @@ -19,9 +20,82 @@ pub struct Signature { recid: RecoveryId, } +impl<'a> TryFrom<&'a [u8]> for Signature { + type Error = ecdsa::Error; + + /// Parses a raw signature which is expected to be 65 bytes long where + /// the first 32 bytes is the `r` value, the second 32 bytes the `s` value + /// and the final byte is the `v` value in 'Electrum' notation. + fn try_from(bytes: &'a [u8]) -> Result { + if bytes.len() != 65 { + return Err(ecdsa::Error::new()); + } + Self::from_bytes(&bytes[..64], bytes[64] as u64) + } +} + +impl FromStr for Signature { + type Err = ecdsa::Error; + + fn from_str(s: &str) -> Result { + match hex::decode(s) { + Ok(bytes) => Self::try_from(&bytes[..]), + Err(e) => Err(ecdsa::Error::from_source(e)), + } + } +} + +impl From<&Signature> for [u8; 65] { + #[inline] + fn from(value: &Signature) -> [u8; 65] { + value.as_bytes() + } +} + +impl From for [u8; 65] { + #[inline] + fn from(value: Signature) -> [u8; 65] { + value.as_bytes() + } +} + +impl From<&Signature> for Vec { + #[inline] + fn from(value: &Signature) -> Vec { + value.as_bytes().to_vec() + } +} + +impl From for Vec { + #[inline] + fn from(value: Signature) -> Vec { + value.as_bytes().to_vec() + } +} + impl Signature { - /// Creates a new signature from the given inner signature and recovery ID. - pub const fn new(inner: ecdsa::Signature, recid: RecoveryId) -> Self { + /// Creates a new [`Signature`] from the given ECDSA signature and recovery ID. + /// + /// Normalizes the signature into "low S" form as described in + /// [BIP 0062: Dealing with Malleability][1]. + /// + /// [1]: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki + #[inline] + pub fn new(mut inner: ecdsa::Signature, mut recid: RecoveryId) -> Self { + // Normalize into "low S" form. See: + // - https://github.com/RustCrypto/elliptic-curves/issues/988 + // - https://github.com/bluealloy/revm/pull/870 + if let Some(normalized) = inner.normalize_s() { + inner = normalized; + recid = RecoveryId::from_byte(recid.to_byte() ^ 1).unwrap(); + } + Self::new_not_normalized(inner, recid) + } + + /// Creates a new signature from the given inner signature and recovery ID, without normalizing + /// it into "low S" form. + #[inline] + pub const fn new_not_normalized(inner: ecdsa::Signature, recid: RecoveryId) -> Self { Self { inner, recid } } @@ -30,7 +104,7 @@ impl Signature { pub fn from_bytes(bytes: &[u8], v: u64) -> Result { let inner = ecdsa::Signature::from_slice(bytes)?; let recid = normalize_v(v); - Ok(Self { inner, recid }) + Ok(Self::new(inner, recid)) } /// Creates a [`Signature`] from the serialized `r` and `s` scalar values, which comprise the @@ -41,7 +115,7 @@ impl Signature { pub fn from_scalars(r: B256, s: B256, v: u64) -> Result { let inner = ecdsa::Signature::from_scalars(r.0, s.0)?; let recid = normalize_v(v); - Ok(Self { inner, recid }) + Ok(Self::new(inner, recid)) } /// Returns the inner ECDSA signature. @@ -92,6 +166,19 @@ impl Signature { self.recid.to_byte() } + /// Returns the byte-array representation of this signature. + /// + /// The first 32 bytes are the `r` value, the second 32 bytes the `s` value + /// and the final byte is the `v` value in 'Electrum' notation. + #[inline] + pub fn as_bytes(&self) -> [u8; 65] { + let mut sig = [0u8; 65]; + sig[..32].copy_from_slice(self.r().to_bytes().as_ref()); + sig[32..64].copy_from_slice(self.s().to_bytes().as_ref()); + sig[64] = self.recid.to_byte(); + sig + } + /// Sets the recovery ID. #[inline] pub fn set_recid(&mut self, recid: RecoveryId) { @@ -120,11 +207,11 @@ impl Signature { self.recover_from_prehash(prehash).map(|pubkey| public_key_to_address(&pubkey)) } - /// Recovers a [`VerifyingKey`] from this signature and the given message by first hashing the - /// message with Keccak-256. + /// Recovers a [`VerifyingKey`] from this signature and the given message by first prefixing and + /// hashing the message according to [EIP-191](eip191_hash_message). #[inline] pub fn recover_from_msg>(&self, msg: T) -> Result { - self.recover_from_prehash(&keccak256(msg)) + self.recover_from_prehash(&eip191_hash_message(msg)) } /// Recovers a [`VerifyingKey`] from this signature and the given prehashed message. @@ -163,3 +250,58 @@ const fn normalize_v(v: u64) -> RecoveryId { None => unsafe { core::hint::unreachable_unchecked() }, } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, b256}; + use std::str::FromStr; + + #[test] + #[cfg(TODO)] + fn can_recover_tx_sender() { + // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f + let tx_rlp = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap(); + let tx: Transaction = rlp::decode(&tx_rlp).unwrap(); + assert_eq!(tx.rlp(), tx_rlp); + assert_eq!( + tx.hash, + "0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f".parse().unwrap() + ); + assert_eq!(tx.transaction_type, Some(2.into())); + let expected = Address::from_str("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5").unwrap(); + assert_eq!(tx.recover_from().unwrap(), expected); + } + + #[test] + fn can_recover_tx_sender_not_normalized() { + let sig = Signature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap(); + let hash = b256!("5eb4f5a33c621f32a8622d5f943b6b102994dfe4e5aebbefe69bb1b2aa0fc93e"); + let expected = address!("0f65fe9276bc9a24ae7083ae28e2660ef72df99e"); + assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), expected); + } + + #[test] + fn recover_web3_signature() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign + let signature = Signature::from_str( + "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" + ).expect("could not parse signature"); + let expected = address!("2c7536E3605D9C16a7a3D7b1898e529396a65c23"); + assert_eq!(signature.recover_address_from_msg("Some data").unwrap(), expected); + } + + #[test] + fn signature_from_str() { + let s1 = Signature::from_str( + "0xaa231fbe0ed2b5418e6ba7c19bee2522852955ec50996c02a2fe3e71d30ddaf1645baf4823fea7cb4fcc7150842493847cfb6a6d63ab93e8ee928ee3f61f503500" + ).expect("could not parse 0x-prefixed signature"); + + let s2 = Signature::from_str( + "aa231fbe0ed2b5418e6ba7c19bee2522852955ec50996c02a2fe3e71d30ddaf1645baf4823fea7cb4fcc7150842493847cfb6a6d63ab93e8ee928ee3f61f503500" + ).expect("could not parse non-prefixed signature"); + + assert_eq!(s1, s2); + } +} From 5ff9a6616eb7ce11a7e88fed80a58713c423bc5a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 24 Nov 2023 02:42:13 +0100 Subject: [PATCH 07/42] fix mnemonic test --- Cargo.toml | 5 +--- crates/signers/src/wallet/mnemonic.rs | 42 ++++++++++----------------- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b1615f2e19f..74465cb6726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ alloy-transport = { version = "0.1.0", path = "crates/transport" } alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" } alloy-transport-ws = { version = "0.1.0", path = "crates/transport-ws" } -alloy-primitives = { version = "0.4.2", features = ["serde"] } +alloy-primitives = { version = "0.5.0", features = ["serde"] } alloy-rlp = "0.3" # crypto @@ -64,6 +64,3 @@ home = "0.5" semver = "1.0" thiserror = "1.0" url = "2.4" - -[patch.crates-io] -alloy-primitives = { path = "../core/crates/primitives" } diff --git a/crates/signers/src/wallet/mnemonic.rs b/crates/signers/src/wallet/mnemonic.rs index 1a408d4e1ee..2edd49c95cc 100644 --- a/crates/signers/src/wallet/mnemonic.rs +++ b/crates/signers/src/wallet/mnemonic.rs @@ -170,19 +170,17 @@ impl MnemonicBuilder { } #[cfg(test)] -#[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; - use crate::coins_bip39::English; use tempfile::tempdir; const TEST_DERIVATION_PATH: &str = "m/44'/60'/0'/2/1"; - #[tokio::test] - async fn mnemonic_deterministic() { + #[test] + fn mnemonic_deterministic() { // Testcases have been taken from MyCryptoWallet - const TESTCASES: [(&str, u32, Option<&str>, &str); 4] = [ + let tests = [ ( "work man father plunge mystery proud hollow address reunion sauce theory bonus", 0u32, @@ -208,28 +206,19 @@ mod tests { "0xFB78b25f69A8e941036fEE2A5EeAf349D81D4ccc", ), ]; - TESTCASES.iter().for_each(|&(phrase, index, password, expected_addr)| { - let wallet = match password { - Some(psswd) => MnemonicBuilder::::default() - .phrase(phrase) - .index(index) - .unwrap() - .password(psswd) - .build() - .unwrap(), - None => MnemonicBuilder::::default() - .phrase(phrase) - .index(index) - .unwrap() - .build() - .unwrap(), - }; + for (phrase, index, password, expected_addr) in tests { + let mut builder = + MnemonicBuilder::::default().phrase(phrase).index(index).unwrap(); + if let Some(psswd) = password { + builder = builder.password(psswd); + } + let wallet = builder.build().unwrap(); assert_eq!(&wallet.address.to_string(), expected_addr); - }) + } } - #[tokio::test] - async fn mnemonic_write_read() { + #[test] + fn mnemonic_write_read() { let dir = tempdir().unwrap(); // Construct a wallet from random mnemonic phrase and write it to the temp dir. @@ -246,10 +235,11 @@ mod tests { let paths = std::fs::read_dir(dir.as_ref()).unwrap(); assert_eq!(paths.count(), 1); - // Use the newly created file's path to instantiate wallet. + // Use the newly created mnemonic to instantiate wallet. let phrase_path = dir.as_ref().join(wallet1.address.to_string()); + let phrase = std::fs::read_to_string(phrase_path).unwrap(); let wallet2 = MnemonicBuilder::::default() - .phrase(phrase_path.to_str().unwrap()) + .phrase(phrase) .derivation_path(TEST_DERIVATION_PATH) .unwrap() .build() From a9a368544259455d6d16b88b16aa917d493ceba6 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 24 Nov 2023 04:15:08 +0100 Subject: [PATCH 08/42] fixes --- Cargo.toml | 3 +- crates/signers/Cargo.toml | 3 + crates/signers/README.md | 21 ++- crates/signers/src/signer.rs | 14 +- crates/signers/src/wallet/mod.rs | 58 ++++---- crates/signers/src/wallet/private_key.rs | 175 +++++++++++++---------- 6 files changed, 155 insertions(+), 119 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 74465cb6726..a705f74c63d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,8 @@ alloy-transport = { version = "0.1.0", path = "crates/transport" } alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" } alloy-transport-ws = { version = "0.1.0", path = "crates/transport-ws" } -alloy-primitives = { version = "0.5.0", features = ["serde"] } +alloy-primitives = { version = "0.5.0", default-features = false, features = ["std"] } +alloy-sol-types = { version = "0.5.0", default-features = false, features = ["std"] } alloy-rlp = "0.3" # crypto diff --git a/crates/signers/Cargo.toml b/crates/signers/Cargo.toml index 55b1698dea1..2bcede9cefa 100644 --- a/crates/signers/Cargo.toml +++ b/crates/signers/Cargo.toml @@ -15,6 +15,8 @@ exclude.workspace = true alloy-primitives.workspace = true alloy-rpc-types.workspace = true +alloy-sol-types.workspace = true + # crypto coins-bip32 = "0.8.7" coins-bip39 = "0.8.7" @@ -68,3 +70,4 @@ ledger = ["dep:coins-ledger", "dep:semver", "dep:futures-util"] trezor = ["dep:trezor-client", "dep:semver", "dep:home", "dep:protobuf", "dep:futures-util"] aws = ["dep:aws-config", "dep:aws-sdk-kms", "dep:spki"] yubi = ["dep:yubihsm"] +eip712 = [] diff --git a/crates/signers/README.md b/crates/signers/README.md index 8021192c744..68ff7772dc8 100644 --- a/crates/signers/README.md +++ b/crates/signers/README.md @@ -14,8 +14,7 @@ Supported signers: ## Examples - - + -Sign an Ethereum prefixed message ([eip-712](https://eips.ethereum.org/EIPS/eip-712)): +Sign an Ethereum prefixed message ([EIP-712](https://eips.ethereum.org/EIPS/eip-712)): + +```rust +use alloy_signers::{LocalWallet, Signer}; -```rust,no_run -# use ethers_signers::{Signer, LocalWallet}; -# async fn foo() -> Result<(), Box> { let message = "Some data"; -let wallet = LocalWallet::new(&mut rand::thread_rng()); +let wallet = LocalWallet::random(); // Sign the message -let signature = wallet.sign_message(message).await?; +let signature = wallet.sign_message(message)?; // Recover the signer from the message -let recovered = signature.recover(message)?; +let recovered = signature.recover_address_from_msg(message)?; assert_eq!(recovered, wallet.address()); -# Ok(()) -# } +# Ok::<_, Box>(()) ``` diff --git a/crates/signers/src/signer.rs b/crates/signers/src/signer.rs index 648569afce9..3d875b6ac56 100644 --- a/crates/signers/src/signer.rs +++ b/crates/signers/src/signer.rs @@ -3,6 +3,9 @@ use alloy_primitives::Address; use async_trait::async_trait; use std::error::Error; +#[cfg(feature = "eip712")] +use alloy_sol_types::{Eip712Domain, SolStruct}; + /// Trait for signing transactions and messages. /// /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. @@ -19,14 +22,17 @@ pub trait Signer: std::fmt::Debug + Send + Sync { #[cfg(TODO)] async fn sign_transaction(&self, message: &TypedTransaction) -> Result; - /// Encodes and signs the typed data according [EIP-712]. + /// Encodes and signs the typed data according to [EIP-712]. /// /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 - #[cfg(TODO)] - async fn sign_typed_data( + #[cfg(feature = "eip712")] + async fn sign_typed_data( &self, payload: &T, - ) -> Result; + domain: &Eip712Domain, + ) -> Result + where + Self: Sized; /// Returns the signer's Ethereum Address. fn address(&self) -> Address; diff --git a/crates/signers/src/wallet/mod.rs b/crates/signers/src/wallet/mod.rs index f7c1a6ba361..49f740395e9 100644 --- a/crates/signers/src/wallet/mod.rs +++ b/crates/signers/src/wallet/mod.rs @@ -4,6 +4,9 @@ use async_trait::async_trait; use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}; use std::fmt; +#[cfg(feature = "eip712")] +use alloy_sol_types::{Eip712Domain, SolStruct}; + mod mnemonic; pub use mnemonic::MnemonicBuilder; @@ -24,11 +27,9 @@ mod yubi; /// prefix the message being hashed with the `Ethereum Signed Message` domain separator. /// /// ``` -/// use ethers_core::rand::thread_rng; -/// use ethers_signers::{LocalWallet, Signer}; +/// use alloy_signers::{LocalWallet, Signer}; /// -/// # async fn foo() -> Result<(), Box> { -/// let wallet = LocalWallet::new(&mut thread_rng()); +/// let wallet = LocalWallet::random(); /// /// // Optionally, the wallet's chain id can be set, in order to use EIP-155 /// // replay protection with different chains @@ -36,15 +37,14 @@ mod yubi; /// /// // The wallet can be used to sign messages /// let message = b"hello"; -/// let signature = wallet.sign_message(message).await?; -/// assert_eq!(signature.recover(&message[..]).unwrap(), wallet.address()); +/// let signature = wallet.sign_message(message)?; +/// assert_eq!(signature.recover_address_from_msg(&message[..]).unwrap(), wallet.address()); /// /// // LocalWallet is clonable: /// let wallet_clone = wallet.clone(); -/// let signature2 = wallet_clone.sign_message(message).await?; +/// let signature2 = wallet_clone.sign_message(message)?; /// assert_eq!(signature, signature2); -/// # Ok(()) -/// # } +/// # Ok::<_, Box>(()) /// ``` #[derive(Clone)] pub struct Wallet> { @@ -56,21 +56,13 @@ pub struct Wallet> { pub(crate) chain_id: u64, } -impl> Wallet { - /// Construct a new wallet with an external Signer - pub const fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { - Wallet { signer, address, chain_id } - } -} - #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl> Signer for Wallet { +impl + Send + Sync> Signer for Wallet { type Error = WalletError; async fn sign_message(&self, message: &[u8]) -> Result { - let message_hash = eip191_hash_message(message); - self.sign_hash(message_hash) + self.sign_message(message) } #[cfg(TODO)] @@ -78,15 +70,13 @@ impl> Signer for self.sign_transaction_sync(tx) } - #[cfg(TODO)] - async fn sign_typed_data( + #[cfg(feature = "eip712")] + async fn sign_typed_data( &self, payload: &T, + domain: &Eip712Domain, ) -> Result { - let encoded = - payload.encode_eip712().map_err(|e| Self::Error::Eip712Error(e.to_string()))?; - - self.sign_hash(B256::from(encoded)) + self.sign_hash(&payload.eip712_signing_hash(domain)) } fn address(&self) -> Address { @@ -103,7 +93,12 @@ impl> Signer for } } -impl> Wallet { +impl + Send + Sync> Wallet { + /// Construct a new wallet with an external Signer + pub const fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { + Wallet { signer, address, chain_id } + } + /// Synchronously signs the provided transaction, normalizing the signature `v` value with /// EIP-155 using the transaction's `chain_id`, or the signer's `chain_id` if the transaction /// does not specify one. @@ -120,13 +115,20 @@ impl> Wallet { Ok(sig) } + /// Signs the provided message after prefixing it and hashing it according to [EIP-191]. + /// + /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 + pub fn sign_message>(&self, msg: T) -> Result { + self.sign_hash(&eip191_hash_message(msg)) + } + /// Signs the provided hash. - pub fn sign_hash(&self, hash: B256) -> Result { + pub fn sign_hash(&self, hash: &B256) -> Result { let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; Ok(Signature::new(recoverable_sig, recovery_id)) } - /// Gets the wallet's signer + /// Returns this wallet's signer. pub const fn signer(&self) -> &D { &self.signer } diff --git a/crates/signers/src/wallet/private_key.rs b/crates/signers/src/wallet/private_key.rs index c2e45efdddb..8d6dd71d81d 100644 --- a/crates/signers/src/wallet/private_key.rs +++ b/crates/signers/src/wallet/private_key.rs @@ -7,7 +7,7 @@ use coins_bip32::Bip32Error; use coins_bip39::MnemonicError; use k256::{ ecdsa::{self, SigningKey}, - SecretKey as K256SecretKey, + FieldBytes, SecretKey as K256SecretKey, }; use rand::{CryptoRng, Rng}; use std::str::FromStr; @@ -45,17 +45,39 @@ pub enum WalletError { /// Error propagated from the mnemonic builder module. #[error(transparent)] MnemonicBuilderError(#[from] MnemonicBuilderError), - /// Error type from Eip712Error message - #[error("error encoding eip712 struct: {0:?}")] - Eip712Error(String), } impl Wallet { + /// Creates a new Wallet instance from a raw scalar serialized as a byte array. + #[inline] + pub fn from_bytes(bytes: &FieldBytes) -> Result { + SigningKey::from_bytes(bytes).map(Self::_new) + } + + /// Creates a new Wallet instance from a raw scalar serialized as a byte slice. + #[inline] + pub fn from_slice(bytes: &[u8]) -> Result { + SigningKey::from_slice(bytes).map(Self::_new) + } + + /// Creates a new random keypair seeded with [`rand::thread_rng()`]. + #[inline] + pub fn random() -> Self { + Self::random_with(&mut rand::thread_rng()) + } + + /// Creates a new random keypair seeded with the provided RNG. + #[inline] + pub fn random_with(rng: &mut R) -> Self { + Self::_new(SigningKey::random(rng)) + } + /// Creates a new random encrypted JSON with the provided password and stores it in the /// provided directory. Returns a tuple (Wallet, String) of the wallet instance for the /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`, /// the keystore is stored as the stringified UUID. #[cfg(not(target_arch = "wasm32"))] + #[inline] pub fn new_keystore( dir: P, rng: &mut R, @@ -68,22 +90,19 @@ impl Wallet { S: AsRef<[u8]>, { let (secret, uuid) = eth_keystore::new(dir, rng, password, name)?; - let signer = SigningKey::from_bytes(secret.as_slice().into())?; - let address = secret_key_to_address(&signer); - Ok((Self { signer, address, chain_id: 1 }, uuid)) + Ok((Self::from_slice(&secret)?, uuid)) } /// Decrypts an encrypted JSON from the provided path to construct a Wallet instance #[cfg(not(target_arch = "wasm32"))] + #[inline] pub fn decrypt_keystore(keypath: P, password: S) -> Result where P: AsRef, S: AsRef<[u8]>, { let secret = eth_keystore::decrypt_key(keypath, password)?; - let signer = SigningKey::from_bytes(secret.as_slice().into())?; - let address = secret_key_to_address(&signer); - Ok(Self { signer, address, chain_id: 1 }) + Ok(Self::from_slice(&secret)?) } /// Creates a new encrypted JSON with the provided private key and password and stores it in the @@ -91,6 +110,7 @@ impl Wallet { /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`, /// the keystore is stored as the stringified UUID. #[cfg(not(target_arch = "wasm32"))] + #[inline] pub fn encrypt_keystore( keypath: P, rng: &mut R, @@ -104,25 +124,16 @@ impl Wallet { B: AsRef<[u8]>, S: AsRef<[u8]>, { - let uuid = eth_keystore::encrypt_key(keypath, rng, &pk, password, name)?; - let signer = SigningKey::from_slice(pk.as_ref())?; - let address = secret_key_to_address(&signer); - Ok((Self { signer, address, chain_id: 1 }, uuid)) + let pk = pk.as_ref(); + let uuid = eth_keystore::encrypt_key(keypath, rng, pk, password, name)?; + Ok((Self::from_slice(pk)?, uuid)) } - /// Creates a new random keypair seeded with the provided RNG - pub fn new(rng: &mut R) -> Self { - let signer = SigningKey::random(rng); + #[inline] + fn _new(signer: SigningKey) -> Self { let address = secret_key_to_address(&signer); Self { signer, address, chain_id: 1 } } - - /// Creates a new Wallet instance from a raw scalar value (big endian). - pub fn from_bytes(bytes: &[u8]) -> Result { - let signer = SigningKey::from_bytes(bytes.into())?; - let address = secret_key_to_address(&signer); - Ok(Self { signer, address, chain_id: 1 }) - } } impl PartialEq for Wallet { @@ -134,19 +145,14 @@ impl PartialEq for Wallet { } impl From for Wallet { - fn from(signer: SigningKey) -> Self { - let address = secret_key_to_address(&signer); - - Self { signer, address, chain_id: 1 } + fn from(value: SigningKey) -> Self { + Self::_new(value) } } impl From for Wallet { - fn from(key: K256SecretKey) -> Self { - let signer = key.into(); - let address = secret_key_to_address(&signer); - - Self { signer, address, chain_id: 1 } + fn from(value: K256SecretKey) -> Self { + Self::_new(value.into()) } } @@ -154,14 +160,8 @@ impl FromStr for Wallet { type Err = WalletError; fn from_str(src: &str) -> Result { - let src = hex::decode(src.strip_prefix("0X").unwrap_or(src))?; - - if src.len() != 32 { - return Err(WalletError::HexError(hex::FromHexError::InvalidStringLength)); - } - - let sk = SigningKey::from_bytes(src.as_slice().into())?; - Ok(sk.into()) + let array = hex::decode_to_array::<_, 32>(src)?; + Ok(Self::from_slice(&array)?) } } @@ -185,8 +185,8 @@ impl TryFrom for Wallet { #[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; - use crate::{LocalWallet, Signer}; - use alloy_primitives::Address; + use crate::LocalWallet; + use alloy_primitives::address; use tempfile::tempdir; #[test] @@ -206,35 +206,35 @@ mod tests { } } - async fn test_encrypted_json_keystore(key: Wallet, uuid: &str, dir: &Path) { + fn test_encrypted_json_keystore(key: Wallet, uuid: &str, dir: &Path) { // sign a message using the given key let message = "Some data"; - let signature = key.sign_message(message.as_bytes()).await.unwrap(); + let signature = key.sign_message(message.as_bytes()).unwrap(); // read from the encrypted JSON keystore and decrypt it, while validating that the // signatures produced by both the keys should match let path = Path::new(dir).join(uuid); let key2 = Wallet::::decrypt_keystore(path.clone(), "randpsswd").unwrap(); - let signature2 = key2.sign_message(message.as_bytes()).await.unwrap(); + let signature2 = key2.sign_message(message.as_bytes()).unwrap(); assert_eq!(signature, signature2); std::fs::remove_file(&path).unwrap(); } - #[tokio::test] - async fn encrypted_json_keystore_new() { + #[test] + fn encrypted_json_keystore_new() { // create and store an encrypted JSON keystore in this directory let dir = tempdir().unwrap(); let mut rng = rand::thread_rng(); let (key, uuid) = Wallet::::new_keystore(&dir, &mut rng, "randpsswd", None).unwrap(); - test_encrypted_json_keystore(key, &uuid, dir.path()).await; + test_encrypted_json_keystore(key, &uuid, dir.path()); } - #[tokio::test] - async fn encrypted_json_keystore_from_pk() { + #[test] + fn encrypted_json_keystore_from_pk() { // create and store an encrypted JSON keystore in this directory let dir = tempdir().unwrap(); let mut rng = rand::thread_rng(); @@ -247,18 +247,18 @@ mod tests { Wallet::::encrypt_keystore(&dir, &mut rng, private_key, "randpsswd", None) .unwrap(); - test_encrypted_json_keystore(key, &uuid, dir.path()).await; + test_encrypted_json_keystore(key, &uuid, dir.path()); } - #[tokio::test] - async fn signs_msg() { + #[test] + fn signs_msg() { let message = "Some data"; let hash = alloy_primitives::utils::eip191_hash_message(message); - let key = Wallet::::new(&mut rand::thread_rng()); + let key = Wallet::::random_with(&mut rand::thread_rng()); let address = key.address; // sign a message - let signature = key.sign_message(message.as_bytes()).await.unwrap(); + let signature = key.sign_message(message.as_bytes()).unwrap(); // ecrecover via the message will hash internally let recovered = signature.recover_address_from_msg(message).unwrap(); @@ -363,28 +363,60 @@ mod tests { sig.verify(sighash, wallet.address).unwrap(); } + #[tokio::test] + #[cfg(feature = "eip712")] + async fn typed_data() { + use crate::Signer; + use alloy_primitives::{keccak256, Address, I256, U256}; + use alloy_sol_types::{eip712_domain, sol, SolStruct}; + + sol! { + #[derive(Debug)] + struct FooBar { + int256 foo; + uint256 bar; + bytes fizz; + bytes32 buzz; + string far; + address out; + } + } + + let domain = eip712_domain! { + name: "Eip712Test", + version: "1", + chain_id: 1, + verifying_contract: address!("0000000000000000000000000000000000000001"), + salt: keccak256("eip712-test-75F0CCte"), + }; + let foo_bar = FooBar { + foo: I256::try_from(10u64).unwrap(), + bar: U256::from(20u64), + fizz: b"fizz".into(), + buzz: keccak256("buzz"), + far: String::from("space"), + out: Address::ZERO, + }; + let wallet = Wallet::random(); + let hash = foo_bar.eip712_signing_hash(&domain); + let sig = wallet.sign_typed_data(&foo_bar, &domain).await.unwrap(); + assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), wallet.address()); + assert_eq!(wallet.sign_hash(&hash).unwrap(), sig); + } + #[test] fn key_to_address() { let wallet: Wallet = "0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); - assert_eq!( - wallet.address, - Address::from_str("7E5F4552091A69125d5DfCb7b8C2659029395Bdf").expect("Decoding failed") - ); + assert_eq!(wallet.address, address!("7E5F4552091A69125d5DfCb7b8C2659029395Bdf")); let wallet: Wallet = "0000000000000000000000000000000000000000000000000000000000000002".parse().unwrap(); - assert_eq!( - wallet.address, - Address::from_str("2B5AD5c4795c026514f8317c7a215E218DcCD6cF").expect("Decoding failed") - ); + assert_eq!(wallet.address, address!("2B5AD5c4795c026514f8317c7a215E218DcCD6cF")); let wallet: Wallet = "0000000000000000000000000000000000000000000000000000000000000003".parse().unwrap(); - assert_eq!( - wallet.address, - Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed") - ); + assert_eq!(wallet.address, address!("6813Eb9362372EEF6200f3b1dbC3f819671cBA69")); } #[test] @@ -412,13 +444,6 @@ mod tests { assert_eq!(wallet.chain_id, wallet_0x.chain_id); assert_eq!(wallet.signer, wallet_0x.signer); - // Check FromStr and `0X` - let wallet_0x_cap: Wallet = - "0X0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); - assert_eq!(wallet.address, wallet_0x_cap.address); - assert_eq!(wallet.chain_id, wallet_0x_cap.chain_id); - assert_eq!(wallet.signer, wallet_0x_cap.signer); - // Check TryFrom<&str> let wallet_0x_tryfrom_str: Wallet = "0x0000000000000000000000000000000000000000000000000000000000000001" From 9a90a52afd9313041e52061628b40d8558a7ae22 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 24 Nov 2023 06:15:41 +0100 Subject: [PATCH 09/42] wip: split to crates --- Cargo.toml | 6 +- crates/signer-aws/Cargo.toml | 33 +++++++ crates/signer-aws/README.md | 5 ++ .../src/aws/mod.rs => signer-aws/src/lib.rs} | 88 ++++++++++--------- crates/signer-aws/src/utils.rs | 38 ++++++++ crates/signer-ledger/Cargo.toml | 33 +++++++ crates/signer-ledger/README.md | 5 ++ .../src/ledger => signer-ledger/src}/app.rs | 70 +++++++-------- .../mod.rs => signer-ledger/src/lib.rs} | 47 +++++++--- .../src/ledger => signer-ledger/src}/types.rs | 13 +-- crates/signer-trezor/Cargo.toml | 35 ++++++++ crates/signer-trezor/README.md | 5 ++ .../src/trezor => signer-trezor/src}/app.rs | 64 +++++++------- crates/signer-trezor/src/lib.rs | 80 +++++++++++++++++ .../src/trezor => signer-trezor/src}/types.rs | 35 ++++---- crates/signer/Cargo.toml | 46 ++++++++++ crates/{signers => signer}/README.md | 2 +- crates/{signers => signer}/src/lib.rs | 34 ++----- crates/{signers => signer}/src/signature.rs | 13 ++- crates/{signers => signer}/src/signer.rs | 0 crates/{signers => signer}/src/utils.rs | 5 +- .../src/wallet/mnemonic.rs | 4 +- crates/{signers => signer}/src/wallet/mod.rs | 2 +- .../src/wallet/private_key.rs | 0 crates/{signers => signer}/src/wallet/yubi.rs | 51 +++++------ crates/signers/Cargo.toml | 73 --------------- crates/signers/src/aws/utils.rs | 49 ----------- crates/signers/src/trezor/mod.rs | 51 ----------- 28 files changed, 503 insertions(+), 384 deletions(-) create mode 100644 crates/signer-aws/Cargo.toml create mode 100644 crates/signer-aws/README.md rename crates/{signers/src/aws/mod.rs => signer-aws/src/lib.rs} (80%) create mode 100644 crates/signer-aws/src/utils.rs create mode 100644 crates/signer-ledger/Cargo.toml create mode 100644 crates/signer-ledger/README.md rename crates/{signers/src/ledger => signer-ledger/src}/app.rs (88%) rename crates/{signers/src/ledger/mod.rs => signer-ledger/src/lib.rs} (51%) rename crates/{signers/src/ledger => signer-ledger/src}/types.rs (93%) create mode 100644 crates/signer-trezor/Cargo.toml create mode 100644 crates/signer-trezor/README.md rename crates/{signers/src/trezor => signer-trezor/src}/app.rs (91%) create mode 100644 crates/signer-trezor/src/lib.rs rename crates/{signers/src/trezor => signer-trezor/src}/types.rs (87%) create mode 100644 crates/signer/Cargo.toml rename crates/{signers => signer}/README.md (97%) rename crates/{signers => signer}/src/lib.rs (56%) rename crates/{signers => signer}/src/signature.rs (96%) rename crates/{signers => signer}/src/signer.rs (100%) rename crates/{signers => signer}/src/utils.rs (96%) rename crates/{signers => signer}/src/wallet/mnemonic.rs (98%) rename crates/{signers => signer}/src/wallet/mod.rs (99%) rename crates/{signers => signer}/src/wallet/private_key.rs (100%) rename crates/{signers => signer}/src/wallet/yubi.rs (68%) delete mode 100644 crates/signers/Cargo.toml delete mode 100644 crates/signers/src/aws/utils.rs delete mode 100644 crates/signers/src/trezor/mod.rs diff --git a/Cargo.toml b/Cargo.toml index a705f74c63d..347f1cc531d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,10 @@ alloy-networks = { version = "0.1.0", path = "crates/networks" } alloy-pubsub = { version = "0.1.0", path = "crates/pubsub" } alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" } alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } -alloy-signers = { version = "0.1.0", path = "crates/signers" } +alloy-signer = { version = "0.1.0", path = "crates/signer" } +alloy-signer-aws = { version = "0.1.0", path = "crates/signer-aws" } +alloy-signer-ledger = { version = "0.1.0", path = "crates/signer-ledger" } +alloy-signer-trezor = { version = "0.1.0", path = "crates/signer-trezor" } alloy-transport = { version = "0.1.0", path = "crates/transport" } alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" } alloy-transport-ws = { version = "0.1.0", path = "crates/transport-ws" } @@ -42,6 +45,7 @@ spki = { version = "0.7.2", default-features = false } async-trait = "0.1.74" futures = "0.3.29" futures-util = "0.3.29" +futures-executor = "0.3.29" hyper = "0.14.27" tokio = { version = "1.33", features = ["sync", "macros"] } diff --git a/crates/signer-aws/Cargo.toml b/crates/signer-aws/Cargo.toml new file mode 100644 index 00000000000..b1fc776548d --- /dev/null +++ b/crates/signer-aws/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "alloy-signer-aws" +description = "Ethereum AWS KMS signer" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +alloy-primitives.workspace = true +alloy-signer.workspace = true + +aws-sdk-kms = { version = "0.39", default-features = false } +spki.workspace = true +k256.workspace = true +tracing.workspace = true +async-trait.workspace = true +thiserror.workspace = true + +# eip712 +alloy-sol-types = { workspace = true, optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +aws-config = { version = "1.0", default-features = false } + +[features] +eip712 = ["alloy-signer/eip712", "dep:alloy-sol-types"] diff --git a/crates/signer-aws/README.md b/crates/signer-aws/README.md new file mode 100644 index 00000000000..f9c673238ff --- /dev/null +++ b/crates/signer-aws/README.md @@ -0,0 +1,5 @@ +# alloy-signer-aws + +Ethereum [AWS KMS] signer. + +[AWS KMS]: https://aws.amazon.com/kms diff --git a/crates/signers/src/aws/mod.rs b/crates/signer-aws/src/lib.rs similarity index 80% rename from crates/signers/src/aws/mod.rs rename to crates/signer-aws/src/lib.rs index 62a676d1115..82685f070c5 100644 --- a/crates/signers/src/aws/mod.rs +++ b/crates/signer-aws/src/lib.rs @@ -1,7 +1,26 @@ -//! AWS KMS-based signer. - -use super::Signer; -use alloy_primitives::utils::eip191_hash_message; +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + // TODO + // missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +#[macro_use] +extern crate tracing; + +use alloy_primitives::{hex, utils::eip191_hash_message, Address, B256}; +use alloy_signer::{Signature as EthSig, Signer}; use aws_sdk_kms::{ error::SdkError, operation::{ @@ -12,15 +31,11 @@ use aws_sdk_kms::{ types::{MessageType, SigningAlgorithmSpec}, Client, }; -use debug; -use ethers_core::types::{ - transaction::{eip2718::TypedTransaction, eip712::Eip712}, - Address, Signature as EthSig, B256, -}; -use instrument; use k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey}; use std::fmt; -use trace; + +#[cfg(feature = "eip712")] +use alloy_sol_types::{Eip712Domain, SolStruct}; mod utils; @@ -36,10 +51,11 @@ mod utils; /// # Examples /// /// ```no_run -/// # async fn test() { +/// use alloy_signer::Signer; +/// use alloy_signer_aws::AwsSigner; /// use aws_config::BehaviorVersion; -/// use ethers_signers::{AwsSigner, Signer}; /// +/// # async fn test() { /// let config = aws_config::load_defaults(BehaviorVersion::latest()).await; /// let client = aws_sdk_kms::Client::new(&config); /// @@ -50,7 +66,7 @@ mod utils; /// let message = vec![0, 1, 2, 3]; /// /// let sig = signer.sign_message(&message).await.unwrap(); -/// sig.verify(message, signer.address()).expect("valid sig"); +/// assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); /// # } /// ``` #[derive(Clone)] @@ -73,12 +89,6 @@ impl fmt::Debug for AwsSigner { } } -impl fmt::Display for AwsSigner { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(self, f) - } -} - /// Errors thrown by [`AwsSigner`]. #[derive(thiserror::Error, Debug)] pub enum AwsSignerError { @@ -120,7 +130,7 @@ impl AwsSigner { let key_id = key_id.as_ref(); let resp = request_get_pubkey(&kms, key_id).await?; let pubkey = decode_pubkey(resp)?; - let address = ethers_core::utils::public_key_to_address(&pubkey); + let address = alloy_signer::utils::public_key_to_address(&pubkey); debug!( "Instantiated AWS signer with pubkey 0x{} and address {address:?}", @@ -147,13 +157,13 @@ impl AwsSigner { pub async fn sign_digest_with_key>( &self, key_id: T, - digest: [u8; 32], + digest: &B256, ) -> Result { request_sign_digest(&self.kms, key_id.as_ref(), digest).await.and_then(decode_signature) } /// Sign a digest with this signer's key - pub async fn sign_digest(&self, digest: [u8; 32]) -> Result { + pub async fn sign_digest(&self, digest: &B256) -> Result { self.sign_digest_with_key(self.key_id.clone(), digest).await } @@ -162,13 +172,12 @@ impl AwsSigner { #[instrument(err, skip(digest), fields(digest = %hex::encode(digest)))] async fn sign_digest_with_eip155( &self, - digest: B256, + digest: &B256, chain_id: u64, ) -> Result { - let sig = self.sign_digest(digest.into()).await?; - let mut sig = - utils::sig_from_digest_bytes_trial_recovery(&sig, digest.into(), &self.pubkey); - utils::apply_eip155(&mut sig, chain_id); + let sig = self.sign_digest(digest).await?; + let mut sig = utils::sig_from_digest_bytes_trial_recovery(sig, digest, &self.pubkey); + sig.apply_eip155(chain_id); Ok(sig) } } @@ -183,9 +192,10 @@ impl Signer for AwsSigner { let message_hash = eip191_hash_message(message); trace!(?message_hash, ?message); - self.sign_digest_with_eip155(message_hash, self.chain_id).await + self.sign_digest_with_eip155(&message_hash, self.chain_id).await } + #[cfg(TODO)] #[instrument(err)] async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { let mut tx_with_chain = tx.clone(); @@ -196,17 +206,15 @@ impl Signer for AwsSigner { self.sign_digest_with_eip155(sighash, chain_id).await } - #[cfg(TODO)] - async fn sign_typed_data( + #[cfg(feature = "eip712")] + async fn sign_typed_data( &self, payload: &T, + domain: &Eip712Domain, ) -> Result { - let digest = - payload.encode_eip712().map_err(|e| Self::Error::Eip712Error(e.to_string()))?; - - let sig = self.sign_digest(digest).await?; - let sig = utils::sig_from_digest_bytes_trial_recovery(&sig, digest, &self.pubkey); - + let digest = payload.eip712_signing_hash(domain); + let sig = self.sign_digest(&digest).await?; + let sig = utils::sig_from_digest_bytes_trial_recovery(sig, &digest, &self.pubkey); Ok(sig) } @@ -236,11 +244,11 @@ async fn request_get_pubkey( async fn request_sign_digest( kms: &Client, key_id: &str, - digest: [u8; 32], + digest: &B256, ) -> Result { kms.sign() .key_id(key_id) - .message(Blob::new(digest)) + .message(Blob::new(digest.as_slice())) .message_type(MessageType::Digest) .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) .send() @@ -289,6 +297,6 @@ mod tests { let message = vec![0, 1, 2, 3]; let sig = signer.sign_message(&message).await.unwrap(); - sig.verify(message, signer.address()).expect("valid sig"); + assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); } } diff --git a/crates/signer-aws/src/utils.rs b/crates/signer-aws/src/utils.rs new file mode 100644 index 00000000000..c200856dc0e --- /dev/null +++ b/crates/signer-aws/src/utils.rs @@ -0,0 +1,38 @@ +//! These utils are NOT meant for general usage. They are ONLY meant for use +//! within this module. They DO NOT perform basic safety checks and may panic +//! if used incorrectly. + +use alloy_primitives::B256; +use alloy_signer::Signature; +use k256::ecdsa::{self, RecoveryId, VerifyingKey}; + +/// Recover an rsig from a signature under a known key by trial/error. +pub(super) fn sig_from_digest_bytes_trial_recovery( + sig: ecdsa::Signature, + digest: &B256, + vk: &VerifyingKey, +) -> Signature { + let mut recid = RecoveryId::from_byte(0).unwrap(); + if check_candidate(&sig, recid, digest, vk) { + return Signature::new(sig, recid); + } + + recid = RecoveryId::from_byte(1).unwrap(); + if check_candidate(&sig, recid, digest, vk) { + return Signature::new(sig, recid); + } + + panic!("bad sig"); +} + +/// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. +fn check_candidate( + sig: &ecdsa::Signature, + recid: RecoveryId, + digest: &B256, + vk: &VerifyingKey, +) -> bool { + VerifyingKey::recover_from_prehash(digest.as_slice(), sig, recid) + .map(|key| key == *vk) + .unwrap_or(false) +} diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml new file mode 100644 index 00000000000..82e1196b726 --- /dev/null +++ b/crates/signer-ledger/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "alloy-signer-ledger" +description = "Ethereum Ledger signer" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +alloy-primitives.workspace = true +alloy-signer.workspace = true + +coins-ledger = { version = "0.8.3", default-features = false } +semver.workspace = true +futures-executor.workspace = true +futures-util.workspace = true +tracing.workspace = true +async-trait.workspace = true +thiserror.workspace = true + +# eip712 +alloy-sol-types = { workspace = true, optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + +[features] +eip712 = ["alloy-signer/eip712", "dep:alloy-sol-types"] diff --git a/crates/signer-ledger/README.md b/crates/signer-ledger/README.md new file mode 100644 index 00000000000..f603b77a70c --- /dev/null +++ b/crates/signer-ledger/README.md @@ -0,0 +1,5 @@ +# alloy-signer-ledger + +Ethereum [Ledger] signer. + +[Ledger]: https://www.ledger.com diff --git a/crates/signers/src/ledger/app.rs b/crates/signer-ledger/src/app.rs similarity index 88% rename from crates/signers/src/ledger/app.rs rename to crates/signer-ledger/src/app.rs index e100ebbf3a8..7b6328c5b74 100644 --- a/crates/signers/src/ledger/app.rs +++ b/crates/signer-ledger/src/app.rs @@ -1,12 +1,19 @@ -use super::types::*; -use crate::{Signature, Transaction, TransactionRequest}; -use alloy_primitives::{hex, keccak256, Address, TxHash, B256, U256}; +//! Ledger Ethereum app wrapper. + +use crate::types::{DerivationType, LedgerError, INS, P1, P1_FIRST, P2}; +use alloy_primitives::{hex, Address}; +use alloy_signer::Signature; use coins_ledger::{ - common::{APDUAnswer, APDUCommand, APDUData}, + common::{APDUCommand, APDUData}, transports::{Ledger, LedgerAsync}, }; use futures_util::lock::Mutex; -use thiserror::Error; + +// TODO: Ledger futures aren't Send. +use futures_executor::block_on; + +#[cfg(feature = "eip712")] +use alloy_sol_types::{Eip712Domain, SolStruct}; /// A Ledger Ethereum App. /// @@ -29,15 +36,14 @@ impl std::fmt::Display for LedgerEthereum { } } -const EIP712_MIN_VERSION: &str = ">=1.6.0"; - impl LedgerEthereum { /// Instantiate the application by acquiring a lock on the ledger device. /// + /// # Examples /// /// ``` /// # async fn foo() -> Result<(), Box> { - /// use ethers_signers::{HDPath, Ledger}; + /// use alloy_signer_ledger::{HDPath, Ledger}; /// /// let ledger = Ledger::new(HDPath::LedgerLive(0), 1).await?; /// # Ok(()) @@ -63,7 +69,6 @@ impl LedgerEthereum { &self, derivation: &DerivationType, ) -> Result { - let data = APDUData::new(&Self::path_to_bytes(derivation)); let transport = self.transport.lock().await; Self::get_address_with_path_transport(&transport, derivation).await } @@ -84,7 +89,7 @@ impl LedgerEthereum { }; debug!("Dispatching get_address request to ethereum app"); - let answer = transport.exchange(&command).await?; + let answer = block_on(transport.exchange(&command))?; let result = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?; let address = { @@ -112,7 +117,7 @@ impl LedgerEthereum { }; debug!("Dispatching get_version"); - let answer = transport.exchange(&command).await?; + let answer = block_on(transport.exchange(&command))?; let result = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?; if result.len() < 4 { return Err(LedgerError::ShortResponse { got: result.len(), at_least: 4 }); @@ -123,6 +128,7 @@ impl LedgerEthereum { } /// Signs an Ethereum transaction (requires confirmation on the ledger) + #[cfg(TODO)] pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { let mut tx_with_chain = tx.clone(); if tx_with_chain.chain_id().is_none() { @@ -160,10 +166,7 @@ impl LedgerEthereum { } /// Signs an ethereum personal message - pub async fn sign_message>( - &self, - message: &[u8], - ) -> Result { + pub async fn sign_message(&self, message: &[u8]) -> Result { let message = message.as_ref(); let mut payload = Self::path_to_bytes(&self.derivation); @@ -174,13 +177,16 @@ impl LedgerEthereum { } /// Signs an EIP712 encoded domain separator and message - pub async fn sign_typed_struct(&self, payload: &T) -> Result - where - T: Eip712, - { + #[cfg(feature = "eip712")] + pub async fn sign_typed_struct( + &self, + strukt: &T, + domain: &Eip712Domain, + ) -> Result { // See comment for v1.6.0 requirement // https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999 - let req = semver::VersionReq::parse(EIP712_MIN_VERSION)?; + const EIP712_MIN_VERSION: &str = ">=1.6.0"; + let req = semver::VersionReq::parse(EIP712_MIN_VERSION).unwrap(); let version = semver::Version::parse(&self.version().await?)?; // Enforce app version is greater than EIP712_MIN_VERSION @@ -188,20 +194,15 @@ impl LedgerEthereum { return Err(LedgerError::UnsupportedAppVersion(EIP712_MIN_VERSION)); } - let domain_separator = - payload.domain_separator().map_err(|e| LedgerError::Eip712Error(e.to_string()))?; - let struct_hash = - payload.struct_hash().map_err(|e| LedgerError::Eip712Error(e.to_string()))?; - let mut payload = Self::path_to_bytes(&self.derivation); - payload.extend_from_slice(&domain_separator); - payload.extend_from_slice(&struct_hash); + payload.extend_from_slice(domain.separator().as_slice()); + payload.extend_from_slice(strukt.eip712_hash_struct().as_slice()); self.sign_payload(INS::SIGN_ETH_EIP_712, &payload).await } - // Helper function for signing either transaction data, personal messages or EIP712 derived - // structs + /// Helper function for signing either transaction data, personal messages or EIP712 derived + /// structs. #[instrument(err, skip_all, fields(command = %command, payload = hex::encode(payload)))] pub async fn sign_payload( &self, @@ -235,7 +236,7 @@ impl LedgerEthereum { command.data = APDUData::new(chunk); debug!("Dispatching packet to device"); - answer = Some(transport.exchange(&command).await?); + answer = Some(block_on(transport.exchange(&command))?); let data = answer.as_ref().expect("just assigned").data(); if data.is_none() { @@ -255,11 +256,10 @@ impl LedgerEthereum { if result.len() < 65 { return Err(LedgerError::ShortResponse { got: result.len(), at_least: 65 }); } - let v = result[0] as u64; - let r = U256::from_big_endian(&result[1..33]); - let s = U256::from_big_endian(&result[33..]); - let sig = Signature { r, s, v }; - debug!(sig = %sig, "Received signature from device"); + + // TODO: don't unwrap + let sig = Signature::from_bytes(&result[1..], result[0] as u64).unwrap(); + debug!(?sig, "Received signature from device"); Ok(sig) } diff --git a/crates/signers/src/ledger/mod.rs b/crates/signer-ledger/src/lib.rs similarity index 51% rename from crates/signers/src/ledger/mod.rs rename to crates/signer-ledger/src/lib.rs index 0e8816a76ac..a5035483e12 100644 --- a/crates/signers/src/ledger/mod.rs +++ b/crates/signer-ledger/src/lib.rs @@ -1,14 +1,37 @@ -pub mod app; -pub mod types; +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + // TODO + // missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -use crate::Signer; +#[macro_use] +extern crate tracing; + +mod app; +pub use app::LedgerEthereum as Ledger; + +mod types; +pub use types::{DerivationType as HDPath, LedgerError}; + +use alloy_primitives::Address; +use alloy_signer::{Signature, Signer}; use app::LedgerEthereum; use async_trait::async_trait; -use ethers_core::types::{ - transaction::{eip2718::TypedTransaction, eip712::Eip712}, - Address, Signature, -}; -use types::LedgerError; + +#[cfg(feature = "eip712")] +use alloy_sol_types::{Eip712Domain, SolStruct}; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -19,6 +42,7 @@ impl Signer for LedgerEthereum { self.sign_message(message).await } + #[cfg(TODO)] async fn sign_transaction(&self, message: &TypedTransaction) -> Result { let mut tx_with_chain = message.clone(); if tx_with_chain.chain_id().is_none() { @@ -28,12 +52,13 @@ impl Signer for LedgerEthereum { self.sign_tx(&tx_with_chain).await } - #[cfg(TODO)] - async fn sign_typed_data( + #[cfg(feature = "eip712")] + async fn sign_typed_data( &self, payload: &T, + domain: &Eip712Domain, ) -> Result { - self.sign_typed_struct(payload).await + self.sign_typed_struct(payload, domain).await } fn address(&self) -> Address { diff --git a/crates/signers/src/ledger/types.rs b/crates/signer-ledger/src/types.rs similarity index 93% rename from crates/signers/src/ledger/types.rs rename to crates/signer-ledger/src/types.rs index cb3af7026c4..becbdbfc59f 100644 --- a/crates/signers/src/ledger/types.rs +++ b/crates/signer-ledger/src/types.rs @@ -4,6 +4,7 @@ #![allow(clippy::upper_case_acronyms)] +use alloy_primitives::hex; use std::fmt; use thiserror::Error; @@ -28,8 +29,8 @@ impl fmt::Display for DerivationType { } } +/// Error when using the Ledger transport. #[derive(Error, Debug)] -/// Error when using the Ledger transport pub enum LedgerError { /// Underlying ledger transport error #[error(transparent)] @@ -57,12 +58,12 @@ pub enum LedgerError { EmptyPayload, } -pub const P1_FIRST: u8 = 0x00; +pub(crate) const P1_FIRST: u8 = 0x00; #[repr(u8)] #[derive(Debug, Copy, Clone, Eq, PartialEq)] -#[allow(non_camel_case_types)] -pub enum INS { +#[allow(non_camel_case_types, dead_code)] +pub(crate) enum INS { GET_PUBLIC_KEY = 0x02, SIGN = 0x04, GET_APP_CONFIGURATION = 0x06, @@ -85,7 +86,7 @@ impl std::fmt::Display for INS { #[repr(u8)] #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[allow(non_camel_case_types)] -pub enum P1 { +pub(crate) enum P1 { NON_CONFIRM = 0x00, MORE = 0x80, } @@ -93,6 +94,6 @@ pub enum P1 { #[repr(u8)] #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[allow(non_camel_case_types)] -pub enum P2 { +pub(crate) enum P2 { NO_CHAINCODE = 0x00, } diff --git a/crates/signer-trezor/Cargo.toml b/crates/signer-trezor/Cargo.toml new file mode 100644 index 00000000000..3a660c3238d --- /dev/null +++ b/crates/signer-trezor/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "alloy-signer-trezor" +description = "Ethereum Trezor signer" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +alloy-primitives.workspace = true +alloy-signer.workspace = true + +# TODO: bump this and remove protobuf pin +trezor-client = { version = "=0.1.0", default-features = false, features = ["ethereum"] } +protobuf = "=3.2.0" + +async-trait.workspace = true +home.workspace = true +semver.workspace = true +thiserror.workspace = true +# tracing.workspace = true + +# eip712 +alloy-sol-types = { workspace = true, optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + +[features] +eip712 = ["alloy-signer/eip712", "dep:alloy-sol-types"] diff --git a/crates/signer-trezor/README.md b/crates/signer-trezor/README.md new file mode 100644 index 00000000000..3c5680df176 --- /dev/null +++ b/crates/signer-trezor/README.md @@ -0,0 +1,5 @@ +# alloy-signer-trezor + +Ethereum [Trezor] signer. + +[Trezor]: https://trezor.io diff --git a/crates/signers/src/trezor/app.rs b/crates/signer-trezor/src/app.rs similarity index 91% rename from crates/signers/src/trezor/app.rs rename to crates/signer-trezor/src/app.rs index 9011bbd58b7..27086d7f7f7 100644 --- a/crates/signers/src/trezor/app.rs +++ b/crates/signer-trezor/src/app.rs @@ -1,21 +1,15 @@ -use super::types::*; -use ethers_core::{ - types::{ - transaction::{eip2718::TypedTransaction, eip712::Eip712}, - Address, NameOrAddress, Signature, Transaction, TransactionRequest, TxHash, B256, U256, - }, - utils::keccak256, -}; -use futures_executor::block_on; -use futures_util::lock::Mutex; +use super::types::{DerivationType, TrezorError}; +use alloy_primitives::{Address, U256}; +use alloy_signer::Signature; use std::{ env, fs, io::{Read, Write}, - path, path::PathBuf, }; -use thiserror::Error; -use trezor_client::client::{AccessListItem as Trezor_AccessListItem, Trezor}; +use trezor_client::client::Trezor; + +#[cfg(feature = "eip712")] +use alloy_sol_types::{Eip712Domain, SolStruct}; /// A Trezor Ethereum App. /// @@ -148,12 +142,11 @@ impl TrezorEthereum { ) -> Result { let mut client = self.get_client(self.session_id.clone())?; let address_str = client.ethereum_get_address(Self::convert_path(derivation))?; - let mut address_bytes = [0; 20]; - hex::decode_to_slice(address_str, &mut address_bytes)?; - Ok(address_bytes.into()) + Ok(address_str.parse()?) } /// Signs an Ethereum transaction (requires confirmation on the Trezor) + #[cfg(TODO)] pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { let mut client = self.get_client(self.session_id.clone())?; @@ -196,9 +189,9 @@ impl TrezorEthereum { } /// Signs an ethereum personal message - pub async fn sign_message>( + pub async fn sign_message + Send + Sync>( &self, - message: &[u8], + message: S, ) -> Result { let message = message.as_ref(); let mut client = self.get_client(self.session_id.clone())?; @@ -206,14 +199,19 @@ impl TrezorEthereum { let signature = client.ethereum_sign_message(message.into(), apath)?; - Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) + let r = U256::from_limbs(signature.r.0); + let s = U256::from_limbs(signature.s.0); + // TODO: don't unwrap + Ok(Signature::from_scalars(r.into(), s.into(), signature.v).unwrap()) } /// Signs an EIP712 encoded domain separator and message - pub async fn sign_typed_struct(&self, payload: &T) -> Result - where - T: Eip712, - { + #[cfg(feature = "eip712")] + pub async fn sign_typed_struct( + &self, + _payload: &T, + _domain: &Eip712Domain, + ) -> Result { unimplemented!() } @@ -221,7 +219,6 @@ impl TrezorEthereum { fn convert_path(derivation: &DerivationType) -> Vec { let derivation = derivation.to_string(); let elements = derivation.split('/').skip(1).collect::>(); - let depth = elements.len(); let mut path = vec![]; for derivation_index in elements { @@ -237,15 +234,10 @@ impl TrezorEthereum { } } -#[cfg(all(test, feature = "trezor"))] +#[cfg(test)] mod tests { use super::*; - use crate::Signer; - use ethers_core::types::{ - transaction::eip2930::{AccessList, AccessListItem}, - Address, Eip1559TransactionRequest, TransactionRequest, I256, U256, - }; - use std::str::FromStr; + use alloy_primitives::address; #[tokio::test] #[ignore] @@ -258,16 +250,17 @@ mod tests { .unwrap(); assert_eq!( trezor.get_address().await.unwrap(), - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), ); assert_eq!( trezor.get_address_with_path(&DerivationType::TrezorLive(0)).await.unwrap(), - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), ); } #[tokio::test] #[ignore] + #[cfg(TODO)] async fn test_sign_tx() { let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); @@ -287,6 +280,7 @@ mod tests { #[tokio::test] #[ignore] + #[cfg(TODO)] async fn test_sign_big_data_tx() { let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); @@ -305,6 +299,7 @@ mod tests { #[tokio::test] #[ignore] + #[cfg(TODO)] async fn test_sign_empty_txes() { // Contract creation (empty `to`), requires data. // To test without the data field, we need to specify a `to` address. @@ -340,6 +335,7 @@ mod tests { #[tokio::test] #[ignore] + #[cfg(TODO)] async fn test_sign_eip1559_tx() { let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); @@ -392,6 +388,6 @@ mod tests { let message = "hello world"; let sig = trezor.sign_message(message).await.unwrap(); let addr = trezor.get_address().await.unwrap(); - sig.verify(message, addr).unwrap(); + assert_eq!(sig.recover_address_from_msg(message).unwrap(), addr); } } diff --git a/crates/signer-trezor/src/lib.rs b/crates/signer-trezor/src/lib.rs new file mode 100644 index 00000000000..2325af56001 --- /dev/null +++ b/crates/signer-trezor/src/lib.rs @@ -0,0 +1,80 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + // TODO: + // missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +// TODO: Add tracing. +// #[macro_use] +// extern crate tracing; + +// TODO: Needed to pin version. +use protobuf as _; + +mod app; +pub use app::TrezorEthereum as Trezor; + +mod types; +pub use types::{DerivationType as TrezorHDPath, TrezorError}; + +use alloy_primitives::Address; +use alloy_signer::{Signature, Signer}; +use app::TrezorEthereum; +use async_trait::async_trait; + +#[cfg(feature = "eip712")] +use alloy_sol_types::{Eip712Domain, SolStruct}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Signer for TrezorEthereum { + type Error = TrezorError; + + async fn sign_message(&self, message: &[u8]) -> Result { + self.sign_message(message).await + } + + #[cfg(TODO)] + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + let mut tx_with_chain = message.clone(); + if tx_with_chain.chain_id().is_none() { + // in the case we don't have a chain_id, let's use the signer chain id instead + tx_with_chain.set_chain_id(self.chain_id); + } + self.sign_tx(&tx_with_chain).await + } + + #[cfg(feature = "eip712")] + async fn sign_typed_data( + &self, + payload: &T, + domain: &Eip712Domain, + ) -> Result { + self.sign_typed_struct(payload, domain).await + } + + fn address(&self) -> Address { + self.address + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } + + fn chain_id(&self) -> u64 { + self.chain_id + } +} diff --git a/crates/signers/src/trezor/types.rs b/crates/signer-trezor/src/types.rs similarity index 87% rename from crates/signers/src/trezor/types.rs rename to crates/signer-trezor/src/types.rs index 045a29ea1f4..72d7992e1ef 100644 --- a/crates/signers/src/trezor/types.rs +++ b/crates/signer-trezor/src/types.rs @@ -4,10 +4,9 @@ #![allow(clippy::upper_case_acronyms)] +use alloy_primitives::{hex, B256, U256}; use std::fmt; use thiserror::Error; - -use ethers_core::types::{transaction::eip2718::TypedTransaction, NameOrAddress, U256}; use trezor_client::client::AccessListItem as Trezor_AccessListItem; #[derive(Clone, Debug)] @@ -58,26 +57,28 @@ pub enum TrezorError { CacheError(String), } -/// Trezor Transaction Struct -pub struct TrezorTransaction { - pub nonce: Vec, - pub gas: Vec, - pub gas_price: Vec, - pub value: Vec, - pub to: String, - pub data: Vec, - pub max_fee_per_gas: Vec, - pub max_priority_fee_per_gas: Vec, - pub access_list: Vec, +/// Trezor transaction. +#[allow(dead_code)] +pub(crate) struct TrezorTransaction { + pub(crate) nonce: Vec, + pub(crate) gas: Vec, + pub(crate) gas_price: Vec, + pub(crate) value: Vec, + pub(crate) to: String, + pub(crate) data: Vec, + pub(crate) max_fee_per_gas: Vec, + pub(crate) max_priority_fee_per_gas: Vec, + pub(crate) access_list: Vec, } impl TrezorTransaction { - fn to_trimmed_big_endian(_value: &U256) -> Vec { - let mut trimmed_value = [0_u8; 32]; - _value.to_big_endian(&mut trimmed_value); - trimmed_value[_value.leading_zeros() as usize / 8..].to_vec() + #[allow(dead_code)] + fn to_trimmed_big_endian(value: &U256) -> Vec { + let trimmed_value = B256::from(*value); + trimmed_value[value.leading_zeros() as usize / 8..].to_vec() } + #[cfg(TODO)] pub fn load(tx: &TypedTransaction) -> Result { let to: String = match tx.to() { Some(v) => match v { diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml new file mode 100644 index 00000000000..a5c1f92302e --- /dev/null +++ b/crates/signer/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "alloy-signer" +description = "Ethereum signer abstraction" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +alloy-primitives.workspace = true +# TODO +# alloy-rpc-types.workspace = true + +alloy-sol-types = { workspace = true, optional = true } + +coins-bip32 = "0.8.7" +coins-bip39 = "0.8.7" +elliptic-curve.workspace = true +k256.workspace = true +rand.workspace = true +thiserror.workspace = true +async-trait.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +eth-keystore = "0.5.0" + +# yubi +yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional = true } + +[dev-dependencies] +serde_json.workspace = true +tempfile.workspace = true +tracing-subscriber.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +yubihsm = { version = "0.42", features = ["secp256k1", "usb", "mockhsm"] } + +[features] +yubihsm = ["dep:yubihsm"] +eip712 = ["dep:alloy-sol-types"] diff --git a/crates/signers/README.md b/crates/signer/README.md similarity index 97% rename from crates/signers/README.md rename to crates/signer/README.md index 68ff7772dc8..8313e3c7b33 100644 --- a/crates/signers/README.md +++ b/crates/signer/README.md @@ -42,7 +42,7 @@ signature.verify("hello world", wallet.address()).unwrap(); Sign an Ethereum prefixed message ([EIP-712](https://eips.ethereum.org/EIPS/eip-712)): ```rust -use alloy_signers::{LocalWallet, Signer}; +use alloy_signer::{LocalWallet, Signer}; let message = "Some data"; let wallet = LocalWallet::random(); diff --git a/crates/signers/src/lib.rs b/crates/signer/src/lib.rs similarity index 56% rename from crates/signers/src/lib.rs rename to crates/signer/src/lib.rs index 120982e3523..4963c034493 100644 --- a/crates/signers/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -11,13 +11,10 @@ clippy::missing_const_for_fn, rustdoc::all )] -// #![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -// #[macro_use] -// extern crate tracing; - mod signature; pub use signature::Signature; @@ -27,38 +24,17 @@ pub use signer::Signer; mod wallet; pub use wallet::{MnemonicBuilder, Wallet, WalletError}; -// #[cfg(all(feature = "ledger", not(target_arch = "wasm32")))] -// mod ledger; -// #[cfg(all(feature = "ledger", not(target_arch = "wasm32")))] -// pub use ledger::{ -// app::LedgerEthereum as Ledger, -// types::{DerivationType as HDPath, LedgerError}, -// }; - -// #[cfg(all(feature = "trezor", not(target_arch = "wasm32")))] -// mod trezor; -// #[cfg(all(feature = "trezor", not(target_arch = "wasm32")))] -// pub use trezor::{ -// app::TrezorEthereum as Trezor, -// types::{DerivationType as TrezorHDPath, TrezorError}, -// }; - -// #[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] -// pub use yubihsm; - -// #[cfg(feature = "aws")] -// mod aws; -// #[cfg(feature = "aws")] -// pub use aws::{AwsSigner, AwsSignerError}; - pub mod utils; +#[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] +pub use yubihsm; + /// Re-export the BIP-32 crate so that wordlists can be accessed conveniently. pub use coins_bip39; /// A wallet instantiated with a locally stored private key pub type LocalWallet = Wallet; -#[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] /// A wallet instantiated with a YubiHSM +#[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] pub type YubiWallet = Wallet>; diff --git a/crates/signers/src/signature.rs b/crates/signer/src/signature.rs similarity index 96% rename from crates/signers/src/signature.rs rename to crates/signer/src/signature.rs index 15f68b28e78..1d85387bcab 100644 --- a/crates/signers/src/signature.rs +++ b/crates/signer/src/signature.rs @@ -1,4 +1,4 @@ -use crate::utils::public_key_to_address; +use crate::utils::{public_key_to_address, to_eip155_v}; use alloy_primitives::{eip191_hash_message, hex, Address, B256}; use elliptic_curve::NonZeroScalar; use k256::{ @@ -188,7 +188,15 @@ impl Signature { /// Sets the recovery ID by normalizing a `v` value. #[inline] pub fn set_v(&mut self, v: u64) { - self.recid = normalize_v(v); + self.set_recid(normalize_v(v)); + } + + /// Modifies the recovery ID by applying [EIP-155] to a `v` value. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + #[inline] + pub fn apply_eip155(&mut self, chain_id: u64) { + self.set_v(to_eip155_v(self.recid.to_byte(), chain_id)); } /// Recovers a [`VerifyingKey`] from this signature and the given message by first hashing the @@ -245,6 +253,7 @@ const fn normalize_v(v: u64) -> RecoveryId { // Case 3: eip155 V value v @ 35.. => ((v - 1) % 2) as u8, }; + debug_assert!(byte <= RecoveryId::MAX); match RecoveryId::from_byte(byte) { Some(recid) => recid, None => unsafe { core::hint::unreachable_unchecked() }, diff --git a/crates/signers/src/signer.rs b/crates/signer/src/signer.rs similarity index 100% rename from crates/signers/src/signer.rs rename to crates/signer/src/signer.rs diff --git a/crates/signers/src/utils.rs b/crates/signer/src/utils.rs similarity index 96% rename from crates/signers/src/utils.rs rename to crates/signer/src/utils.rs index a56b9d171d2..6dbed0d0ec0 100644 --- a/crates/signers/src/utils.rs +++ b/crates/signer/src/utils.rs @@ -8,8 +8,9 @@ use k256::{ }; /// Applies [EIP-155](https://eips.ethereum.org/EIPS/eip-155). -pub const fn to_eip155_v(recovery_id: u8, chain_id: u64) -> u64 { - (recovery_id as u64) + 35 + chain_id * 2 +#[inline] +pub const fn to_eip155_v(v: u8, chain_id: u64) -> u64 { + (v as u64) + 35 + chain_id * 2 } /// Converts an ECDSA private key to its corresponding Ethereum Address. diff --git a/crates/signers/src/wallet/mnemonic.rs b/crates/signer/src/wallet/mnemonic.rs similarity index 98% rename from crates/signers/src/wallet/mnemonic.rs rename to crates/signer/src/wallet/mnemonic.rs index 2edd49c95cc..4b385a0a3b4 100644 --- a/crates/signers/src/wallet/mnemonic.rs +++ b/crates/signer/src/wallet/mnemonic.rs @@ -67,7 +67,7 @@ impl MnemonicBuilder { /// # Examples /// /// ``` - /// use alloy_signers::{MnemonicBuilder, coins_bip39::English}; + /// use alloy_signer::{MnemonicBuilder, coins_bip39::English}; /// # async fn foo() -> Result<(), Box> { /// /// let wallet = MnemonicBuilder::::default() @@ -88,7 +88,7 @@ impl MnemonicBuilder { /// # Examples /// /// ``` - /// use alloy_signers::{coins_bip39::English, MnemonicBuilder}; + /// use alloy_signer::{coins_bip39::English, MnemonicBuilder}; /// # async fn foo() -> Result<(), Box> { /// /// let mut rng = rand::thread_rng(); diff --git a/crates/signers/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs similarity index 99% rename from crates/signers/src/wallet/mod.rs rename to crates/signer/src/wallet/mod.rs index 49f740395e9..05b4cded2a7 100644 --- a/crates/signers/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -27,7 +27,7 @@ mod yubi; /// prefix the message being hashed with the `Ethereum Signed Message` domain separator. /// /// ``` -/// use alloy_signers::{LocalWallet, Signer}; +/// use alloy_signer::{LocalWallet, Signer}; /// /// let wallet = LocalWallet::random(); /// diff --git a/crates/signers/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs similarity index 100% rename from crates/signers/src/wallet/private_key.rs rename to crates/signer/src/wallet/private_key.rs diff --git a/crates/signers/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs similarity index 68% rename from crates/signers/src/wallet/yubi.rs rename to crates/signer/src/wallet/yubi.rs index a69e0a2194e..fbf6de97125 100644 --- a/crates/signers/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -1,11 +1,8 @@ -//! Helpers for creating wallets for YubiHSM2 +//! Helpers for creating wallets for YubiHSM2. + use super::Wallet; -use elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; -use ethers_core::{ - k256::{PublicKey, Secp256k1}, - types::Address, - utils::keccak256, -}; +use crate::utils::raw_public_key_to_address; +use k256::Secp256k1; use yubihsm::{ asymmetric::Algorithm::EcK256, ecdsa::Signer as YubiSigner, object, object::Label, Capability, Client, Connector, Credentials, Domain, @@ -55,27 +52,24 @@ impl Wallet> { impl From> for Wallet> { fn from(signer: YubiSigner) -> Self { - // this will never fail - let public_key = PublicKey::from_encoded_point(signer.public_key()).unwrap(); - let public_key = public_key.to_encoded_point(/* compress = */ false); - let public_key = public_key.as_bytes(); - debug_assert_eq!(public_key[0], 0x04); - let hash = keccak256(&public_key[1..]); - let address = Address::from_slice(&hash[12..]); - - Self { signer, address, chain_id: 1 } + // TODO: ? + // let pubkey = PublicKey::from_encoded_point(signer.public_key()).unwrap(); + // let pubkey = public_key.to_encoded_point(/* compress = */ false); + let pubkey = signer.public_key().as_bytes(); + debug_assert_eq!(pubkey[0], 0x04); + let address = raw_public_key_to_address(&pubkey[1..]); + Self::new_with_signer(signer, address, 1) } } #[cfg(test)] -#[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; use crate::Signer; - use std::str::FromStr; + use alloy_primitives::{address, hex}; - #[tokio::test] - async fn from_key() { + #[test] + fn from_key() { let key = hex::decode("2d8c44dc2dd2f0bea410e342885379192381e82d855b1b112f9b55544f1e0900") .unwrap(); @@ -90,16 +84,13 @@ mod tests { ); let msg = "Some data"; - let sig = wallet.sign_message(msg).await.unwrap(); - assert_eq!(sig.recover(msg).unwrap(), wallet.address()); - assert_eq!( - wallet.address(), - Address::from_str("2DE2C386082Cff9b28D62E60983856CE1139eC49").unwrap() - ); + let sig = wallet.sign_message(msg).unwrap(); + assert_eq!(sig.recover_address_from_msg(msg).unwrap(), wallet.address()); + assert_eq!(wallet.address(), address!("2DE2C386082Cff9b28D62E60983856CE1139eC49")); } - #[tokio::test] - async fn new_key() { + #[test] + fn new_key() { let connector = yubihsm::Connector::mockhsm(); let wallet = Wallet::>::new( connector, @@ -110,7 +101,7 @@ mod tests { ); let msg = "Some data"; - let sig = wallet.sign_message(msg).await.unwrap(); - assert_eq!(sig.recover(msg).unwrap(), wallet.address()); + let sig = wallet.sign_message(msg).unwrap(); + assert_eq!(sig.recover_address_from_msg(msg).unwrap(), wallet.address()); } } diff --git a/crates/signers/Cargo.toml b/crates/signers/Cargo.toml deleted file mode 100644 index 2bcede9cefa..00000000000 --- a/crates/signers/Cargo.toml +++ /dev/null @@ -1,73 +0,0 @@ -[package] -name = "alloy-signers" -description = "Ethereum signer abstraction" - -version.workspace = true -edition.workspace = true -rust-version.workspace = true -authors.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -exclude.workspace = true - -[dependencies] -alloy-primitives.workspace = true -alloy-rpc-types.workspace = true - -alloy-sol-types.workspace = true - -# crypto -coins-bip32 = "0.8.7" -coins-bip39 = "0.8.7" -elliptic-curve.workspace = true -k256.workspace = true -rand.workspace = true -sha2.workspace = true - -# misc -thiserror.workspace = true -tracing.workspace = true -async-trait.workspace = true - -# futures -futures-util = { workspace = true, optional = true } - -# aws -aws-config = { version = "1.0", default-features = false, optional = true } -aws-sdk-kms = { version = "0.39", default-features = false, optional = true } -spki = { workspace = true, optional = true } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -eth-keystore = "0.5.0" -home = { workspace = true, optional = true } - -# ledger -coins-ledger = { version = "0.8.3", default-features = false, optional = true } -semver = { workspace = true, optional = true } - -# trezor -# TODO: bump this and remove protobuf pin -trezor-client = { version = "=0.1.0", default-features = false, features = [ - "ethereum", -], optional = true } -protobuf = { version = "=3.2.0", optional = true } - -# yubi -yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional = true } - -[dev-dependencies] -serde_json.workspace = true -tempfile.workspace = true -tracing-subscriber.workspace = true - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -yubihsm = { version = "0.42", features = ["secp256k1", "usb", "mockhsm"] } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } - -[features] -ledger = ["dep:coins-ledger", "dep:semver", "dep:futures-util"] -trezor = ["dep:trezor-client", "dep:semver", "dep:home", "dep:protobuf", "dep:futures-util"] -aws = ["dep:aws-config", "dep:aws-sdk-kms", "dep:spki"] -yubi = ["dep:yubihsm"] -eip712 = [] diff --git a/crates/signers/src/aws/utils.rs b/crates/signers/src/aws/utils.rs deleted file mode 100644 index b649ab8118e..00000000000 --- a/crates/signers/src/aws/utils.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! These utils are NOT meant for general usage. They are ONLY meant for use -//! within this module. They DO NOT perform basic safety checks and may panic -//! if used incorrectly. - -use ethers_core::{ - k256::{ - ecdsa::{RecoveryId, Signature as RSig, Signature as KSig, VerifyingKey}, - FieldBytes, - }, - types::{Signature as EthSig, U256}, -}; - -/// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. -fn check_candidate( - sig: &RSig, - recovery_id: RecoveryId, - digest: [u8; 32], - vk: &VerifyingKey, -) -> bool { - VerifyingKey::recover_from_prehash(digest.as_slice(), sig, recovery_id) - .map(|key| key == *vk) - .unwrap_or(false) -} - -/// Recover an rsig from a signature under a known key by trial/error. -pub(super) fn sig_from_digest_bytes_trial_recovery( - sig: &KSig, - digest: [u8; 32], - vk: &VerifyingKey, -) -> EthSig { - let r_bytes: FieldBytes = sig.r().into(); - let s_bytes: FieldBytes = sig.s().into(); - let r = U256::from_big_endian(r_bytes.as_slice()); - let s = U256::from_big_endian(s_bytes.as_slice()); - - if check_candidate(sig, RecoveryId::from_byte(0).unwrap(), digest, vk) { - EthSig { r, s, v: 0 } - } else if check_candidate(sig, RecoveryId::from_byte(1).unwrap(), digest, vk) { - EthSig { r, s, v: 1 } - } else { - panic!("bad sig"); - } -} - -/// Modify the `v` value of a signature to conform to EIP-155. -pub(super) fn apply_eip155(sig: &mut EthSig, chain_id: u64) { - let v = (chain_id * 2 + 35) + sig.v; - sig.v = v; -} diff --git a/crates/signers/src/trezor/mod.rs b/crates/signers/src/trezor/mod.rs deleted file mode 100644 index f4012d1597e..00000000000 --- a/crates/signers/src/trezor/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -pub mod app; -pub mod types; - -use crate::Signer; -use app::TrezorEthereum; -use async_trait::async_trait; -use ethers_core::types::{ - transaction::{eip2718::TypedTransaction, eip712::Eip712}, - Address, Signature, -}; -use types::TrezorError; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Signer for TrezorEthereum { - type Error = TrezorError; - - async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_message(message).await - } - - async fn sign_transaction(&self, message: &TypedTransaction) -> Result { - let mut tx_with_chain = message.clone(); - if tx_with_chain.chain_id().is_none() { - // in the case we don't have a chain_id, let's use the signer chain id instead - tx_with_chain.set_chain_id(self.chain_id); - } - self.sign_tx(&tx_with_chain).await - } - - #[cfg(TODO)] - async fn sign_typed_data( - &self, - payload: &T, - ) -> Result { - self.sign_typed_struct(payload).await - } - - fn address(&self) -> Address { - self.address - } - - fn with_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); - self - } - - fn chain_id(&self) -> u64 { - self.chain_id - } -} From ebccc56f3b3a89b845de05a900450c831698a6a3 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 24 Nov 2023 06:28:26 +0100 Subject: [PATCH 10/42] fixes --- crates/signer-aws/src/utils.rs | 25 +++++++++---------------- crates/signer/src/signature.rs | 28 +++++++++++++++++++--------- crates/signer/src/wallet/yubi.rs | 15 ++++++++------- deny.toml | 1 + 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/crates/signer-aws/src/utils.rs b/crates/signer-aws/src/utils.rs index c200856dc0e..a25801f755e 100644 --- a/crates/signer-aws/src/utils.rs +++ b/crates/signer-aws/src/utils.rs @@ -9,30 +9,23 @@ use k256::ecdsa::{self, RecoveryId, VerifyingKey}; /// Recover an rsig from a signature under a known key by trial/error. pub(super) fn sig_from_digest_bytes_trial_recovery( sig: ecdsa::Signature, - digest: &B256, + hash: &B256, vk: &VerifyingKey, ) -> Signature { - let mut recid = RecoveryId::from_byte(0).unwrap(); - if check_candidate(&sig, recid, digest, vk) { - return Signature::new(sig, recid); + let mut signature = Signature::new(sig, RecoveryId::from_byte(0).unwrap()); + if check_candidate(&signature, hash, vk) { + return signature; } - recid = RecoveryId::from_byte(1).unwrap(); - if check_candidate(&sig, recid, digest, vk) { - return Signature::new(sig, recid); + signature.set_v(1); + if check_candidate(&signature, hash, vk) { + return signature; } panic!("bad sig"); } /// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. -fn check_candidate( - sig: &ecdsa::Signature, - recid: RecoveryId, - digest: &B256, - vk: &VerifyingKey, -) -> bool { - VerifyingKey::recover_from_prehash(digest.as_slice(), sig, recid) - .map(|key| key == *vk) - .unwrap_or(false) +fn check_candidate(signature: &Signature, hash: &B256, vk: &VerifyingKey) -> bool { + signature.recover_from_prehash(hash).map(|key| key == *vk).unwrap_or(false) } diff --git a/crates/signer/src/signature.rs b/crates/signer/src/signature.rs index 1d85387bcab..d6764adb07c 100644 --- a/crates/signer/src/signature.rs +++ b/crates/signer/src/signature.rs @@ -81,15 +81,10 @@ impl Signature { /// /// [1]: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki #[inline] - pub fn new(mut inner: ecdsa::Signature, mut recid: RecoveryId) -> Self { - // Normalize into "low S" form. See: - // - https://github.com/RustCrypto/elliptic-curves/issues/988 - // - https://github.com/bluealloy/revm/pull/870 - if let Some(normalized) = inner.normalize_s() { - inner = normalized; - recid = RecoveryId::from_byte(recid.to_byte() ^ 1).unwrap(); - } - Self::new_not_normalized(inner, recid) + pub fn new(inner: ecdsa::Signature, recid: RecoveryId) -> Self { + let mut sig = Self::new_not_normalized(inner, recid); + sig.normalize_s(); + sig } /// Creates a new signature from the given inner signature and recovery ID, without normalizing @@ -99,6 +94,21 @@ impl Signature { Self { inner, recid } } + /// Normalizes the signature into "low S" form as described in + /// [BIP 0062: Dealing with Malleability][1]. + /// + /// [1]: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki + #[inline] + pub fn normalize_s(&mut self) { + // Normalize into "low S" form. See: + // - https://github.com/RustCrypto/elliptic-curves/issues/988 + // - https://github.com/bluealloy/revm/pull/870 + if let Some(normalized) = self.inner.normalize_s() { + self.inner = normalized; + self.recid = RecoveryId::from_byte(self.recid.to_byte() ^ 1).unwrap(); + } + } + /// Parses a signature from a byte slice. #[inline] pub fn from_bytes(bytes: &[u8], v: u64) -> Result { diff --git a/crates/signer/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs index fbf6de97125..849b0c66fc8 100644 --- a/crates/signer/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -2,7 +2,8 @@ use super::Wallet; use crate::utils::raw_public_key_to_address; -use k256::Secp256k1; +use elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; +use k256::{PublicKey, Secp256k1}; use yubihsm::{ asymmetric::Algorithm::EcK256, ecdsa::Signer as YubiSigner, object, object::Label, Capability, Client, Connector, Credentials, Domain, @@ -52,12 +53,12 @@ impl Wallet> { impl From> for Wallet> { fn from(signer: YubiSigner) -> Self { - // TODO: ? - // let pubkey = PublicKey::from_encoded_point(signer.public_key()).unwrap(); - // let pubkey = public_key.to_encoded_point(/* compress = */ false); - let pubkey = signer.public_key().as_bytes(); - debug_assert_eq!(pubkey[0], 0x04); - let address = raw_public_key_to_address(&pubkey[1..]); + // Uncompress the public key. + let pubkey = PublicKey::from_encoded_point(signer.public_key()).unwrap(); + let pubkey = pubkey.to_encoded_point(false); + let bytes = pubkey.as_bytes(); + debug_assert_eq!(bytes[0], 0x04); + let address = raw_public_key_to_address(&bytes[1..]); Self::new_with_signer(signer, address, 1) } } diff --git a/deny.toml b/deny.toml index 8d7bf3ea155..e069ddd7932 100644 --- a/deny.toml +++ b/deny.toml @@ -37,6 +37,7 @@ exceptions = [ # so we prefer to not have dependencies using it # https://tldrlegal.com/license/creative-commons-cc0-1.0-universal { allow = ["CC0-1.0"], name = "tiny-keccak" }, + { allow = ["CC0-1.0"], name = "trezor-client" }, ] [[licenses.clarify]] From 9b0952dec680d2e163559c4bc738e2a737a72815 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 24 Nov 2023 06:30:59 +0100 Subject: [PATCH 11/42] chore: clippy --- crates/json-rpc/Cargo.toml | 2 +- crates/signer-aws/src/lib.rs | 1 - crates/signer-ledger/src/app.rs | 4 ++-- crates/signer-trezor/src/types.rs | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/json-rpc/Cargo.toml b/crates/json-rpc/Cargo.toml index 65837e5b76d..1e607f6d4af 100644 --- a/crates/json-rpc/Cargo.toml +++ b/crates/json-rpc/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true exclude.workspace = true [dependencies] -alloy-primitives.workspace = true +alloy-primitives = { workspace = true, features = ["serde"] } serde.workspace = true serde_json = { workspace = true, features = ["raw_value"] } thiserror.workspace = true diff --git a/crates/signer-aws/src/lib.rs b/crates/signer-aws/src/lib.rs index 82685f070c5..ea3e67f6854 100644 --- a/crates/signer-aws/src/lib.rs +++ b/crates/signer-aws/src/lib.rs @@ -188,7 +188,6 @@ impl Signer for AwsSigner { #[instrument(err, skip(message))] async fn sign_message(&self, message: &[u8]) -> Result { - let message = message.as_ref(); let message_hash = eip191_hash_message(message); trace!(?message_hash, ?message); diff --git a/crates/signer-ledger/src/app.rs b/crates/signer-ledger/src/app.rs index 7b6328c5b74..906fa169688 100644 --- a/crates/signer-ledger/src/app.rs +++ b/crates/signer-ledger/src/app.rs @@ -166,7 +166,7 @@ impl LedgerEthereum { } /// Signs an ethereum personal message - pub async fn sign_message(&self, message: &[u8]) -> Result { + pub async fn sign_message>(&self, message: T) -> Result { let message = message.as_ref(); let mut payload = Self::path_to_bytes(&self.derivation); @@ -207,7 +207,7 @@ impl LedgerEthereum { pub async fn sign_payload( &self, command: INS, - payload: &Vec, + payload: &[u8], ) -> Result { if payload.is_empty() { return Err(LedgerError::EmptyPayload); diff --git a/crates/signer-trezor/src/types.rs b/crates/signer-trezor/src/types.rs index 72d7992e1ef..b18d1892a9c 100644 --- a/crates/signer-trezor/src/types.rs +++ b/crates/signer-trezor/src/types.rs @@ -75,7 +75,7 @@ impl TrezorTransaction { #[allow(dead_code)] fn to_trimmed_big_endian(value: &U256) -> Vec { let trimmed_value = B256::from(*value); - trimmed_value[value.leading_zeros() as usize / 8..].to_vec() + trimmed_value[value.leading_zeros() / 8..].to_vec() } #[cfg(TODO)] From 49e8a353387f4d1f52be39e76cd132e70aed0390 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Fri, 24 Nov 2023 06:31:36 +0100 Subject: [PATCH 12/42] sort --- crates/signer-aws/Cargo.toml | 6 +++--- crates/signer-ledger/Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/signer-aws/Cargo.toml b/crates/signer-aws/Cargo.toml index b1fc776548d..bafd0dce334 100644 --- a/crates/signer-aws/Cargo.toml +++ b/crates/signer-aws/Cargo.toml @@ -15,12 +15,12 @@ exclude.workspace = true alloy-primitives.workspace = true alloy-signer.workspace = true +async-trait.workspace = true aws-sdk-kms = { version = "0.39", default-features = false } -spki.workspace = true k256.workspace = true -tracing.workspace = true -async-trait.workspace = true +spki.workspace = true thiserror.workspace = true +tracing.workspace = true # eip712 alloy-sol-types = { workspace = true, optional = true } diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml index 82e1196b726..e46237fdf6d 100644 --- a/crates/signer-ledger/Cargo.toml +++ b/crates/signer-ledger/Cargo.toml @@ -15,13 +15,13 @@ exclude.workspace = true alloy-primitives.workspace = true alloy-signer.workspace = true +async-trait.workspace = true coins-ledger = { version = "0.8.3", default-features = false } -semver.workspace = true futures-executor.workspace = true futures-util.workspace = true -tracing.workspace = true -async-trait.workspace = true +semver.workspace = true thiserror.workspace = true +tracing.workspace = true # eip712 alloy-sol-types = { workspace = true, optional = true } From 8254a51df34f7067a088596f6b415700ca9dc70e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:19:09 +0100 Subject: [PATCH 13/42] chore: consolidate aws to a single file --- crates/signer-aws/src/lib.rs | 283 +----------------------------- crates/signer-aws/src/signer.rs | 302 ++++++++++++++++++++++++++++++++ crates/signer-aws/src/utils.rs | 31 ---- 3 files changed, 304 insertions(+), 312 deletions(-) create mode 100644 crates/signer-aws/src/signer.rs delete mode 100644 crates/signer-aws/src/utils.rs diff --git a/crates/signer-aws/src/lib.rs b/crates/signer-aws/src/lib.rs index ea3e67f6854..5669b96d1a5 100644 --- a/crates/signer-aws/src/lib.rs +++ b/crates/signer-aws/src/lib.rs @@ -6,7 +6,6 @@ #![warn( missing_copy_implementations, missing_debug_implementations, - // TODO // missing_docs, unreachable_pub, clippy::missing_const_for_fn, @@ -19,283 +18,5 @@ #[macro_use] extern crate tracing; -use alloy_primitives::{hex, utils::eip191_hash_message, Address, B256}; -use alloy_signer::{Signature as EthSig, Signer}; -use aws_sdk_kms::{ - error::SdkError, - operation::{ - get_public_key::{GetPublicKeyError, GetPublicKeyOutput}, - sign::{SignError, SignOutput}, - }, - primitives::Blob, - types::{MessageType, SigningAlgorithmSpec}, - Client, -}; -use k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey}; -use std::fmt; - -#[cfg(feature = "eip712")] -use alloy_sol_types::{Eip712Domain, SolStruct}; - -mod utils; - -/// An Ethers signer that uses keys held in Amazon Web Services Key Management Service (AWS KMS). -/// -/// The AWS Signer passes signing requests to the cloud service. AWS KMS keys -/// are identified by a UUID, the `key_id`. -/// -/// Because the public key is unknown, we retrieve it on instantiation of the -/// signer. This means that the new function is `async` and must be called -/// within some runtime. -/// -/// # Examples -/// -/// ```no_run -/// use alloy_signer::Signer; -/// use alloy_signer_aws::AwsSigner; -/// use aws_config::BehaviorVersion; -/// -/// # async fn test() { -/// let config = aws_config::load_defaults(BehaviorVersion::latest()).await; -/// let client = aws_sdk_kms::Client::new(&config); -/// -/// let key_id = "..."; -/// let chain_id = 1; -/// let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); -/// -/// let message = vec![0, 1, 2, 3]; -/// -/// let sig = signer.sign_message(&message).await.unwrap(); -/// assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); -/// # } -/// ``` -#[derive(Clone)] -pub struct AwsSigner { - kms: Client, - chain_id: u64, - key_id: String, - pubkey: VerifyingKey, - address: Address, -} - -impl fmt::Debug for AwsSigner { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AwsSigner") - .field("key_id", &self.key_id) - .field("chain_id", &self.chain_id) - .field("pubkey", &hex::encode(self.pubkey.to_sec1_bytes())) - .field("address", &self.address) - .finish() - } -} - -/// Errors thrown by [`AwsSigner`]. -#[derive(thiserror::Error, Debug)] -pub enum AwsSignerError { - #[error(transparent)] - SignError(#[from] SdkError), - #[error(transparent)] - GetPublicKeyError(#[from] SdkError), - #[error(transparent)] - K256(#[from] K256Error), - #[error(transparent)] - Spki(#[from] spki::Error), - /// Error when converting from a hex string - #[error(transparent)] - HexError(#[from] hex::FromHexError), - /// Error type from Eip712Error message - #[error("failed encoding eip712 struct: {0:?}")] - Eip712Error(String), - #[error("{0}")] - Other(String), -} - -impl From for AwsSignerError { - fn from(value: String) -> Self { - Self::Other(value) - } -} - -impl AwsSigner { - /// Instantiate a new signer from an existing `Client` and key ID. - /// - /// This function retrieves the public key from AWS and calculates the - /// Etheruem address. It is therefore `async`. - #[instrument(err, skip_all, fields(key_id = %key_id.as_ref()))] - pub async fn new>( - kms: Client, - key_id: T, - chain_id: u64, - ) -> Result { - let key_id = key_id.as_ref(); - let resp = request_get_pubkey(&kms, key_id).await?; - let pubkey = decode_pubkey(resp)?; - let address = alloy_signer::utils::public_key_to_address(&pubkey); - - debug!( - "Instantiated AWS signer with pubkey 0x{} and address {address:?}", - hex::encode(pubkey.to_sec1_bytes()), - ); - - Ok(Self { kms, chain_id, key_id: key_id.into(), pubkey, address }) - } - - /// Fetch the pubkey associated with a key ID. - pub async fn get_pubkey_for_key(&self, key_id: T) -> Result - where - T: AsRef, - { - request_get_pubkey(&self.kms, key_id.as_ref()).await.and_then(decode_pubkey) - } - - /// Fetch the pubkey associated with this signer's key ID. - pub async fn get_pubkey(&self) -> Result { - self.get_pubkey_for_key(&self.key_id).await - } - - /// Sign a digest with the key associated with a key ID. - pub async fn sign_digest_with_key>( - &self, - key_id: T, - digest: &B256, - ) -> Result { - request_sign_digest(&self.kms, key_id.as_ref(), digest).await.and_then(decode_signature) - } - - /// Sign a digest with this signer's key - pub async fn sign_digest(&self, digest: &B256) -> Result { - self.sign_digest_with_key(self.key_id.clone(), digest).await - } - - /// Sign a digest with this signer's key and add the eip155 `v` value - /// corresponding to the input chain_id - #[instrument(err, skip(digest), fields(digest = %hex::encode(digest)))] - async fn sign_digest_with_eip155( - &self, - digest: &B256, - chain_id: u64, - ) -> Result { - let sig = self.sign_digest(digest).await?; - let mut sig = utils::sig_from_digest_bytes_trial_recovery(sig, digest, &self.pubkey); - sig.apply_eip155(chain_id); - Ok(sig) - } -} - -#[async_trait::async_trait] -impl Signer for AwsSigner { - type Error = AwsSignerError; - - #[instrument(err, skip(message))] - async fn sign_message(&self, message: &[u8]) -> Result { - let message_hash = eip191_hash_message(message); - trace!(?message_hash, ?message); - - self.sign_digest_with_eip155(&message_hash, self.chain_id).await - } - - #[cfg(TODO)] - #[instrument(err)] - async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - let mut tx_with_chain = tx.clone(); - let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); - tx_with_chain.set_chain_id(chain_id); - - let sighash = tx_with_chain.sighash(); - self.sign_digest_with_eip155(sighash, chain_id).await - } - - #[cfg(feature = "eip712")] - async fn sign_typed_data( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result { - let digest = payload.eip712_signing_hash(domain); - let sig = self.sign_digest(&digest).await?; - let sig = utils::sig_from_digest_bytes_trial_recovery(sig, &digest, &self.pubkey); - Ok(sig) - } - - fn address(&self) -> Address { - self.address - } - - fn chain_id(&self) -> u64 { - self.chain_id - } - - fn with_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); - self - } -} - -#[instrument(err, skip(kms))] -async fn request_get_pubkey( - kms: &Client, - key_id: &str, -) -> Result { - kms.get_public_key().key_id(key_id).send().await.map_err(Into::into) -} - -#[instrument(err, skip(kms, digest), fields(digest = %hex::encode(digest)))] -async fn request_sign_digest( - kms: &Client, - key_id: &str, - digest: &B256, -) -> Result { - kms.sign() - .key_id(key_id) - .message(Blob::new(digest.as_slice())) - .message_type(MessageType::Digest) - .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) - .send() - .await - .map_err(Into::into) -} - -/// Decode an AWS KMS Pubkey response. -fn decode_pubkey(resp: GetPublicKeyOutput) -> Result { - let raw = resp - .public_key - .as_ref() - .ok_or_else(|| AwsSignerError::from("Pubkey not found in response".to_owned()))?; - - let spki = spki::SubjectPublicKeyInfoRef::try_from(raw.as_ref())?; - let key = VerifyingKey::from_sec1_bytes(spki.subject_public_key.raw_bytes())?; - - Ok(key) -} - -/// Decode an AWS KMS Signature response. -fn decode_signature(resp: SignOutput) -> Result { - let raw = resp - .signature - .as_ref() - .ok_or_else(|| AwsSignerError::from("Signature not found in response".to_owned()))?; - - let sig = KSig::from_der(raw.as_ref())?; - Ok(sig.normalize_s().unwrap_or(sig)) -} - -#[cfg(test)] -mod tests { - use super::*; - use aws_config::BehaviorVersion; - - #[tokio::test] - async fn sign_message() { - let Ok(key_id) = std::env::var("AWS_KEY_ID") else { return }; - let config = aws_config::load_defaults(BehaviorVersion::latest()).await; - let client = aws_sdk_kms::Client::new(&config); - - let chain_id = 1; - let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); - - let message = vec![0, 1, 2, 3]; - - let sig = signer.sign_message(&message).await.unwrap(); - assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); - } -} +mod signer; +pub use signer::{AwsSigner, AwsSignerError}; diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs new file mode 100644 index 00000000000..4c35312083d --- /dev/null +++ b/crates/signer-aws/src/signer.rs @@ -0,0 +1,302 @@ +use alloy_primitives::{hex, utils::eip191_hash_message, Address, B256}; +use alloy_signer::{Signature as EthSig, Signature, Signer}; +use aws_sdk_kms::{ + error::SdkError, + operation::{ + get_public_key::{GetPublicKeyError, GetPublicKeyOutput}, + sign::{SignError, SignOutput}, + }, + primitives::Blob, + types::{MessageType, SigningAlgorithmSpec}, + Client, +}; +use k256::ecdsa::{self, Error as K256Error, RecoveryId, Signature as KSig, VerifyingKey}; +use std::fmt; + +#[cfg(feature = "eip712")] +use alloy_sol_types::{Eip712Domain, SolStruct}; + +/// An Ethers signer that uses keys held in Amazon Web Services Key Management Service (AWS KMS). +/// +/// The AWS Signer passes signing requests to the cloud service. AWS KMS keys +/// are identified by a UUID, the `key_id`. +/// +/// Because the public key is unknown, we retrieve it on instantiation of the +/// signer. This means that the new function is `async` and must be called +/// within some runtime. +/// +/// # Examples +/// +/// ```no_run +/// use alloy_signer::Signer; +/// use alloy_signer_aws::AwsSigner; +/// use aws_config::BehaviorVersion; +/// +/// # async fn test() { +/// let config = aws_config::load_defaults(BehaviorVersion::latest()).await; +/// let client = aws_sdk_kms::Client::new(&config); +/// +/// let key_id = "..."; +/// let chain_id = 1; +/// let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); +/// +/// let message = vec![0, 1, 2, 3]; +/// +/// let sig = signer.sign_message(&message).await.unwrap(); +/// assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); +/// # } +/// ``` +#[derive(Clone)] +pub struct AwsSigner { + kms: Client, + chain_id: u64, + key_id: String, + pubkey: VerifyingKey, + address: Address, +} + +impl fmt::Debug for AwsSigner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AwsSigner") + .field("key_id", &self.key_id) + .field("chain_id", &self.chain_id) + .field("pubkey", &hex::encode(self.pubkey.to_sec1_bytes())) + .field("address", &self.address) + .finish() + } +} + +/// Errors thrown by [`AwsSigner`]. +#[derive(thiserror::Error, Debug)] +pub enum AwsSignerError { + #[error(transparent)] + SignError(#[from] SdkError), + #[error(transparent)] + GetPublicKeyError(#[from] SdkError), + #[error(transparent)] + K256(#[from] K256Error), + #[error(transparent)] + Spki(#[from] spki::Error), + /// Error when converting from a hex string + #[error(transparent)] + HexError(#[from] hex::FromHexError), + /// Error type from Eip712Error message + #[error("failed encoding eip712 struct: {0:?}")] + Eip712Error(String), + #[error("{0}")] + Other(String), +} + +impl From for AwsSignerError { + fn from(value: String) -> Self { + Self::Other(value) + } +} + +impl AwsSigner { + /// Instantiate a new signer from an existing `Client` and key ID. + /// + /// This function retrieves the public key from AWS and calculates the + /// Etheruem address. It is therefore `async`. + #[instrument(err, skip_all, fields(key_id = %key_id.as_ref()))] + pub async fn new>( + kms: Client, + key_id: T, + chain_id: u64, + ) -> Result { + let key_id = key_id.as_ref(); + let resp = request_get_pubkey(&kms, key_id).await?; + let pubkey = decode_pubkey(resp)?; + let address = alloy_signer::utils::public_key_to_address(&pubkey); + + debug!( + "Instantiated AWS signer with pubkey 0x{} and address {address:?}", + hex::encode(pubkey.to_sec1_bytes()), + ); + + Ok(Self { kms, chain_id, key_id: key_id.into(), pubkey, address }) + } + + /// Fetch the pubkey associated with a key ID. + pub async fn get_pubkey_for_key(&self, key_id: T) -> Result + where + T: AsRef, + { + request_get_pubkey(&self.kms, key_id.as_ref()).await.and_then(decode_pubkey) + } + + /// Fetch the pubkey associated with this signer's key ID. + pub async fn get_pubkey(&self) -> Result { + self.get_pubkey_for_key(&self.key_id).await + } + + /// Sign a digest with the key associated with a key ID. + pub async fn sign_digest_with_key>( + &self, + key_id: T, + digest: &B256, + ) -> Result { + request_sign_digest(&self.kms, key_id.as_ref(), digest).await.and_then(decode_signature) + } + + /// Sign a digest with this signer's key + pub async fn sign_digest(&self, digest: &B256) -> Result { + self.sign_digest_with_key(self.key_id.clone(), digest).await + } + + /// Sign a digest with this signer's key and add the eip155 `v` value + /// corresponding to the input chain_id + #[instrument(err, skip(digest), fields(digest = %hex::encode(digest)))] + async fn sign_digest_with_eip155( + &self, + digest: &B256, + chain_id: u64, + ) -> Result { + let sig = self.sign_digest(digest).await?; + let mut sig = sig_from_digest_bytes_trial_recovery(sig, digest, &self.pubkey); + sig.apply_eip155(chain_id); + Ok(sig) + } +} + +#[async_trait::async_trait] +impl Signer for AwsSigner { + type Error = AwsSignerError; + + #[instrument(err, skip(message))] + async fn sign_message(&self, message: &[u8]) -> Result { + let message_hash = eip191_hash_message(message); + trace!(?message_hash, ?message); + + self.sign_digest_with_eip155(&message_hash, self.chain_id).await + } + + #[cfg(TODO)] + #[instrument(err)] + async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { + let mut tx_with_chain = tx.clone(); + let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + tx_with_chain.set_chain_id(chain_id); + + let sighash = tx_with_chain.sighash(); + self.sign_digest_with_eip155(sighash, chain_id).await + } + + #[cfg(feature = "eip712")] + async fn sign_typed_data( + &self, + payload: &T, + domain: &Eip712Domain, + ) -> Result { + let digest = payload.eip712_signing_hash(domain); + let sig = self.sign_digest(&digest).await?; + let sig = sig_from_digest_bytes_trial_recovery(sig, &digest, &self.pubkey); + Ok(sig) + } + + fn address(&self) -> Address { + self.address + } + + fn chain_id(&self) -> u64 { + self.chain_id + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } +} + +#[instrument(err, skip(kms))] +async fn request_get_pubkey( + kms: &Client, + key_id: &str, +) -> Result { + kms.get_public_key().key_id(key_id).send().await.map_err(Into::into) +} + +#[instrument(err, skip(kms, digest), fields(digest = %hex::encode(digest)))] +async fn request_sign_digest( + kms: &Client, + key_id: &str, + digest: &B256, +) -> Result { + kms.sign() + .key_id(key_id) + .message(Blob::new(digest.as_slice())) + .message_type(MessageType::Digest) + .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) + .send() + .await + .map_err(Into::into) +} + +/// Decode an AWS KMS Pubkey response. +fn decode_pubkey(resp: GetPublicKeyOutput) -> Result { + let raw = resp + .public_key + .as_ref() + .ok_or_else(|| AwsSignerError::from("Pubkey not found in response".to_owned()))?; + + let spki = spki::SubjectPublicKeyInfoRef::try_from(raw.as_ref())?; + let key = VerifyingKey::from_sec1_bytes(spki.subject_public_key.raw_bytes())?; + + Ok(key) +} + +/// Decode an AWS KMS Signature response. +fn decode_signature(resp: SignOutput) -> Result { + let raw = resp + .signature + .as_ref() + .ok_or_else(|| AwsSignerError::from("Signature not found in response".to_owned()))?; + + let sig = KSig::from_der(raw.as_ref())?; + Ok(sig.normalize_s().unwrap_or(sig)) +} + +/// Recover an rsig from a signature under a known key by trial/error. +fn sig_from_digest_bytes_trial_recovery( + sig: ecdsa::Signature, + hash: &B256, + vk: &VerifyingKey, +) -> Signature { + let mut signature = Signature::new(sig, RecoveryId::from_byte(0).unwrap()); + if check_candidate(&signature, hash, vk) { + return signature; + } + + signature.set_v(1); + if check_candidate(&signature, hash, vk) { + return signature; + } + + panic!("bad sig"); +} + +/// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. +fn check_candidate(signature: &Signature, hash: &B256, vk: &VerifyingKey) -> bool { + signature.recover_from_prehash(hash).map(|key| key == *vk).unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_config::BehaviorVersion; + + #[tokio::test] + async fn sign_message() { + let Ok(key_id) = std::env::var("AWS_KEY_ID") else { return }; + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let client = aws_sdk_kms::Client::new(&config); + + let chain_id = 1; + let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); + + let message = vec![0, 1, 2, 3]; + + let sig = signer.sign_message(&message).await.unwrap(); + assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); + } +} diff --git a/crates/signer-aws/src/utils.rs b/crates/signer-aws/src/utils.rs deleted file mode 100644 index a25801f755e..00000000000 --- a/crates/signer-aws/src/utils.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! These utils are NOT meant for general usage. They are ONLY meant for use -//! within this module. They DO NOT perform basic safety checks and may panic -//! if used incorrectly. - -use alloy_primitives::B256; -use alloy_signer::Signature; -use k256::ecdsa::{self, RecoveryId, VerifyingKey}; - -/// Recover an rsig from a signature under a known key by trial/error. -pub(super) fn sig_from_digest_bytes_trial_recovery( - sig: ecdsa::Signature, - hash: &B256, - vk: &VerifyingKey, -) -> Signature { - let mut signature = Signature::new(sig, RecoveryId::from_byte(0).unwrap()); - if check_candidate(&signature, hash, vk) { - return signature; - } - - signature.set_v(1); - if check_candidate(&signature, hash, vk) { - return signature; - } - - panic!("bad sig"); -} - -/// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. -fn check_candidate(signature: &Signature, hash: &B256, vk: &VerifyingKey) -> bool { - signature.recover_from_prehash(hash).map(|key| key == *vk).unwrap_or(false) -} From 1d7aa37b7d252b52315c532e4a54f5b9dfc49e0c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:23:44 +0100 Subject: [PATCH 14/42] rename `{Ledger,Trezor}` to `{Ledger,Trezor}Signer` --- crates/signer-ledger/src/app.rs | 14 +++++++------- crates/signer-ledger/src/lib.rs | 19 +++++++++++-------- crates/signer-trezor/src/app.rs | 18 +++++++++--------- crates/signer-trezor/src/lib.rs | 19 +++++++++++-------- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/crates/signer-ledger/src/app.rs b/crates/signer-ledger/src/app.rs index 906fa169688..f6a193db014 100644 --- a/crates/signer-ledger/src/app.rs +++ b/crates/signer-ledger/src/app.rs @@ -19,14 +19,14 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// /// This is a simple wrapper around the [Ledger transport](Ledger) #[derive(Debug)] -pub struct LedgerEthereum { +pub struct LedgerSigner { transport: Mutex, derivation: DerivationType, pub(crate) chain_id: u64, pub(crate) address: Address, } -impl std::fmt::Display for LedgerEthereum { +impl std::fmt::Display for LedgerSigner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, @@ -36,7 +36,7 @@ impl std::fmt::Display for LedgerEthereum { } } -impl LedgerEthereum { +impl LedgerSigner { /// Instantiate the application by acquiring a lock on the ledger device. /// /// # Examples @@ -296,7 +296,7 @@ mod tests { // Replace this with your ETH addresses. async fn test_get_address() { // Instantiate it with the default ledger derivation path - let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).await.unwrap(); + let ledger = LedgerSigner::new(DerivationType::LedgerLive(0), 1).await.unwrap(); assert_eq!( ledger.get_address().await.unwrap(), "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() @@ -310,7 +310,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_sign_tx() { - let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).await.unwrap(); + let ledger = LedgerSigner::new(DerivationType::LedgerLive(0), 1).await.unwrap(); // approve uni v2 router 0xff let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); @@ -329,7 +329,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_version() { - let ledger = LedgerEthereum::new(DerivationType::LedgerLive(0), 1).await.unwrap(); + let ledger = LedgerSigner::new(DerivationType::LedgerLive(0), 1).await.unwrap(); let version = ledger.version().await.unwrap(); assert_eq!(version, "1.3.7"); @@ -338,7 +338,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_sign_message() { - let ledger = LedgerEthereum::new(DerivationType::Legacy(0), 1).await.unwrap(); + let ledger = LedgerSigner::new(DerivationType::Legacy(0), 1).await.unwrap(); let message = "hello world"; let sig = ledger.sign_message(message).await.unwrap(); let addr = ledger.get_address().await.unwrap(); diff --git a/crates/signer-ledger/src/lib.rs b/crates/signer-ledger/src/lib.rs index a5035483e12..bc3c37b9a3f 100644 --- a/crates/signer-ledger/src/lib.rs +++ b/crates/signer-ledger/src/lib.rs @@ -19,23 +19,26 @@ #[macro_use] extern crate tracing; -mod app; -pub use app::LedgerEthereum as Ledger; - -mod types; -pub use types::{DerivationType as HDPath, LedgerError}; - use alloy_primitives::Address; use alloy_signer::{Signature, Signer}; -use app::LedgerEthereum; use async_trait::async_trait; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; +mod app; +pub use app::LedgerSigner; + +mod types; +pub use types::{DerivationType as HDPath, LedgerError}; + +#[doc(hidden)] +#[deprecated(note = "use `LedgerSigner` instead")] +pub type Ledger = LedgerSigner; + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Signer for LedgerEthereum { +impl Signer for LedgerSigner { type Error = LedgerError; async fn sign_message(&self, message: &[u8]) -> Result { diff --git a/crates/signer-trezor/src/app.rs b/crates/signer-trezor/src/app.rs index 27086d7f7f7..101b614aa13 100644 --- a/crates/signer-trezor/src/app.rs +++ b/crates/signer-trezor/src/app.rs @@ -15,7 +15,7 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// /// This is a simple wrapper around the [Trezor transport](Trezor) #[derive(Debug)] -pub struct TrezorEthereum { +pub struct TrezorSigner { derivation: DerivationType, session_id: Vec, cache_dir: PathBuf, @@ -31,7 +31,7 @@ const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; const SESSION_ID_LENGTH: usize = 32; const SESSION_FILE_NAME: &str = "trezor.session"; -impl TrezorEthereum { +impl TrezorSigner { pub async fn new( derivation: DerivationType, chain_id: u64, @@ -245,7 +245,7 @@ mod tests { async fn test_get_address() { // Instantiate it with the default trezor derivation path let trezor = - TrezorEthereum::new(DerivationType::TrezorLive(1), 1, Some(PathBuf::from("randomdir"))) + TrezorSigner::new(DerivationType::TrezorLive(1), 1, Some(PathBuf::from("randomdir"))) .await .unwrap(); assert_eq!( @@ -262,7 +262,7 @@ mod tests { #[ignore] #[cfg(TODO)] async fn test_sign_tx() { - let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); // approve uni v2 router 0xff let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); @@ -282,7 +282,7 @@ mod tests { #[ignore] #[cfg(TODO)] async fn test_sign_big_data_tx() { - let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); // invalid data let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + "aa").unwrap(); @@ -303,7 +303,7 @@ mod tests { async fn test_sign_empty_txes() { // Contract creation (empty `to`), requires data. // To test without the data field, we need to specify a `to` address. - let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); { let tx_req = Eip1559TransactionRequest::new() .to("2ed7afa17473e17ac59908f088b4371d28585476".parse::
().unwrap()) @@ -322,7 +322,7 @@ mod tests { // Contract creation (empty `to`, with data) should show on the trezor device as: // ` "0 Wei ETH // ` new contract?" - let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); { let tx_req = Eip1559TransactionRequest::new().data(data.clone()).into(); let tx = trezor.sign_transaction(&tx_req).await.unwrap(); @@ -337,7 +337,7 @@ mod tests { #[ignore] #[cfg(TODO)] async fn test_sign_eip1559_tx() { - let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); // approve uni v2 router 0xff let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); @@ -384,7 +384,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_sign_message() { - let trezor = TrezorEthereum::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); let message = "hello world"; let sig = trezor.sign_message(message).await.unwrap(); let addr = trezor.get_address().await.unwrap(); diff --git a/crates/signer-trezor/src/lib.rs b/crates/signer-trezor/src/lib.rs index 2325af56001..6aad77c3b0f 100644 --- a/crates/signer-trezor/src/lib.rs +++ b/crates/signer-trezor/src/lib.rs @@ -23,23 +23,26 @@ // TODO: Needed to pin version. use protobuf as _; -mod app; -pub use app::TrezorEthereum as Trezor; - -mod types; -pub use types::{DerivationType as TrezorHDPath, TrezorError}; - use alloy_primitives::Address; use alloy_signer::{Signature, Signer}; -use app::TrezorEthereum; use async_trait::async_trait; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; +mod app; +pub use app::TrezorSigner; + +mod types; +pub use types::{DerivationType as TrezorHDPath, TrezorError}; + +#[doc(hidden)] +#[deprecated(note = "use `TrezorSigner` instead")] +pub type Trezor = TrezorSigner; + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Signer for TrezorEthereum { +impl Signer for TrezorSigner { type Error = TrezorError; async fn sign_message(&self, message: &[u8]) -> Result { From d5e3aa44d46713f183ab9b942d5abdc2ec5c161b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:27:20 +0100 Subject: [PATCH 15/42] restructure --- crates/signer-ledger/src/lib.rs | 53 +--------------- .../signer-ledger/src/{app.rs => signer.rs} | 49 ++++++++++++++- crates/signer-trezor/src/lib.rs | 53 +--------------- .../signer-trezor/src/{app.rs => signer.rs} | 61 ++++++++++++++++--- 4 files changed, 102 insertions(+), 114 deletions(-) rename crates/signer-ledger/src/{app.rs => signer.rs} (90%) rename crates/signer-trezor/src/{app.rs => signer.rs} (91%) diff --git a/crates/signer-ledger/src/lib.rs b/crates/signer-ledger/src/lib.rs index bc3c37b9a3f..11d299ceeff 100644 --- a/crates/signer-ledger/src/lib.rs +++ b/crates/signer-ledger/src/lib.rs @@ -19,15 +19,8 @@ #[macro_use] extern crate tracing; -use alloy_primitives::Address; -use alloy_signer::{Signature, Signer}; -use async_trait::async_trait; - -#[cfg(feature = "eip712")] -use alloy_sol_types::{Eip712Domain, SolStruct}; - -mod app; -pub use app::LedgerSigner; +mod signer; +pub use signer::LedgerSigner; mod types; pub use types::{DerivationType as HDPath, LedgerError}; @@ -35,45 +28,3 @@ pub use types::{DerivationType as HDPath, LedgerError}; #[doc(hidden)] #[deprecated(note = "use `LedgerSigner` instead")] pub type Ledger = LedgerSigner; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Signer for LedgerSigner { - type Error = LedgerError; - - async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_message(message).await - } - - #[cfg(TODO)] - async fn sign_transaction(&self, message: &TypedTransaction) -> Result { - let mut tx_with_chain = message.clone(); - if tx_with_chain.chain_id().is_none() { - // in the case we don't have a chain_id, let's use the signer chain id instead - tx_with_chain.set_chain_id(self.chain_id); - } - self.sign_tx(&tx_with_chain).await - } - - #[cfg(feature = "eip712")] - async fn sign_typed_data( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result { - self.sign_typed_struct(payload, domain).await - } - - fn address(&self) -> Address { - self.address - } - - fn with_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); - self - } - - fn chain_id(&self) -> u64 { - self.chain_id - } -} diff --git a/crates/signer-ledger/src/app.rs b/crates/signer-ledger/src/signer.rs similarity index 90% rename from crates/signer-ledger/src/app.rs rename to crates/signer-ledger/src/signer.rs index f6a193db014..0e6af605409 100644 --- a/crates/signer-ledger/src/app.rs +++ b/crates/signer-ledger/src/signer.rs @@ -2,7 +2,8 @@ use crate::types::{DerivationType, LedgerError, INS, P1, P1_FIRST, P2}; use alloy_primitives::{hex, Address}; -use alloy_signer::Signature; +use alloy_signer::{Signature, Signer}; +use async_trait::async_trait; use coins_ledger::{ common::{APDUCommand, APDUData}, transports::{Ledger, LedgerAsync}, @@ -15,9 +16,9 @@ use futures_executor::block_on; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; -/// A Ledger Ethereum App. +/// A Ledger Ethereum signer. /// -/// This is a simple wrapper around the [Ledger transport](Ledger) +/// This is a simple wrapper around the [Ledger transport](Ledger). #[derive(Debug)] pub struct LedgerSigner { transport: Mutex, @@ -36,6 +37,48 @@ impl std::fmt::Display for LedgerSigner { } } +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Signer for LedgerSigner { + type Error = LedgerError; + + async fn sign_message(&self, message: &[u8]) -> Result { + self.sign_message(message).await + } + + #[cfg(TODO)] + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + let mut tx_with_chain = message.clone(); + if tx_with_chain.chain_id().is_none() { + // in the case we don't have a chain_id, let's use the signer chain id instead + tx_with_chain.set_chain_id(self.chain_id); + } + self.sign_tx(&tx_with_chain).await + } + + #[cfg(feature = "eip712")] + async fn sign_typed_data( + &self, + payload: &T, + domain: &Eip712Domain, + ) -> Result { + self.sign_typed_struct(payload, domain).await + } + + fn address(&self) -> Address { + self.address + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } + + fn chain_id(&self) -> u64 { + self.chain_id + } +} + impl LedgerSigner { /// Instantiate the application by acquiring a lock on the ledger device. /// diff --git a/crates/signer-trezor/src/lib.rs b/crates/signer-trezor/src/lib.rs index 6aad77c3b0f..a93b989aa0c 100644 --- a/crates/signer-trezor/src/lib.rs +++ b/crates/signer-trezor/src/lib.rs @@ -23,15 +23,8 @@ // TODO: Needed to pin version. use protobuf as _; -use alloy_primitives::Address; -use alloy_signer::{Signature, Signer}; -use async_trait::async_trait; - -#[cfg(feature = "eip712")] -use alloy_sol_types::{Eip712Domain, SolStruct}; - -mod app; -pub use app::TrezorSigner; +mod signer; +pub use signer::TrezorSigner; mod types; pub use types::{DerivationType as TrezorHDPath, TrezorError}; @@ -39,45 +32,3 @@ pub use types::{DerivationType as TrezorHDPath, TrezorError}; #[doc(hidden)] #[deprecated(note = "use `TrezorSigner` instead")] pub type Trezor = TrezorSigner; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl Signer for TrezorSigner { - type Error = TrezorError; - - async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_message(message).await - } - - #[cfg(TODO)] - async fn sign_transaction(&self, message: &TypedTransaction) -> Result { - let mut tx_with_chain = message.clone(); - if tx_with_chain.chain_id().is_none() { - // in the case we don't have a chain_id, let's use the signer chain id instead - tx_with_chain.set_chain_id(self.chain_id); - } - self.sign_tx(&tx_with_chain).await - } - - #[cfg(feature = "eip712")] - async fn sign_typed_data( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result { - self.sign_typed_struct(payload, domain).await - } - - fn address(&self) -> Address { - self.address - } - - fn with_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); - self - } - - fn chain_id(&self) -> u64 { - self.chain_id - } -} diff --git a/crates/signer-trezor/src/app.rs b/crates/signer-trezor/src/signer.rs similarity index 91% rename from crates/signer-trezor/src/app.rs rename to crates/signer-trezor/src/signer.rs index 101b614aa13..b08ac663251 100644 --- a/crates/signer-trezor/src/app.rs +++ b/crates/signer-trezor/src/signer.rs @@ -1,6 +1,7 @@ use super::types::{DerivationType, TrezorError}; use alloy_primitives::{Address, U256}; -use alloy_signer::Signature; +use alloy_signer::{Signature, Signer}; +use async_trait::async_trait; use std::{ env, fs, io::{Read, Write}, @@ -11,9 +12,17 @@ use trezor_client::client::Trezor; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; -/// A Trezor Ethereum App. +// we need firmware that supports EIP-1559 and EIP-712 +const FIRMWARE_1_MIN_VERSION: &str = ">=1.11.1"; +const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; + +// https://docs.trezor.io/trezor-firmware/common/communication/sessions.html +const SESSION_ID_LENGTH: usize = 32; +const SESSION_FILE_NAME: &str = "trezor.session"; + +/// A Trezor Ethereum signer. /// -/// This is a simple wrapper around the [Trezor transport](Trezor) +/// This is a simple wrapper around the [Trezor transport](Trezor). #[derive(Debug)] pub struct TrezorSigner { derivation: DerivationType, @@ -23,13 +32,47 @@ pub struct TrezorSigner { pub(crate) address: Address, } -// we need firmware that supports EIP-1559 and EIP-712 -const FIRMWARE_1_MIN_VERSION: &str = ">=1.11.1"; -const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Signer for TrezorSigner { + type Error = TrezorError; -// https://docs.trezor.io/trezor-firmware/common/communication/sessions.html -const SESSION_ID_LENGTH: usize = 32; -const SESSION_FILE_NAME: &str = "trezor.session"; + async fn sign_message(&self, message: &[u8]) -> Result { + self.sign_message(message).await + } + + #[cfg(TODO)] + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + let mut tx_with_chain = message.clone(); + if tx_with_chain.chain_id().is_none() { + // in the case we don't have a chain_id, let's use the signer chain id instead + tx_with_chain.set_chain_id(self.chain_id); + } + self.sign_tx(&tx_with_chain).await + } + + #[cfg(feature = "eip712")] + async fn sign_typed_data( + &self, + payload: &T, + domain: &Eip712Domain, + ) -> Result { + self.sign_typed_struct(payload, domain).await + } + + fn address(&self) -> Address { + self.address + } + + fn with_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = chain_id.into(); + self + } + + fn chain_id(&self) -> u64 { + self.chain_id + } +} impl TrezorSigner { pub async fn new( From 42e1824e30ecad3cccfd402bdb88155d033a9d6f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:30:53 +0100 Subject: [PATCH 16/42] add `set_chain_id`, provide a default `with_chain_id` --- crates/signer-aws/src/signer.rs | 8 +++++--- crates/signer-ledger/src/signer.rs | 12 +++++++----- crates/signer-trezor/src/signer.rs | 12 +++++++----- crates/signer/src/signer.rs | 12 ++++++++++-- crates/signer/src/wallet/mod.rs | 8 +++++--- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index 4c35312083d..fffa340f1c1 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -194,17 +194,19 @@ impl Signer for AwsSigner { Ok(sig) } + #[inline] fn address(&self) -> Address { self.address } + #[inline] fn chain_id(&self) -> u64 { self.chain_id } - fn with_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); - self + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; } } diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 0e6af605409..50c33045267 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -65,18 +65,20 @@ impl Signer for LedgerSigner { self.sign_typed_struct(payload, domain).await } + #[inline] fn address(&self) -> Address { self.address } - fn with_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); - self - } - + #[inline] fn chain_id(&self) -> u64 { self.chain_id } + + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; + } } impl LedgerSigner { diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index b08ac663251..ac128048539 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -60,18 +60,20 @@ impl Signer for TrezorSigner { self.sign_typed_struct(payload, domain).await } + #[inline] fn address(&self) -> Address { self.address } - fn with_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); - self - } - + #[inline] fn chain_id(&self) -> u64 { self.chain_id } + + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; + } } impl TrezorSigner { diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 3d875b6ac56..6bdd593073c 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -41,10 +41,18 @@ pub trait Signer: std::fmt::Debug + Send + Sync { fn chain_id(&self) -> u64; /// Sets the signer's chain ID. + fn set_chain_id(&mut self, chain_id: u64); + + /// Sets the signer's chain ID and returns `self`. + #[inline] #[must_use] - fn with_chain_id>(self, chain_id: T) -> Self + fn with_chain_id(mut self, chain_id: u64) -> Self where - Self: Sized; + Self: Sized, + { + self.set_chain_id(chain_id); + self + } } #[cfg(test)] diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 05b4cded2a7..6201dc824ea 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -79,17 +79,19 @@ impl + Send + Sync> Signer for self.sign_hash(&payload.eip712_signing_hash(domain)) } + #[inline] fn address(&self) -> Address { self.address } + #[inline] fn chain_id(&self) -> u64 { self.chain_id } - fn with_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = chain_id.into(); - self + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; } } From 01b6086a74c2b227b857264878fcf9adf0ae455a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:45:08 +0100 Subject: [PATCH 17/42] bump AWS to 1.0 --- crates/signer-aws/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/signer-aws/Cargo.toml b/crates/signer-aws/Cargo.toml index bafd0dce334..0f9dedd1ccb 100644 --- a/crates/signer-aws/Cargo.toml +++ b/crates/signer-aws/Cargo.toml @@ -16,7 +16,7 @@ alloy-primitives.workspace = true alloy-signer.workspace = true async-trait.workspace = true -aws-sdk-kms = { version = "0.39", default-features = false } +aws-sdk-kms = { version = "1.1", default-features = false } k256.workspace = true spki.workspace = true thiserror.workspace = true From 7a27dfbe9c2d0130d70e3b6df89d034f37ea5d64 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:33:57 +0100 Subject: [PATCH 18/42] better default impls for trait methods, sync feature --- crates/signer-aws/src/signer.rs | 188 +++++++++++------------ crates/signer-ledger/src/signer.rs | 129 +++++++--------- crates/signer-ledger/src/types.rs | 20 +-- crates/signer-trezor/Cargo.toml | 6 - crates/signer-trezor/src/signer.rs | 56 ++----- crates/signer-trezor/src/types.rs | 22 ++- crates/signer/Cargo.toml | 23 ++- crates/signer/README.md | 2 +- crates/signer/src/error.rs | 41 +++++ crates/signer/src/lib.rs | 7 +- crates/signer/src/signer.rs | 193 ++++++++++++++++++++++-- crates/signer/src/wallet/mod.rs | 71 +++------ crates/signer/src/wallet/private_key.rs | 37 ++--- crates/signer/src/wallet/yubi.rs | 4 +- 14 files changed, 470 insertions(+), 329 deletions(-) create mode 100644 crates/signer/src/error.rs diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index fffa340f1c1..670940a2980 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -1,5 +1,5 @@ -use alloy_primitives::{hex, utils::eip191_hash_message, Address, B256}; -use alloy_signer::{Signature as EthSig, Signature, Signer}; +use alloy_primitives::{hex, Address, B256}; +use alloy_signer::{Signature, Signer}; use aws_sdk_kms::{ error::SdkError, operation::{ @@ -10,20 +10,22 @@ use aws_sdk_kms::{ types::{MessageType, SigningAlgorithmSpec}, Client, }; -use k256::ecdsa::{self, Error as K256Error, RecoveryId, Signature as KSig, VerifyingKey}; +use k256::ecdsa::{self, RecoveryId, VerifyingKey}; use std::fmt; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; -/// An Ethers signer that uses keys held in Amazon Web Services Key Management Service (AWS KMS). +/// Amazon Web Services Key Management Service (AWS KMS) Ethereum signer. /// -/// The AWS Signer passes signing requests to the cloud service. AWS KMS keys -/// are identified by a UUID, the `key_id`. +/// The AWS Signer passes signing requests to the cloud service. AWS KMS keys are identified by a +/// UUID, the `key_id`. /// -/// Because the public key is unknown, we retrieve it on instantiation of the -/// signer. This means that the new function is `async` and must be called -/// within some runtime. +/// Because the public key is unknown, we retrieve it on instantiation of the signer. This means +/// that the new function is `async` and must be called within some runtime. +/// +/// Note that this signer only supports asynchronous operations by default. Enable the `"sync"` +/// feature to enable synchronous operations through `tokio` runtime blocking. /// /// # Examples /// @@ -74,15 +76,12 @@ pub enum AwsSignerError { #[error(transparent)] GetPublicKeyError(#[from] SdkError), #[error(transparent)] - K256(#[from] K256Error), + K256(#[from] ecdsa::Error), #[error(transparent)] Spki(#[from] spki::Error), /// Error when converting from a hex string #[error(transparent)] HexError(#[from] hex::FromHexError), - /// Error type from Eip712Error message - #[error("failed encoding eip712 struct: {0:?}")] - Eip712Error(String), #[error("{0}")] Other(String), } @@ -93,19 +92,74 @@ impl From for AwsSignerError { } } +#[async_trait::async_trait] +impl Signer for AwsSigner { + type Error = AwsSignerError; + + #[instrument(err)] + fn sign_hash(&self, hash: &B256) -> Result { + todo!() + } + + #[instrument(err)] + async fn sign_hash_async(&self, hash: &B256) -> Result { + self.sign_digest_with_eip155(hash, self.chain_id).await + } + + #[cfg(TODO)] + #[instrument(err)] + async fn sign_transaction_async( + &self, + tx: &TypedTransaction, + ) -> Result { + let mut tx_with_chain = tx.clone(); + let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + tx_with_chain.set_chain_id(chain_id); + + let sighash = tx_with_chain.sighash(); + self.sign_digest_with_eip155(sighash, chain_id).await + } + + #[cfg(feature = "eip712")] + async fn sign_typed_data_async( + &self, + payload: &T, + domain: &Eip712Domain, + ) -> Result { + let digest = payload.eip712_signing_hash(domain); + let sig = self.sign_digest(&digest).await?; + let sig = sig_from_digest_bytes_trial_recovery(sig, &digest, &self.pubkey); + Ok(sig) + } + + #[inline] + fn address(&self) -> Address { + self.address + } + + #[inline] + fn chain_id(&self) -> u64 { + self.chain_id + } + + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; + } +} + impl AwsSigner { /// Instantiate a new signer from an existing `Client` and key ID. /// /// This function retrieves the public key from AWS and calculates the /// Etheruem address. It is therefore `async`. - #[instrument(err, skip_all, fields(key_id = %key_id.as_ref()))] - pub async fn new>( + #[instrument(skip(kms), err)] + pub async fn new( kms: Client, - key_id: T, + key_id: String, chain_id: u64, ) -> Result { - let key_id = key_id.as_ref(); - let resp = request_get_pubkey(&kms, key_id).await?; + let resp = request_get_pubkey(&kms, key_id.clone()).await?; let pubkey = decode_pubkey(resp)?; let address = alloy_signer::utils::public_key_to_address(&pubkey); @@ -114,33 +168,30 @@ impl AwsSigner { hex::encode(pubkey.to_sec1_bytes()), ); - Ok(Self { kms, chain_id, key_id: key_id.into(), pubkey, address }) + Ok(Self { kms, chain_id, key_id, pubkey, address }) } /// Fetch the pubkey associated with a key ID. - pub async fn get_pubkey_for_key(&self, key_id: T) -> Result - where - T: AsRef, - { - request_get_pubkey(&self.kms, key_id.as_ref()).await.and_then(decode_pubkey) + pub async fn get_pubkey_for_key(&self, key_id: String) -> Result { + request_get_pubkey(&self.kms, key_id).await.and_then(decode_pubkey) } /// Fetch the pubkey associated with this signer's key ID. pub async fn get_pubkey(&self) -> Result { - self.get_pubkey_for_key(&self.key_id).await + self.get_pubkey_for_key(self.key_id.clone()).await } /// Sign a digest with the key associated with a key ID. - pub async fn sign_digest_with_key>( + pub async fn sign_digest_with_key( &self, - key_id: T, + key_id: String, digest: &B256, - ) -> Result { - request_sign_digest(&self.kms, key_id.as_ref(), digest).await.and_then(decode_signature) + ) -> Result { + request_sign_digest(&self.kms, key_id, digest).await.and_then(decode_signature) } /// Sign a digest with this signer's key - pub async fn sign_digest(&self, digest: &B256) -> Result { + pub async fn sign_digest(&self, digest: &B256) -> Result { self.sign_digest_with_key(self.key_id.clone(), digest).await } @@ -151,7 +202,7 @@ impl AwsSigner { &self, digest: &B256, chain_id: u64, - ) -> Result { + ) -> Result { let sig = self.sign_digest(digest).await?; let mut sig = sig_from_digest_bytes_trial_recovery(sig, digest, &self.pubkey); sig.apply_eip155(chain_id); @@ -159,69 +210,18 @@ impl AwsSigner { } } -#[async_trait::async_trait] -impl Signer for AwsSigner { - type Error = AwsSignerError; - - #[instrument(err, skip(message))] - async fn sign_message(&self, message: &[u8]) -> Result { - let message_hash = eip191_hash_message(message); - trace!(?message_hash, ?message); - - self.sign_digest_with_eip155(&message_hash, self.chain_id).await - } - - #[cfg(TODO)] - #[instrument(err)] - async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - let mut tx_with_chain = tx.clone(); - let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); - tx_with_chain.set_chain_id(chain_id); - - let sighash = tx_with_chain.sighash(); - self.sign_digest_with_eip155(sighash, chain_id).await - } - - #[cfg(feature = "eip712")] - async fn sign_typed_data( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result { - let digest = payload.eip712_signing_hash(domain); - let sig = self.sign_digest(&digest).await?; - let sig = sig_from_digest_bytes_trial_recovery(sig, &digest, &self.pubkey); - Ok(sig) - } - - #[inline] - fn address(&self) -> Address { - self.address - } - - #[inline] - fn chain_id(&self) -> u64 { - self.chain_id - } - - #[inline] - fn set_chain_id(&mut self, chain_id: u64) { - self.chain_id = chain_id; - } -} - -#[instrument(err, skip(kms))] +#[instrument(skip(kms), err)] async fn request_get_pubkey( kms: &Client, - key_id: &str, + key_id: String, ) -> Result { kms.get_public_key().key_id(key_id).send().await.map_err(Into::into) } -#[instrument(err, skip(kms, digest), fields(digest = %hex::encode(digest)))] +#[instrument(skip(kms, digest), fields(digest = %hex::encode(digest)), err)] async fn request_sign_digest( kms: &Client, - key_id: &str, + key_id: String, digest: &B256, ) -> Result { kms.sign() @@ -248,13 +248,13 @@ fn decode_pubkey(resp: GetPublicKeyOutput) -> Result Result { +fn decode_signature(resp: SignOutput) -> Result { let raw = resp .signature .as_ref() .ok_or_else(|| AwsSignerError::from("Signature not found in response".to_owned()))?; - let sig = KSig::from_der(raw.as_ref())?; + let sig = ecdsa::Signature::from_der(raw.as_ref())?; Ok(sig.normalize_s().unwrap_or(sig)) } @@ -262,15 +262,15 @@ fn decode_signature(resp: SignOutput) -> Result { fn sig_from_digest_bytes_trial_recovery( sig: ecdsa::Signature, hash: &B256, - vk: &VerifyingKey, + pubkey: &VerifyingKey, ) -> Signature { let mut signature = Signature::new(sig, RecoveryId::from_byte(0).unwrap()); - if check_candidate(&signature, hash, vk) { + if check_candidate(&signature, hash, pubkey) { return signature; } signature.set_v(1); - if check_candidate(&signature, hash, vk) { + if check_candidate(&signature, hash, pubkey) { return signature; } @@ -278,8 +278,8 @@ fn sig_from_digest_bytes_trial_recovery( } /// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. -fn check_candidate(signature: &Signature, hash: &B256, vk: &VerifyingKey) -> bool { - signature.recover_from_prehash(hash).map(|key| key == *vk).unwrap_or(false) +fn check_candidate(signature: &Signature, hash: &B256, pubkey: &VerifyingKey) -> bool { + signature.recover_from_prehash(hash).map(|key| key == *pubkey).unwrap_or(false) } #[cfg(test)] @@ -298,7 +298,7 @@ mod tests { let message = vec![0, 1, 2, 3]; - let sig = signer.sign_message(&message).await.unwrap(); + let sig = signer.sign_message_async(&message).await.unwrap(); assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); } } diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 50c33045267..a7eb6e5299e 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -9,6 +9,7 @@ use coins_ledger::{ transports::{Ledger, LedgerAsync}, }; use futures_util::lock::Mutex; +use tracing::field; // TODO: Ledger futures aren't Send. use futures_executor::block_on; @@ -19,6 +20,9 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// A Ledger Ethereum signer. /// /// This is a simple wrapper around the [Ledger transport](Ledger). +/// +/// Note that this signer only supports asynchronous operations by default. Enable the `"sync"` +/// feature to enable synchronous operations through `tokio` runtime blocking. #[derive(Debug)] pub struct LedgerSigner { transport: Mutex, @@ -42,12 +46,22 @@ impl std::fmt::Display for LedgerSigner { impl Signer for LedgerSigner { type Error = LedgerError; - async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_message(message).await + #[inline] + async fn sign_message_async(&self, message: &[u8]) -> Result { + let message = message.as_ref(); + + let mut payload = Self::path_to_bytes(&self.derivation); + payload.extend_from_slice(&(message.len() as u32).to_be_bytes()); + payload.extend_from_slice(message); + + self.sign_payload(INS::SIGN_PERSONAL_MESSAGE, &payload).await } #[cfg(TODO)] - async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + async fn sign_transaction_async( + &self, + message: &TypedTransaction, + ) -> Result { let mut tx_with_chain = message.clone(); if tx_with_chain.chain_id().is_none() { // in the case we don't have a chain_id, let's use the signer chain id instead @@ -57,12 +71,28 @@ impl Signer for LedgerSigner { } #[cfg(feature = "eip712")] - async fn sign_typed_data( + #[inline] + async fn sign_typed_data_async( &self, payload: &T, domain: &Eip712Domain, ) -> Result { - self.sign_typed_struct(payload, domain).await + // See comment for v1.6.0 requirement + // https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999 + const EIP712_MIN_VERSION: &str = ">=1.6.0"; + let req = semver::VersionReq::parse(EIP712_MIN_VERSION).unwrap(); + let version = semver::Version::parse(&self.version().await?)?; + + // Enforce app version is greater than EIP712_MIN_VERSION + if !req.matches(&version) { + return Err(LedgerError::UnsupportedAppVersion(EIP712_MIN_VERSION)); + } + + let mut data = Self::path_to_bytes(&self.derivation); + data.extend_from_slice(domain.separator().as_slice()); + data.extend_from_slice(payload.eip712_hash_struct().as_slice()); + + self.sign_payload(INS::SIGN_ETH_EIP_712, &data).await } #[inline] @@ -163,11 +193,11 @@ impl LedgerSigner { debug!("Dispatching get_version"); let answer = block_on(transport.exchange(&command))?; - let result = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?; - if result.len() < 4 { - return Err(LedgerError::ShortResponse { got: result.len(), at_least: 4 }); + let data = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?; + if data.len() != 4 { + return Err(LedgerError::ShortResponse { got: data.len(), expected: 4 }); } - let version = format!("{}.{}.{}", result[1], result[2], result[3]); + let version = format!("{}.{}.{}", data[1], data[2], data[3]); debug!(version, "Retrieved version from device"); Ok(version) } @@ -210,53 +240,10 @@ impl LedgerSigner { Ok(signature) } - /// Signs an ethereum personal message - pub async fn sign_message>(&self, message: T) -> Result { - let message = message.as_ref(); - - let mut payload = Self::path_to_bytes(&self.derivation); - payload.extend_from_slice(&(message.len() as u32).to_be_bytes()); - payload.extend_from_slice(message); - - self.sign_payload(INS::SIGN_PERSONAL_MESSAGE, &payload).await - } - - /// Signs an EIP712 encoded domain separator and message - #[cfg(feature = "eip712")] - pub async fn sign_typed_struct( - &self, - strukt: &T, - domain: &Eip712Domain, - ) -> Result { - // See comment for v1.6.0 requirement - // https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999 - const EIP712_MIN_VERSION: &str = ">=1.6.0"; - let req = semver::VersionReq::parse(EIP712_MIN_VERSION).unwrap(); - let version = semver::Version::parse(&self.version().await?)?; - - // Enforce app version is greater than EIP712_MIN_VERSION - if !req.matches(&version) { - return Err(LedgerError::UnsupportedAppVersion(EIP712_MIN_VERSION)); - } - - let mut payload = Self::path_to_bytes(&self.derivation); - payload.extend_from_slice(domain.separator().as_slice()); - payload.extend_from_slice(strukt.eip712_hash_struct().as_slice()); - - self.sign_payload(INS::SIGN_ETH_EIP_712, &payload).await - } - /// Helper function for signing either transaction data, personal messages or EIP712 derived /// structs. #[instrument(err, skip_all, fields(command = %command, payload = hex::encode(payload)))] - pub async fn sign_payload( - &self, - command: INS, - payload: &[u8], - ) -> Result { - if payload.is_empty() { - return Err(LedgerError::EmptyPayload); - } + async fn sign_payload(&self, command: INS, payload: &[u8]) -> Result { let transport = self.transport.lock().await; let mut command = APDUCommand { ins: command as u8, @@ -273,37 +260,37 @@ impl LedgerSigner { (0..=255).rev().find(|i| payload.len() % i != 3).expect("true for any length"); // Iterate in 255 byte chunks - let span = debug_span!("send_loop", index = 0, chunk = ""); - let guard = span.entered(); + let span = debug_span!("send_loop", index = field::Empty, chunk = field::Empty).entered(); for (index, chunk) in payload.chunks(chunk_size).enumerate() { - guard.record("index", index); - guard.record("chunk", hex::encode(chunk)); + if !span.is_disabled() { + span.record("index", index); + span.record("chunk", hex::encode(chunk)); + } command.data = APDUData::new(chunk); debug!("Dispatching packet to device"); - answer = Some(block_on(transport.exchange(&command))?); - let data = answer.as_ref().expect("just assigned").data(); - if data.is_none() { + let ans = block_on(transport.exchange(&command))?; + let Some(data) = ans.data() else { return Err(LedgerError::UnexpectedNullResponse); - } - debug!( - response = hex::encode(data.expect("just checked")), - "Received response from device" - ); + }; + debug!(response = hex::encode(data), "Received response from device"); + answer = Some(ans); // We need more data command.p1 = P1::MORE as u8; } - drop(guard); - let answer = answer.expect("payload is non-empty, therefore loop ran"); - let result = answer.data().expect("check in loop"); - if result.len() < 65 { - return Err(LedgerError::ShortResponse { got: result.len(), at_least: 65 }); + drop(span); + drop(transport); + + let answer = answer.unwrap(); + let data = answer.data().unwrap(); + if data.len() != 65 { + return Err(LedgerError::ShortResponse { got: data.len(), expected: 65 }); } // TODO: don't unwrap - let sig = Signature::from_bytes(&result[1..], result[0] as u64).unwrap(); + let sig = Signature::from_bytes(&data[1..], data[0] as u64).unwrap(); debug!(?sig, "Received signature from device"); Ok(sig) } diff --git a/crates/signer-ledger/src/types.rs b/crates/signer-ledger/src/types.rs index becbdbfc59f..a76ef7a3e49 100644 --- a/crates/signer-ledger/src/types.rs +++ b/crates/signer-ledger/src/types.rs @@ -32,30 +32,24 @@ impl fmt::Display for DerivationType { /// Error when using the Ledger transport. #[derive(Error, Debug)] pub enum LedgerError { - /// Underlying ledger transport error + /// Underlying Ledger transport error. #[error(transparent)] LedgerError(#[from] coins_ledger::errors::LedgerError), /// Device response was unexpectedly none #[error("Received unexpected response from device. Expected data in response, found none.")] UnexpectedNullResponse, #[error(transparent)] - /// Error when converting from a hex string + /// [`hex`] error. HexError(#[from] hex::FromHexError), #[error(transparent)] - /// Error when converting a semver requirement + /// [`semver`] error. SemVerError(#[from] semver::Error), - /// Error type from Eip712Error message - #[error("error encoding eip712 struct: {0:?}")] - Eip712Error(String), - /// Error when signing EIP712 struct with not compatible Ledger ETH app - #[error("Ledger ethereum app requires at least version {0}")] + /// Thrown when trying to sign using EIP-712 with an incompatible Ledger Ethereum app. + #[error("Ledger Ethereum app requires at least version {0}")] UnsupportedAppVersion(&'static str), /// Got a response, but it didn't contain as much data as expected - #[error("Cannot deserialize ledger response, insufficient bytes. Got {got} expected at least {at_least}")] - ShortResponse { got: usize, at_least: usize }, - /// Payload is empty - #[error("Payload must not be empty")] - EmptyPayload, + #[error("bad response; got {got} bytes, expected {expected}")] + ShortResponse { got: usize, expected: usize }, } pub(crate) const P1_FIRST: u8 = 0x00; diff --git a/crates/signer-trezor/Cargo.toml b/crates/signer-trezor/Cargo.toml index 3a660c3238d..d9611cc25a7 100644 --- a/crates/signer-trezor/Cargo.toml +++ b/crates/signer-trezor/Cargo.toml @@ -25,11 +25,5 @@ semver.workspace = true thiserror.workspace = true # tracing.workspace = true -# eip712 -alloy-sol-types = { workspace = true, optional = true } - [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } - -[features] -eip712 = ["alloy-signer/eip712", "dep:alloy-sol-types"] diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index ac128048539..a680968cbb2 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -9,9 +9,6 @@ use std::{ }; use trezor_client::client::Trezor; -#[cfg(feature = "eip712")] -use alloy_sol_types::{Eip712Domain, SolStruct}; - // we need firmware that supports EIP-1559 and EIP-712 const FIRMWARE_1_MIN_VERSION: &str = ">=1.11.1"; const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; @@ -23,6 +20,9 @@ const SESSION_FILE_NAME: &str = "trezor.session"; /// A Trezor Ethereum signer. /// /// This is a simple wrapper around the [Trezor transport](Trezor). +/// +/// Note that this signer only supports asynchronous operations by default. Enable the `"sync"` +/// feature to enable synchronous operations through `tokio` runtime blocking. #[derive(Debug)] pub struct TrezorSigner { derivation: DerivationType, @@ -37,8 +37,16 @@ pub struct TrezorSigner { impl Signer for TrezorSigner { type Error = TrezorError; - async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_message(message).await + async fn sign_message_async(&self, message: &[u8]) -> Result { + let mut client = self.get_client(self.session_id.clone())?; + let apath = Self::convert_path(&self.derivation); + + let signature = client.ethereum_sign_message(message.into(), apath)?; + + let r = U256::from_limbs(signature.r.0); + let s = U256::from_limbs(signature.s.0); + // TODO: don't unwrap + Ok(Signature::from_scalars(r.into(), s.into(), signature.v).unwrap()) } #[cfg(TODO)] @@ -51,15 +59,6 @@ impl Signer for TrezorSigner { self.sign_tx(&tx_with_chain).await } - #[cfg(feature = "eip712")] - async fn sign_typed_data( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result { - self.sign_typed_struct(payload, domain).await - } - #[inline] fn address(&self) -> Address { self.address @@ -233,33 +232,6 @@ impl TrezorSigner { Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) } - /// Signs an ethereum personal message - pub async fn sign_message + Send + Sync>( - &self, - message: S, - ) -> Result { - let message = message.as_ref(); - let mut client = self.get_client(self.session_id.clone())?; - let apath = Self::convert_path(&self.derivation); - - let signature = client.ethereum_sign_message(message.into(), apath)?; - - let r = U256::from_limbs(signature.r.0); - let s = U256::from_limbs(signature.s.0); - // TODO: don't unwrap - Ok(Signature::from_scalars(r.into(), s.into(), signature.v).unwrap()) - } - - /// Signs an EIP712 encoded domain separator and message - #[cfg(feature = "eip712")] - pub async fn sign_typed_struct( - &self, - _payload: &T, - _domain: &Eip712Domain, - ) -> Result { - unimplemented!() - } - // helper which converts a derivation path to [u32] fn convert_path(derivation: &DerivationType) -> Vec { let derivation = derivation.to_string(); @@ -431,7 +403,7 @@ mod tests { async fn test_sign_message() { let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); let message = "hello world"; - let sig = trezor.sign_message(message).await.unwrap(); + let sig = trezor.sign_message_async(message.as_bytes()).await.unwrap(); let addr = trezor.get_address().await.unwrap(); assert_eq!(sig.recover_address_from_msg(message).unwrap(), addr); } diff --git a/crates/signer-trezor/src/types.rs b/crates/signer-trezor/src/types.rs index b18d1892a9c..b390532e4c1 100644 --- a/crates/signer-trezor/src/types.rs +++ b/crates/signer-trezor/src/types.rs @@ -1,6 +1,6 @@ //! Helpers for interacting with the Ethereum Trezor App. //! -//! [Official Docs](https://github.com/TrezorHQ/app-ethereum/blob/master/doc/ethapp.asc) +//! [Official Docs](https://docs.trezor.io/trezor-firmware/index.html) #![allow(clippy::upper_case_acronyms)] @@ -9,26 +9,24 @@ use std::fmt; use thiserror::Error; use trezor_client::client::AccessListItem as Trezor_AccessListItem; +/// Trezor wallet type. #[derive(Clone, Debug)] -/// Trezor wallet type pub enum DerivationType { /// Trezor Live-generated HD path TrezorLive(usize), - /// Any other path. Attention! Trezor by default forbids custom derivation paths - /// Run trezorctl set safety-checks prompt, to allow it + /// Any other path. + /// + /// **Warning**: Trezor by default forbids custom derivation paths; + /// run `trezorctl set safety-checks prompt` to enable them. Other(String), } impl fmt::Display for DerivationType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!( - f, - "{}", - match self { - DerivationType::TrezorLive(index) => format!("m/44'/60'/{index}'/0/0"), - DerivationType::Other(inner) => inner.to_owned(), - } - ) + match self { + DerivationType::TrezorLive(index) => write!(f, "m/44'/60'/{index}'/0/0"), + DerivationType::Other(inner) => f.write_str(inner), + } } } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index a5c1f92302e..20ea4b207ff 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -16,8 +16,6 @@ alloy-primitives.workspace = true # TODO # alloy-rpc-types.workspace = true -alloy-sol-types = { workspace = true, optional = true } - coins-bip32 = "0.8.7" coins-bip39 = "0.8.7" elliptic-curve.workspace = true @@ -26,8 +24,14 @@ rand.workspace = true thiserror.workspace = true async-trait.workspace = true -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -eth-keystore = "0.5.0" +# eip712 +alloy-sol-types = { workspace = true, optional = true } + +# sync +tokio = { workspace = true, features = ["macros", "rt"], optional = true } + +# keystore +eth-keystore = { version = "0.5.0", default-features = false, optional = true } # yubi yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional = true } @@ -36,11 +40,14 @@ yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional serde_json.workspace = true tempfile.workspace = true tracing-subscriber.workspace = true - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -yubihsm = { version = "0.42", features = ["secp256k1", "usb", "mockhsm"] } + +# need mockhsm feature for tests +yubihsm = { version = "0.42", features = ["mockhsm"] } [features] -yubihsm = ["dep:yubihsm"] eip712 = ["dep:alloy-sol-types"] +sync = ["dep:tokio"] + +keystore = ["dep:eth-keystore"] +yubihsm = ["dep:yubihsm"] diff --git a/crates/signer/README.md b/crates/signer/README.md index 8313e3c7b33..a3f0b188b2d 100644 --- a/crates/signer/README.md +++ b/crates/signer/README.md @@ -48,7 +48,7 @@ let message = "Some data"; let wallet = LocalWallet::random(); // Sign the message -let signature = wallet.sign_message(message)?; +let signature = wallet.sign_message(message.as_bytes())?; // Recover the signer from the message let recovered = signature.recover_address_from_msg(message)?; diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs new file mode 100644 index 00000000000..4a399863b91 --- /dev/null +++ b/crates/signer/src/error.rs @@ -0,0 +1,41 @@ +use alloy_primitives::hex; +use k256::ecdsa; +use thiserror::Error; + +/// Result type alias for [`SignerError`]. +pub type SignerResult = std::result::Result; + +/// Generic error type for [`Signer`](crate::Signer) implementations. +#[derive(Debug, Error)] +pub enum SignerError { + /// This operation is not supported by the signer. + #[error("signer operation {0} not supported")] + UnsupportedOperation, // TODO: enum UnsupportedOperation ? + /// Mismatch between provided transaction chain ID and signer chain ID. + #[error("")] + TransactionChainIdMismatch(u64, u64), + /// [`ecdsa`] error. + #[error(transparent)] + Ecdsa(#[from] ecdsa::Error), + /// [`hex`] error. + #[error(transparent)] + HexError(#[from] hex::FromHexError), + /// Generic error. + #[error(transparent)] + Other(#[from] Box), +} + +impl SignerError { + pub fn new(error: impl Into>) -> Self { + Self::Other(error.into()) + } +} + +// impl From for SignerError +// where +// Box: From, +// { +// fn from(value: T) -> Self { +// Self::Other(Box::from(value)) +// } +// } diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 4963c034493..fee1e58d4d6 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -15,6 +15,9 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +// mod error; +// pub use error::{SignerError, SignerResult}; + mod signature; pub use signature::Signature; @@ -26,7 +29,7 @@ pub use wallet::{MnemonicBuilder, Wallet, WalletError}; pub mod utils; -#[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] +#[cfg(feature = "yubihsm")] pub use yubihsm; /// Re-export the BIP-32 crate so that wordlists can be accessed conveniently. @@ -36,5 +39,5 @@ pub use coins_bip39; pub type LocalWallet = Wallet; /// A wallet instantiated with a YubiHSM -#[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] +#[cfg(feature = "yubihsm")] pub type YubiWallet = Wallet>; diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 6bdd593073c..246268d0afb 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -1,38 +1,145 @@ use crate::Signature; -use alloy_primitives::Address; +use alloy_primitives::{eip191_hash_message, Address, B256}; use async_trait::async_trait; -use std::error::Error; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; -/// Trait for signing transactions and messages. +#[cfg(feature = "sync")] +macro_rules! try_block_on { + ($future:expr, $default:expr $(,)?) => { + match tokio::runtime::Handle::try_current() { + Ok(handle) => handle.block_on($future), + Err(_) => $default, + } + }; +} + +#[cfg(not(feature = "sync"))] +macro_rules! try_block_on { + ($future:expr, $default:expr $(,)?) => { + $default + }; +} + +/// Ethereum signer. /// -/// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. +/// All signing methods rely on [`sign_hash`](Signer::sign_hash). If the signer is not able to +/// implement this method, then all other methods must be implemented directly, or they will return +/// an "unimplemented" error. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait Signer: std::fmt::Debug + Send + Sync { +pub trait Signer: Send + Sync { /// The error type returned by the signer. - type Error: Error + Send + Sync; + type Error: Send + Sync; - /// Signs the hash of the provided message after prefixing it. - async fn sign_message(&self, message: &[u8]) -> Result; + /// Signs the hash. + #[inline] + fn sign_hash(&self, _hash: &B256) -> Result { + // TODO: error not panic + try_block_on!(self.sign_hash_async(_hash), unimplemented!()) + } + + /// Signs the hash. + /// + /// Asynchronous version of [`sign_hash`](Signer::sign_hash). + #[inline] + async fn sign_hash_async(&self, hash: &B256) -> Result { + if cfg!(feature = "sync") { + // TODO: error not panic + unimplemented!() + } else { + self.sign_hash(hash) + } + } + + /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. + /// + /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 + #[inline] + fn sign_message(&self, message: &[u8]) -> Result { + try_block_on!( + self.sign_message_async(message), + self.sign_hash(&eip191_hash_message(message)), + ) + } + + /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. + /// + /// Asynchronous version of [`sign_message`](Signer::sign_message). + /// + /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 + #[inline] + async fn sign_message_async(&self, message: &[u8]) -> Result { + if cfg!(feature = "sync") { + self.sign_hash_async(&eip191_hash_message(message)).await + } else { + self.sign_message(message) + } + } + + /// Signs the transaction. + #[cfg(TODO)] + fn sign_transaction(&self, message: &TypedTransaction) -> Result { + try_block_on!(self.sign_transaction_async(message), self.sign_hash(&message.sighash())) + } /// Signs the transaction. + /// + /// Asynchronous version of [`sign_transaction`](Signer::sign_transaction). #[cfg(TODO)] - async fn sign_transaction(&self, message: &TypedTransaction) -> Result; + #[inline] + async fn sign_transaction_async( + &self, + message: &TypedTransaction, + ) -> Result { + if cfg!(feature = "sync") { + self.sign_hash_async(&message.sighash()).await + } else { + self.sign_transaction(message) + } + } /// Encodes and signs the typed data according to [EIP-712]. /// /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 #[cfg(feature = "eip712")] - async fn sign_typed_data( + #[inline] + fn sign_typed_data( &self, payload: &T, domain: &Eip712Domain, ) -> Result where - Self: Sized; + Self: Sized, + { + try_block_on!( + self.sign_typed_data_async(payload, domain), + self.sign_hash(&payload.eip712_signing_hash(domain)), + ) + } + + /// Encodes and signs the typed data according to [EIP-712]. + /// + /// Asynchronous version of [`sign_typed_data`](Signer::sign_typed_data). + /// + /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 + #[cfg(feature = "eip712")] + #[inline] + async fn sign_typed_data_async( + &self, + payload: &T, + domain: &Eip712Domain, + ) -> Result + where + Self: Sized, + { + if cfg!(feature = "sync") { + self.sign_hash_async(&payload.eip712_signing_hash(domain)).await + } else { + self.sign_typed_data(payload, domain) + } + } /// Returns the signer's Ethereum Address. fn address(&self) -> Address; @@ -56,4 +163,66 @@ pub trait Signer: std::fmt::Debug + Send + Sync { } #[cfg(test)] -struct _ObjectSafe(dyn Signer); +mod tests { + use super::*; + + #[cfg(feature = "eip712")] + alloy_sol_types::sol! { + #[derive(Default)] + struct Eip712Data { + uint64 a; + } + } + + struct _ObjectSafe(Box>); + + async fn test_unimplemented_signer(s: &S) { + test_unsized_unimplemented_signer(s).await; + + #[cfg(feature = "eip712")] + { + assert!(s.sign_typed_data(&Eip712Data::default(), &Eip712Domain::default()).is_err()); + assert!(s + .sign_typed_data_async(&Eip712Data::default(), &Eip712Domain::default()) + .await + .is_err()); + } + } + + async fn test_unsized_unimplemented_signer(s: &S) { + assert!(s.sign_hash(&B256::ZERO).is_err()); + assert!(s.sign_hash_async(&B256::ZERO).await.is_err()); + + assert!(s.sign_message(&[]).is_err()); + assert!(s.sign_message_async(&[]).await.is_err()); + + #[cfg(TODO)] + assert!(s.sign_transaction(&TypedTransaction::default()).is_err()); + #[cfg(TODO)] + assert!(s.sign_transaction_async(&TypedTransaction::default()).await.is_err()); + } + + #[tokio::test] + async fn unimplemented() { + struct UnimplementedSigner; + + impl Signer for UnimplementedSigner { + type Error = (); + + fn address(&self) -> Address { + unimplemented!() + } + + fn chain_id(&self) -> u64 { + unimplemented!() + } + + fn set_chain_id(&mut self, _chain_id: u64) { + unimplemented!() + } + } + + test_unimplemented_signer(&UnimplementedSigner).await; + test_unsized_unimplemented_signer(&UnimplementedSigner as &dyn Signer).await; + } +} diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 6201dc824ea..753298536bc 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -1,12 +1,9 @@ use crate::{Signature, Signer}; -use alloy_primitives::{utils::eip191_hash_message, Address, B256}; +use alloy_primitives::{Address, B256}; use async_trait::async_trait; use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}; use std::fmt; -#[cfg(feature = "eip712")] -use alloy_sol_types::{Eip712Domain, SolStruct}; - mod mnemonic; pub use mnemonic::MnemonicBuilder; @@ -23,7 +20,8 @@ mod yubi; /// ## Signing and Verifying a message /// /// The wallet can be used to produce ECDSA [`Signature`] objects, which can be -/// then verified. Note that this uses [`eip191_hash_message`] under the hood which will +/// then verified. Note that this uses +/// [`eip191_hash_message`](alloy_primitives::eip191_hash_message) under the hood which will /// prefix the message being hashed with the `Ethereum Signed Message` domain separator. /// /// ``` @@ -47,7 +45,7 @@ mod yubi; /// # Ok::<_, Box>(()) /// ``` #[derive(Clone)] -pub struct Wallet> { +pub struct Wallet { /// The wallet's private key. pub(crate) signer: D, /// The wallet's address. @@ -61,22 +59,24 @@ pub struct Wallet> { impl + Send + Sync> Signer for Wallet { type Error = WalletError; - async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_message(message) + #[inline] + fn sign_hash(&self, hash: &B256) -> Result { + let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; + Ok(Signature::new(recoverable_sig, recovery_id)) } #[cfg(TODO)] - async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - self.sign_transaction_sync(tx) - } + #[inline] + fn sign_transaction(&self, tx: &TypedTransaction) -> Result { + // rlp (for sighash) must have the same chain id as v in the signature + let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + let mut tx = tx.clone(); + tx.set_chain_id(chain_id); - #[cfg(feature = "eip712")] - async fn sign_typed_data( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result { - self.sign_hash(&payload.eip712_signing_hash(domain)) + let sighash = tx.sighash(); + let mut sig = self.sign_hash(&sighash)?; + sig.apply_eip155(chain_id); + Ok(sig) } #[inline] @@ -96,41 +96,14 @@ impl + Send + Sync> Signer for } impl + Send + Sync> Wallet { - /// Construct a new wallet with an external Signer + /// Construct a new wallet with an external [`PrehashSigner`]. + #[inline] pub const fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { Wallet { signer, address, chain_id } } - /// Synchronously signs the provided transaction, normalizing the signature `v` value with - /// EIP-155 using the transaction's `chain_id`, or the signer's `chain_id` if the transaction - /// does not specify one. - #[cfg(TODO)] - pub fn sign_transaction_sync(&self, tx: &TypedTransaction) -> Result { - // rlp (for sighash) must have the same chain id as v in the signature - let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); - let mut tx = tx.clone(); - tx.set_chain_id(chain_id); - - let sighash = tx.sighash(); - let mut sig = self.sign_hash(sighash)?; - sig.set_v(to_eip155_v(sig.recid().to_byte(), chain_id)); - Ok(sig) - } - - /// Signs the provided message after prefixing it and hashing it according to [EIP-191]. - /// - /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 - pub fn sign_message>(&self, msg: T) -> Result { - self.sign_hash(&eip191_hash_message(msg)) - } - - /// Signs the provided hash. - pub fn sign_hash(&self, hash: &B256) -> Result { - let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; - Ok(Signature::new(recoverable_sig, recovery_id)) - } - /// Returns this wallet's signer. + #[inline] pub const fn signer(&self) -> &D { &self.signer } @@ -141,7 +114,7 @@ impl> fmt::Debug for Wallet fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Wallet") .field("address", &self.address) - .field("chain_Id", &self.chain_id) + .field("chain_id", &self.chain_id) .finish() } } diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index 8d6dd71d81d..b4b5feba4d3 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -13,12 +13,8 @@ use rand::{CryptoRng, Rng}; use std::str::FromStr; use thiserror::Error; -#[cfg(not(target_arch = "wasm32"))] -use elliptic_curve::rand_core; -#[cfg(not(target_arch = "wasm32"))] -use eth_keystore::KeystoreError; -#[cfg(not(target_arch = "wasm32"))] -use std::path::Path; +#[cfg(feature = "keystore")] +use {elliptic_curve::rand_core, eth_keystore::KeystoreError, std::path::Path}; /// Error thrown by the Wallet module #[derive(Debug, Error)] @@ -30,7 +26,7 @@ pub enum WalletError { #[error(transparent)] Bip39Error(#[from] MnemonicError), /// Underlying eth keystore error - #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "keystore")] #[error(transparent)] EthKeystoreError(#[from] KeystoreError), /// Error propagated from k256's ECDSA module @@ -72,6 +68,15 @@ impl Wallet { Self::_new(SigningKey::random(rng)) } + #[inline] + fn _new(signer: SigningKey) -> Self { + let address = secret_key_to_address(&signer); + Self { signer, address, chain_id: 1 } + } +} + +#[cfg(feature = "keystore")] +impl Wallet { /// Creates a new random encrypted JSON with the provided password and stores it in the /// provided directory. Returns a tuple (Wallet, String) of the wallet instance for the /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`, @@ -128,12 +133,6 @@ impl Wallet { let uuid = eth_keystore::encrypt_key(keypath, rng, pk, password, name)?; Ok((Self::from_slice(pk)?, uuid)) } - - #[inline] - fn _new(signer: SigningKey) -> Self { - let address = secret_key_to_address(&signer); - Self { signer, address, chain_id: 1 } - } } impl PartialEq for Wallet { @@ -185,8 +184,9 @@ impl TryFrom for Wallet { #[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; - use crate::LocalWallet; + use crate::{LocalWallet, Signer}; use alloy_primitives::address; + use std::path::Path; use tempfile::tempdir; #[test] @@ -206,6 +206,7 @@ mod tests { } } + #[cfg(feature = "keystore")] fn test_encrypted_json_keystore(key: Wallet, uuid: &str, dir: &Path) { // sign a message using the given key let message = "Some data"; @@ -223,6 +224,7 @@ mod tests { } #[test] + #[cfg(feature = "keystore")] fn encrypted_json_keystore_new() { // create and store an encrypted JSON keystore in this directory let dir = tempdir().unwrap(); @@ -234,6 +236,7 @@ mod tests { } #[test] + #[cfg(feature = "keystore")] fn encrypted_json_keystore_from_pk() { // create and store an encrypted JSON keystore in this directory let dir = tempdir().unwrap(); @@ -363,9 +366,9 @@ mod tests { sig.verify(sighash, wallet.address).unwrap(); } - #[tokio::test] + #[test] #[cfg(feature = "eip712")] - async fn typed_data() { + fn typed_data() { use crate::Signer; use alloy_primitives::{keccak256, Address, I256, U256}; use alloy_sol_types::{eip712_domain, sol, SolStruct}; @@ -399,7 +402,7 @@ mod tests { }; let wallet = Wallet::random(); let hash = foo_bar.eip712_signing_hash(&domain); - let sig = wallet.sign_typed_data(&foo_bar, &domain).await.unwrap(); + let sig = wallet.sign_typed_data(&foo_bar, &domain).unwrap(); assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), wallet.address()); assert_eq!(wallet.sign_hash(&hash).unwrap(), sig); } diff --git a/crates/signer/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs index 849b0c66fc8..3e19243a408 100644 --- a/crates/signer/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -85,7 +85,7 @@ mod tests { ); let msg = "Some data"; - let sig = wallet.sign_message(msg).unwrap(); + let sig = wallet.sign_message(msg.as_bytes()).unwrap(); assert_eq!(sig.recover_address_from_msg(msg).unwrap(), wallet.address()); assert_eq!(wallet.address(), address!("2DE2C386082Cff9b28D62E60983856CE1139eC49")); } @@ -102,7 +102,7 @@ mod tests { ); let msg = "Some data"; - let sig = wallet.sign_message(msg).unwrap(); + let sig = wallet.sign_message(msg.as_bytes()).unwrap(); assert_eq!(sig.recover_address_from_msg(msg).unwrap(), wallet.address()); } } From e6b16b5a15b88e6597239f23c5211744a7a141b7 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 10:36:31 +0100 Subject: [PATCH 19/42] remove Error GAT --- Cargo.toml | 1 + crates/signer-aws/Cargo.toml | 5 +- crates/signer-aws/src/lib.rs | 2 +- crates/signer-aws/src/signer.rs | 90 ++++-------- crates/signer-ledger/src/lib.rs | 3 +- crates/signer-ledger/src/signer.rs | 69 ++++----- crates/signer-ledger/src/types.rs | 9 +- crates/signer-trezor/Cargo.toml | 3 +- crates/signer-trezor/src/lib.rs | 7 +- crates/signer-trezor/src/signer.rs | 162 +++++++-------------- crates/signer-trezor/src/types.rs | 38 ++--- crates/signer/Cargo.toml | 15 +- crates/signer/src/error.rs | 80 ++++++++--- crates/signer/src/lib.rs | 10 +- crates/signer/src/signature.rs | 12 +- crates/signer/src/signer.rs | 184 ++++++++++-------------- crates/signer/src/wallet/mnemonic.rs | 12 +- crates/signer/src/wallet/mod.rs | 24 +--- crates/signer/src/wallet/private_key.rs | 83 ++++------- 19 files changed, 345 insertions(+), 464 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 347f1cc531d..fead3d2e1a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ tracing-subscriber = "0.3.18" tempfile = "3.8" +assert_matches = "1.5" base64 = "0.21" bimap = "0.6" itertools = "0.12" diff --git a/crates/signer-aws/Cargo.toml b/crates/signer-aws/Cargo.toml index 0f9dedd1ccb..70a654943b0 100644 --- a/crates/signer-aws/Cargo.toml +++ b/crates/signer-aws/Cargo.toml @@ -22,12 +22,9 @@ spki.workspace = true thiserror.workspace = true tracing.workspace = true -# eip712 -alloy-sol-types = { workspace = true, optional = true } - [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } aws-config = { version = "1.0", default-features = false } [features] -eip712 = ["alloy-signer/eip712", "dep:alloy-sol-types"] +eip712 = ["alloy-signer/eip712"] diff --git a/crates/signer-aws/src/lib.rs b/crates/signer-aws/src/lib.rs index 5669b96d1a5..443d9a2f713 100644 --- a/crates/signer-aws/src/lib.rs +++ b/crates/signer-aws/src/lib.rs @@ -6,7 +6,7 @@ #![warn( missing_copy_implementations, missing_debug_implementations, - // missing_docs, + missing_docs, unreachable_pub, clippy::missing_const_for_fn, rustdoc::all diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index 670940a2980..d1fff52e464 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -1,5 +1,5 @@ use alloy_primitives::{hex, Address, B256}; -use alloy_signer::{Signature, Signer}; +use alloy_signer::{Result, Signature, Signer}; use aws_sdk_kms::{ error::SdkError, operation::{ @@ -13,9 +13,6 @@ use aws_sdk_kms::{ use k256::ecdsa::{self, RecoveryId, VerifyingKey}; use std::fmt; -#[cfg(feature = "eip712")] -use alloy_sol_types::{Eip712Domain, SolStruct}; - /// Amazon Web Services Key Management Service (AWS KMS) Ethereum signer. /// /// The AWS Signer passes signing requests to the cloud service. AWS KMS keys are identified by a @@ -24,8 +21,8 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// Because the public key is unknown, we retrieve it on instantiation of the signer. This means /// that the new function is `async` and must be called within some runtime. /// -/// Note that this signer only supports asynchronous operations by default. Enable the `"sync"` -/// feature to enable synchronous operations through `tokio` runtime blocking. +/// Note that this signer only supports asynchronous operations. Calling a non-asynchronous method +/// will always return an error. /// /// # Examples /// @@ -38,13 +35,13 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// let config = aws_config::load_defaults(BehaviorVersion::latest()).await; /// let client = aws_sdk_kms::Client::new(&config); /// -/// let key_id = "..."; +/// let key_id = "...".to_string(); /// let chain_id = 1; /// let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); /// /// let message = vec![0, 1, 2, 3]; /// -/// let sig = signer.sign_message(&message).await.unwrap(); +/// let sig = signer.sign_message_async(&message).await.unwrap(); /// assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); /// # } /// ``` @@ -71,47 +68,39 @@ impl fmt::Debug for AwsSigner { /// Errors thrown by [`AwsSigner`]. #[derive(thiserror::Error, Debug)] pub enum AwsSignerError { + /// Thrown when the AWS KMS API returns a signing error. #[error(transparent)] - SignError(#[from] SdkError), + Sign(#[from] SdkError), + /// Thrown when the AWS KMS API returns an error. #[error(transparent)] - GetPublicKeyError(#[from] SdkError), + GetPublicKey(#[from] SdkError), + /// [`ecdsa`] error. #[error(transparent)] K256(#[from] ecdsa::Error), + /// [`spki`] error. #[error(transparent)] Spki(#[from] spki::Error), - /// Error when converting from a hex string + /// [`hex`] error. #[error(transparent)] - HexError(#[from] hex::FromHexError), - #[error("{0}")] - Other(String), -} - -impl From for AwsSignerError { - fn from(value: String) -> Self { - Self::Other(value) - } + Hex(#[from] hex::FromHexError), + /// Thrown when the AWS KMS API returns a response without a signature. + #[error("signature not found in response")] + SignatureNotFound, + /// Thrown when the AWS KMS API returns a response without a public key. + #[error("public key not found in response")] + PublicKeyNotFound, } #[async_trait::async_trait] impl Signer for AwsSigner { - type Error = AwsSignerError; - #[instrument(err)] - fn sign_hash(&self, hash: &B256) -> Result { - todo!() - } - - #[instrument(err)] - async fn sign_hash_async(&self, hash: &B256) -> Result { - self.sign_digest_with_eip155(hash, self.chain_id).await + async fn sign_hash_async(&self, hash: &B256) -> Result { + self.sign_digest_with_eip155(hash, self.chain_id).await.map_err(alloy_signer::Error::other) } #[cfg(TODO)] #[instrument(err)] - async fn sign_transaction_async( - &self, - tx: &TypedTransaction, - ) -> Result { + async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { let mut tx_with_chain = tx.clone(); let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); tx_with_chain.set_chain_id(chain_id); @@ -120,18 +109,6 @@ impl Signer for AwsSigner { self.sign_digest_with_eip155(sighash, chain_id).await } - #[cfg(feature = "eip712")] - async fn sign_typed_data_async( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result { - let digest = payload.eip712_signing_hash(domain); - let sig = self.sign_digest(&digest).await?; - let sig = sig_from_digest_bytes_trial_recovery(sig, &digest, &self.pubkey); - Ok(sig) - } - #[inline] fn address(&self) -> Address { self.address @@ -151,8 +128,7 @@ impl Signer for AwsSigner { impl AwsSigner { /// Instantiate a new signer from an existing `Client` and key ID. /// - /// This function retrieves the public key from AWS and calculates the - /// Etheruem address. It is therefore `async`. + /// Retrieves the public key from AWS and calculates the Ethereum address. #[instrument(skip(kms), err)] pub async fn new( kms: Client, @@ -162,12 +138,7 @@ impl AwsSigner { let resp = request_get_pubkey(&kms, key_id.clone()).await?; let pubkey = decode_pubkey(resp)?; let address = alloy_signer::utils::public_key_to_address(&pubkey); - - debug!( - "Instantiated AWS signer with pubkey 0x{} and address {address:?}", - hex::encode(pubkey.to_sec1_bytes()), - ); - + debug!(?pubkey, %address, "instantiated AWS signer"); Ok(Self { kms, chain_id, key_id, pubkey, address }) } @@ -236,24 +207,15 @@ async fn request_sign_digest( /// Decode an AWS KMS Pubkey response. fn decode_pubkey(resp: GetPublicKeyOutput) -> Result { - let raw = resp - .public_key - .as_ref() - .ok_or_else(|| AwsSignerError::from("Pubkey not found in response".to_owned()))?; - + let raw = resp.public_key.as_ref().ok_or(AwsSignerError::PublicKeyNotFound)?; let spki = spki::SubjectPublicKeyInfoRef::try_from(raw.as_ref())?; let key = VerifyingKey::from_sec1_bytes(spki.subject_public_key.raw_bytes())?; - Ok(key) } /// Decode an AWS KMS Signature response. fn decode_signature(resp: SignOutput) -> Result { - let raw = resp - .signature - .as_ref() - .ok_or_else(|| AwsSignerError::from("Signature not found in response".to_owned()))?; - + let raw = resp.signature.as_ref().ok_or(AwsSignerError::SignatureNotFound)?; let sig = ecdsa::Signature::from_der(raw.as_ref())?; Ok(sig.normalize_s().unwrap_or(sig)) } diff --git a/crates/signer-ledger/src/lib.rs b/crates/signer-ledger/src/lib.rs index 11d299ceeff..78fdf989c94 100644 --- a/crates/signer-ledger/src/lib.rs +++ b/crates/signer-ledger/src/lib.rs @@ -6,8 +6,7 @@ #![warn( missing_copy_implementations, missing_debug_implementations, - // TODO - // missing_docs, + missing_docs, unreachable_pub, clippy::missing_const_for_fn, rustdoc::all diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index a7eb6e5299e..4ea676a6c7b 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -2,7 +2,7 @@ use crate::types::{DerivationType, LedgerError, INS, P1, P1_FIRST, P2}; use alloy_primitives::{hex, Address}; -use alloy_signer::{Signature, Signer}; +use alloy_signer::{Result, Signature, Signer}; use async_trait::async_trait; use coins_ledger::{ common::{APDUCommand, APDUData}, @@ -21,8 +21,8 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// /// This is a simple wrapper around the [Ledger transport](Ledger). /// -/// Note that this signer only supports asynchronous operations by default. Enable the `"sync"` -/// feature to enable synchronous operations through `tokio` runtime blocking. +/// Note that this signer only supports asynchronous operations. Calling a non-asynchronous method +/// will always return an error. #[derive(Debug)] pub struct LedgerSigner { transport: Mutex, @@ -44,24 +44,21 @@ impl std::fmt::Display for LedgerSigner { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for LedgerSigner { - type Error = LedgerError; - #[inline] - async fn sign_message_async(&self, message: &[u8]) -> Result { + async fn sign_message_async(&self, message: &[u8]) -> Result { let message = message.as_ref(); let mut payload = Self::path_to_bytes(&self.derivation); payload.extend_from_slice(&(message.len() as u32).to_be_bytes()); payload.extend_from_slice(message); - self.sign_payload(INS::SIGN_PERSONAL_MESSAGE, &payload).await + self.sign_payload(INS::SIGN_PERSONAL_MESSAGE, &payload) + .await + .map_err(alloy_signer::Error::other) } #[cfg(TODO)] - async fn sign_transaction_async( - &self, - message: &TypedTransaction, - ) -> Result { + async fn sign_transaction_async(&self, message: &TypedTransaction) -> Result { let mut tx_with_chain = message.clone(); if tx_with_chain.chain_id().is_none() { // in the case we don't have a chain_id, let's use the signer chain id instead @@ -76,23 +73,8 @@ impl Signer for LedgerSigner { &self, payload: &T, domain: &Eip712Domain, - ) -> Result { - // See comment for v1.6.0 requirement - // https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999 - const EIP712_MIN_VERSION: &str = ">=1.6.0"; - let req = semver::VersionReq::parse(EIP712_MIN_VERSION).unwrap(); - let version = semver::Version::parse(&self.version().await?)?; - - // Enforce app version is greater than EIP712_MIN_VERSION - if !req.matches(&version) { - return Err(LedgerError::UnsupportedAppVersion(EIP712_MIN_VERSION)); - } - - let mut data = Self::path_to_bytes(&self.derivation); - data.extend_from_slice(domain.separator().as_slice()); - data.extend_from_slice(payload.eip712_hash_struct().as_slice()); - - self.sign_payload(INS::SIGN_ETH_EIP_712, &data).await + ) -> Result { + self.sign_typed_data_(payload, domain).await.map_err(alloy_signer::Error::other) } #[inline] @@ -131,9 +113,6 @@ impl LedgerSigner { Ok(Self { transport: Mutex::new(transport), derivation, chain_id, address }) } - /// Consume self and drop the ledger mutex - pub fn close(self) {} - /// Get the account which corresponds to our derivation path pub async fn get_address(&self) -> Result { self.get_address_with_path(&self.derivation).await @@ -240,6 +219,30 @@ impl LedgerSigner { Ok(signature) } + #[cfg(feature = "eip712")] + async fn sign_typed_data_( + &self, + payload: &T, + domain: &Eip712Domain, + ) -> Result { + // See comment for v1.6.0 requirement + // https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999 + const EIP712_MIN_VERSION: &str = ">=1.6.0"; + let req = semver::VersionReq::parse(EIP712_MIN_VERSION).unwrap(); + let version = semver::Version::parse(&self.version().await?)?; + + // Enforce app version is greater than EIP712_MIN_VERSION + if !req.matches(&version) { + return Err(LedgerError::UnsupportedAppVersion(EIP712_MIN_VERSION)); + } + + let mut data = Self::path_to_bytes(&self.derivation); + data.extend_from_slice(domain.separator().as_slice()); + data.extend_from_slice(payload.eip712_hash_struct().as_slice()); + + self.sign_payload(INS::SIGN_ETH_EIP_712, &data).await + } + /// Helper function for signing either transaction data, personal messages or EIP712 derived /// structs. #[instrument(err, skip_all, fields(command = %command, payload = hex::encode(payload)))] @@ -271,9 +274,7 @@ impl LedgerSigner { debug!("Dispatching packet to device"); let ans = block_on(transport.exchange(&command))?; - let Some(data) = ans.data() else { - return Err(LedgerError::UnexpectedNullResponse); - }; + let data = ans.data().ok_or(LedgerError::UnexpectedNullResponse)?; debug!(response = hex::encode(data), "Received response from device"); answer = Some(ans); diff --git a/crates/signer-ledger/src/types.rs b/crates/signer-ledger/src/types.rs index a76ef7a3e49..700fcba09af 100644 --- a/crates/signer-ledger/src/types.rs +++ b/crates/signer-ledger/src/types.rs @@ -36,7 +36,7 @@ pub enum LedgerError { #[error(transparent)] LedgerError(#[from] coins_ledger::errors::LedgerError), /// Device response was unexpectedly none - #[error("Received unexpected response from device. Expected data in response, found none.")] + #[error("received an unexpected empty response")] UnexpectedNullResponse, #[error(transparent)] /// [`hex`] error. @@ -49,7 +49,12 @@ pub enum LedgerError { UnsupportedAppVersion(&'static str), /// Got a response, but it didn't contain as much data as expected #[error("bad response; got {got} bytes, expected {expected}")] - ShortResponse { got: usize, expected: usize }, + ShortResponse { + /// Number of bytes received. + got: usize, + /// Number of bytes expected. + expected: usize, + }, } pub(crate) const P1_FIRST: u8 = 0x00; diff --git a/crates/signer-trezor/Cargo.toml b/crates/signer-trezor/Cargo.toml index d9611cc25a7..1a9641d3f8a 100644 --- a/crates/signer-trezor/Cargo.toml +++ b/crates/signer-trezor/Cargo.toml @@ -20,10 +20,9 @@ trezor-client = { version = "=0.1.0", default-features = false, features = ["eth protobuf = "=3.2.0" async-trait.workspace = true -home.workspace = true semver.workspace = true thiserror.workspace = true -# tracing.workspace = true +k256.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/signer-trezor/src/lib.rs b/crates/signer-trezor/src/lib.rs index a93b989aa0c..01ad48d192f 100644 --- a/crates/signer-trezor/src/lib.rs +++ b/crates/signer-trezor/src/lib.rs @@ -6,8 +6,7 @@ #![warn( missing_copy_implementations, missing_debug_implementations, - // TODO: - // missing_docs, + missing_docs, unreachable_pub, clippy::missing_const_for_fn, rustdoc::all @@ -16,10 +15,6 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -// TODO: Add tracing. -// #[macro_use] -// extern crate tracing; - // TODO: Needed to pin version. use protobuf as _; diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index a680968cbb2..2c4cc26c83d 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -1,33 +1,23 @@ use super::types::{DerivationType, TrezorError}; use alloy_primitives::{Address, U256}; -use alloy_signer::{Signature, Signer}; +use alloy_signer::{Result, Signature, Signer}; use async_trait::async_trait; -use std::{ - env, fs, - io::{Read, Write}, - path::PathBuf, -}; use trezor_client::client::Trezor; // we need firmware that supports EIP-1559 and EIP-712 const FIRMWARE_1_MIN_VERSION: &str = ">=1.11.1"; const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; -// https://docs.trezor.io/trezor-firmware/common/communication/sessions.html -const SESSION_ID_LENGTH: usize = 32; -const SESSION_FILE_NAME: &str = "trezor.session"; - /// A Trezor Ethereum signer. /// /// This is a simple wrapper around the [Trezor transport](Trezor). /// -/// Note that this signer only supports asynchronous operations by default. Enable the `"sync"` -/// feature to enable synchronous operations through `tokio` runtime blocking. +/// Note that this signer only supports asynchronous operations. Calling a non-asynchronous method +/// will always return an error. #[derive(Debug)] pub struct TrezorSigner { derivation: DerivationType, session_id: Vec, - cache_dir: PathBuf, pub(crate) chain_id: u64, pub(crate) address: Address, } @@ -35,22 +25,12 @@ pub struct TrezorSigner { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for TrezorSigner { - type Error = TrezorError; - - async fn sign_message_async(&self, message: &[u8]) -> Result { - let mut client = self.get_client(self.session_id.clone())?; - let apath = Self::convert_path(&self.derivation); - - let signature = client.ethereum_sign_message(message.into(), apath)?; - - let r = U256::from_limbs(signature.r.0); - let s = U256::from_limbs(signature.s.0); - // TODO: don't unwrap - Ok(Signature::from_scalars(r.into(), s.into(), signature.v).unwrap()) + async fn sign_message_async(&self, message: &[u8]) -> Result { + self.sign_message_(message).await.map_err(alloy_signer::Error::other) } #[cfg(TODO)] - async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { let mut tx_with_chain = message.clone(); if tx_with_chain.chain_id().is_none() { // in the case we don't have a chain_id, let's use the signer chain id instead @@ -76,39 +56,20 @@ impl Signer for TrezorSigner { } impl TrezorSigner { - pub async fn new( - derivation: DerivationType, - chain_id: u64, - cache_dir: Option, - ) -> Result { - let cache_dir = (match cache_dir.or_else(home::home_dir) { - Some(path) => path, - None => match env::current_dir() { - Ok(path) => path, - Err(e) => return Err(TrezorError::CacheError(e.to_string())), - }, - }) - .join(".ethers-rs") - .join("trezor") - .join("cache"); - - let mut blank = Self { + /// Instantiates a new Trezor signer. + pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { + let mut signer = Self { derivation: derivation.clone(), chain_id, - cache_dir, - address: Address::from([0_u8; 20]), + address: Address::ZERO, session_id: vec![], }; - - // Check if reachable - blank.initate_session()?; - blank.address = blank.get_address_with_path(&derivation).await?; - Ok(blank) + signer.initate_session()?; + signer.address = signer.get_address_with_path(&derivation).await?; + Ok(signer) } - fn check_version(version: String) -> Result<(), TrezorError> { - let version = semver::Version::parse(&version)?; - + fn check_version(version: semver::Version) -> Result<(), TrezorError> { let min_version = match version.major { 1 => FIRMWARE_1_MIN_VERSION, 2 => FIRMWARE_2_MIN_VERSION, @@ -117,7 +78,7 @@ impl TrezorSigner { _ => return Ok(()), }; - let req = semver::VersionReq::parse(min_version)?; + let req = semver::VersionReq::parse(min_version).unwrap(); // Enforce firmware version is greater than "min_version" if !req.matches(&version) { return Err(TrezorError::UnsupportedFirmwareVersion(min_version.to_string())); @@ -126,51 +87,26 @@ impl TrezorSigner { Ok(()) } - fn get_cached_session(&self) -> Result>, TrezorError> { - let mut session = [0; SESSION_ID_LENGTH]; - - if let Ok(mut file) = fs::File::open(self.cache_dir.join(SESSION_FILE_NAME)) { - file.read_exact(&mut session).map_err(|e| TrezorError::CacheError(e.to_string()))?; - Ok(Some(session.to_vec())) - } else { - Ok(None) - } - } - - fn save_session(&mut self, session_id: Vec) -> Result<(), TrezorError> { - fs::create_dir_all(&self.cache_dir).map_err(|e| TrezorError::CacheError(e.to_string()))?; - - let mut file = fs::File::create(self.cache_dir.join(SESSION_FILE_NAME)) - .map_err(|e| TrezorError::CacheError(e.to_string()))?; - - file.write_all(&session_id).map_err(|e| TrezorError::CacheError(e.to_string()))?; - - self.session_id = session_id; - Ok(()) - } - fn initate_session(&mut self) -> Result<(), TrezorError> { let mut client = trezor_client::unique(false)?; - client.init_device(self.get_cached_session()?)?; + client.init_device(None)?; let features = client.features().ok_or(TrezorError::FeaturesError)?; + let version = semver::Version::new( + features.major_version() as u64, + features.minor_version() as u64, + features.patch_version() as u64, + ); + Self::check_version(version)?; - Self::check_version(format!( - "{}.{}.{}", - features.major_version(), - features.minor_version(), - features.patch_version() - ))?; - - self.save_session(features.session_id().to_vec())?; + self.session_id = features.session_id().to_vec(); Ok(()) } - /// You need to drop(client) once you're done with it - fn get_client(&self, session_id: Vec) -> Result { + fn get_client(&self) -> Result { let mut client = trezor_client::unique(false)?; - client.init_device(Some(session_id))?; + client.init_device(Some(self.session_id.clone()))?; Ok(client) } @@ -184,7 +120,7 @@ impl TrezorSigner { &self, derivation: &DerivationType, ) -> Result { - let mut client = self.get_client(self.session_id.clone())?; + let mut client = self.get_client()?; let address_str = client.ethereum_get_address(Self::convert_path(derivation))?; Ok(address_str.parse()?) } @@ -192,7 +128,7 @@ impl TrezorSigner { /// Signs an Ethereum transaction (requires confirmation on the Trezor) #[cfg(TODO)] pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { - let mut client = self.get_client(self.session_id.clone())?; + let mut client = self.get_client()?; let arr_path = Self::convert_path(&self.derivation); @@ -232,6 +168,17 @@ impl TrezorSigner { Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) } + async fn sign_message_(&self, message: &[u8]) -> Result { + let mut client = self.get_client()?; + let apath = Self::convert_path(&self.derivation); + + let signature = client.ethereum_sign_message(message.into(), apath)?; + + let r = U256::from_limbs(signature.r.0); + let s = U256::from_limbs(signature.s.0); + Signature::from_scalars(r.into(), s.into(), signature.v).map_err(Into::into) + } + // helper which converts a derivation path to [u32] fn convert_path(derivation: &DerivationType) -> Vec { let derivation = derivation.to_string(); @@ -261,10 +208,7 @@ mod tests { // Replace this with your ETH addresses. async fn test_get_address() { // Instantiate it with the default trezor derivation path - let trezor = - TrezorSigner::new(DerivationType::TrezorLive(1), 1, Some(PathBuf::from("randomdir"))) - .await - .unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(1), 1).await.unwrap(); assert_eq!( trezor.get_address().await.unwrap(), address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), @@ -275,11 +219,21 @@ mod tests { ); } + #[tokio::test] + #[ignore] + async fn test_sign_message() { + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); + let message = "hello world"; + let sig = trezor.sign_message_async(message.as_bytes()).await.unwrap(); + let addr = trezor.get_address().await.unwrap(); + assert_eq!(sig.recover_address_from_msg(message).unwrap(), addr); + } + #[tokio::test] #[ignore] #[cfg(TODO)] async fn test_sign_tx() { - let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); // approve uni v2 router 0xff let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); @@ -299,7 +253,7 @@ mod tests { #[ignore] #[cfg(TODO)] async fn test_sign_big_data_tx() { - let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); // invalid data let big_data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_string()+ &"ff".repeat(1032*2) + "aa").unwrap(); @@ -339,7 +293,7 @@ mod tests { // Contract creation (empty `to`, with data) should show on the trezor device as: // ` "0 Wei ETH // ` new contract?" - let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); { let tx_req = Eip1559TransactionRequest::new().data(data.clone()).into(); let tx = trezor.sign_transaction(&tx_req).await.unwrap(); @@ -354,7 +308,7 @@ mod tests { #[ignore] #[cfg(TODO)] async fn test_sign_eip1559_tx() { - let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); // approve uni v2 router 0xff let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); @@ -397,14 +351,4 @@ mod tests { let tx = trezor.sign_transaction(&tx_req).await.unwrap(); } - - #[tokio::test] - #[ignore] - async fn test_sign_message() { - let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1, None).await.unwrap(); - let message = "hello world"; - let sig = trezor.sign_message_async(message.as_bytes()).await.unwrap(); - let addr = trezor.get_address().await.unwrap(); - assert_eq!(sig.recover_address_from_msg(message).unwrap(), addr); - } } diff --git a/crates/signer-trezor/src/types.rs b/crates/signer-trezor/src/types.rs index b390532e4c1..abb33be09f2 100644 --- a/crates/signer-trezor/src/types.rs +++ b/crates/signer-trezor/src/types.rs @@ -2,8 +2,6 @@ //! //! [Official Docs](https://docs.trezor.io/trezor-firmware/index.html) -#![allow(clippy::upper_case_acronyms)] - use alloy_primitives::{hex, B256, U256}; use std::fmt; use thiserror::Error; @@ -33,26 +31,28 @@ impl fmt::Display for DerivationType { #[derive(Error, Debug)] /// Error when using the Trezor transport pub enum TrezorError { - /// Underlying Trezor transport error + /// Underlying Trezor transport error. #[error(transparent)] - TrezorError(#[from] trezor_client::error::Error), - #[error("Trezor was not able to retrieve device features")] - FeaturesError, - #[error("Not able to unpack value for TrezorTransaction.")] - DataError, - /// Error when converting from a hex string + Client(#[from] trezor_client::error::Error), + /// Thrown when converting from a hex string. #[error(transparent)] - HexError(#[from] hex::FromHexError), - /// Error when converting a semver requirement + Hex(#[from] hex::FromHexError), + /// Thrown when converting a semver requirement. #[error(transparent)] - SemVerError(#[from] semver::Error), - /// Error when signing EIP712 struct with not compatible Trezor ETH app - #[error("Trezor ethereum app requires at least version: {0:?}")] + Semver(#[from] semver::Error), + /// [`ecdsa`](k256::ecdsa) error. + #[error(transparent)] + Ecdsa(#[from] k256::ecdsa::Error), + /// Thrown when trying to sign an EIP-712 struct with an incompatible Trezor Ethereum app + /// version. + #[error("Trezor Ethereum app requires at least version {0:?}")] UnsupportedFirmwareVersion(String), - #[error("Does not support ENS.")] - NoENSSupport, - #[error("Unable to access trezor cached session.")] - CacheError(String), + /// No ENS support. + #[error("Trezor does not support ENS")] + NoEnsSupport, + /// Could not retrieve device features. + #[error("could not retrieve device features")] + FeaturesError, } /// Trezor transaction. @@ -80,7 +80,7 @@ impl TrezorTransaction { pub fn load(tx: &TypedTransaction) -> Result { let to: String = match tx.to() { Some(v) => match v { - NameOrAddress::Name(_) => return Err(TrezorError::NoENSSupport), + NameOrAddress::Name(_) => return Err(TrezorError::NoEnsSupport), NameOrAddress::Address(value) => hex::encode_prefixed(value), }, // Contract Creation diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 20ea4b207ff..ac76a6888bd 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -16,8 +16,6 @@ alloy-primitives.workspace = true # TODO # alloy-rpc-types.workspace = true -coins-bip32 = "0.8.7" -coins-bip39 = "0.8.7" elliptic-curve.workspace = true k256.workspace = true rand.workspace = true @@ -27,27 +25,30 @@ async-trait.workspace = true # eip712 alloy-sol-types = { workspace = true, optional = true } -# sync -tokio = { workspace = true, features = ["macros", "rt"], optional = true } - # keystore eth-keystore = { version = "0.5.0", default-features = false, optional = true } +# mnemonic +coins-bip32 = { version = "0.8.7", default-features = false, optional = true } +coins-bip39 = { version = "0.8.7", default-features = false, optional = true } + # yubi yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional = true } [dev-dependencies] +assert_matches.workspace = true serde_json.workspace = true tempfile.workspace = true tracing-subscriber.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -# need mockhsm feature for tests +# need to enable features for tests yubihsm = { version = "0.42", features = ["mockhsm"] } +coins-bip39 = { version = "0.8.7", default-features = false, features = ["english"] } [features] eip712 = ["dep:alloy-sol-types"] -sync = ["dep:tokio"] keystore = ["dep:eth-keystore"] +mnemonic = ["dep:coins-bip32", "dep:coins-bip39"] yubihsm = ["dep:yubihsm"] diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index 4a399863b91..1dc9d99317b 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -1,19 +1,26 @@ +use std::fmt; + use alloy_primitives::hex; use k256::ecdsa; use thiserror::Error; /// Result type alias for [`SignerError`]. -pub type SignerResult = std::result::Result; +pub type Result = std::result::Result; /// Generic error type for [`Signer`](crate::Signer) implementations. #[derive(Debug, Error)] -pub enum SignerError { +pub enum Error { /// This operation is not supported by the signer. - #[error("signer operation {0} not supported")] - UnsupportedOperation, // TODO: enum UnsupportedOperation ? + #[error("operation `{0}` is not supported by the signer")] + UnsupportedOperation(UnsupportedSignerOperation), /// Mismatch between provided transaction chain ID and signer chain ID. - #[error("")] - TransactionChainIdMismatch(u64, u64), + #[error("transaction chain ID ({tx}) does not match the signer's ({signer})")] + TransactionChainIdMismatch { + /// The signer's chain ID. + signer: u64, + /// The chain ID provided by the transaction. + tx: u64, + }, /// [`ecdsa`] error. #[error(transparent)] Ecdsa(#[from] ecdsa::Error), @@ -25,17 +32,58 @@ pub enum SignerError { Other(#[from] Box), } -impl SignerError { - pub fn new(error: impl Into>) -> Self { +impl Error { + /// Constructs a new [`Other`](Self::Other) error. + #[cold] + pub fn other(error: impl Into>) -> Self { Self::Other(error.into()) } + + /// Returns `true` if the error is [`UnsupportedOperation`](Self::UnsupportedOperation). + #[inline] + pub const fn is_unsupported(&self) -> bool { + matches!(self, Self::UnsupportedOperation(_)) + } + + /// Returns the [`UnsupportedSignerOperation`] if the error is + /// [`UnsupportedOperation`](Self::UnsupportedOperation). + #[inline] + pub const fn unsupported(&self) -> Option { + match self { + Self::UnsupportedOperation(op) => Some(*op), + _ => None, + } + } +} + +/// An unsupported signer operation. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum UnsupportedSignerOperation { + /// `sign_hash` is not supported by the signer. + SignHash, + /// `sign_message` is not supported by the signer. + SignMessage, + /// `sign_transaction` is not supported by the signer. + SignTransaction, + /// `sign_typed_data` is not supported by the signer. + SignTypedData, +} + +impl fmt::Display for UnsupportedSignerOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } } -// impl From for SignerError -// where -// Box: From, -// { -// fn from(value: T) -> Self { -// Self::Other(Box::from(value)) -// } -// } +impl UnsupportedSignerOperation { + /// Returns the string representation of the operation. + #[inline] + pub const fn as_str(&self) -> &'static str { + match self { + Self::SignHash => "sign_hash", + Self::SignMessage => "sign_message", + Self::SignTransaction => "sign_transaction", + Self::SignTypedData => "sign_typed_data", + } + } +} diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index fee1e58d4d6..ea5d50bbd6a 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -15,8 +15,8 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -// mod error; -// pub use error::{SignerError, SignerResult}; +mod error; +pub use error::{Error, Result, UnsupportedSignerOperation}; mod signature; pub use signature::Signature; @@ -25,14 +25,16 @@ mod signer; pub use signer::Signer; mod wallet; -pub use wallet::{MnemonicBuilder, Wallet, WalletError}; +#[cfg(feature = "mnemonic")] +pub use wallet::MnemonicBuilder; +pub use wallet::{Wallet, WalletError}; pub mod utils; #[cfg(feature = "yubihsm")] pub use yubihsm; -/// Re-export the BIP-32 crate so that wordlists can be accessed conveniently. +#[cfg(feature = "mnemonic")] pub use coins_bip39; /// A wallet instantiated with a locally stored private key diff --git a/crates/signer/src/signature.rs b/crates/signer/src/signature.rs index d6764adb07c..9bb3112c1da 100644 --- a/crates/signer/src/signature.rs +++ b/crates/signer/src/signature.rs @@ -256,12 +256,12 @@ impl Signature { #[inline] const fn normalize_v(v: u64) -> RecoveryId { let byte = match v { - // Case 0: raw/bare - v @ 0..=26 => (v % 4) as u8, - // Case 2: non-eip155 v value - v @ 27..=34 => ((v - 27) % 4) as u8, - // Case 3: eip155 V value - v @ 35.. => ((v - 1) % 2) as u8, + // Case 1: raw/bare + 0..=26 => (v % 4) as u8, + // Case 2: non-EIP-155 v value + 27..=34 => ((v - 27) % 4) as u8, + // Case 3: EIP-155 V value + 35.. => ((v - 1) % 2) as u8, }; debug_assert!(byte <= RecoveryId::MAX); match RecoveryId::from_byte(byte) { diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 246268d0afb..2c4ac3f72a6 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -1,103 +1,68 @@ -use crate::Signature; +use crate::{Error, Result, Signature, UnsupportedSignerOperation}; use alloy_primitives::{eip191_hash_message, Address, B256}; use async_trait::async_trait; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; -#[cfg(feature = "sync")] -macro_rules! try_block_on { - ($future:expr, $default:expr $(,)?) => { - match tokio::runtime::Handle::try_current() { - Ok(handle) => handle.block_on($future), - Err(_) => $default, - } - }; -} - -#[cfg(not(feature = "sync"))] -macro_rules! try_block_on { - ($future:expr, $default:expr $(,)?) => { - $default - }; -} - /// Ethereum signer. /// -/// All signing methods rely on [`sign_hash`](Signer::sign_hash). If the signer is not able to -/// implement this method, then all other methods must be implemented directly, or they will return -/// an "unimplemented" error. +/// All provided implementations rely on [`sign_hash`](Signer::sign_hash). If the signer is not able +/// to implement this method, then all other methods must be implemented directly, or they will +/// return [`UnsupportedOperation`](Error::UnsupportedOperation). #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait Signer: Send + Sync { - /// The error type returned by the signer. - type Error: Send + Sync; - /// Signs the hash. #[inline] - fn sign_hash(&self, _hash: &B256) -> Result { - // TODO: error not panic - try_block_on!(self.sign_hash_async(_hash), unimplemented!()) + fn sign_hash(&self, hash: &B256) -> Result { + let _ = hash; + Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) } /// Signs the hash. /// - /// Asynchronous version of [`sign_hash`](Signer::sign_hash). + /// Asynchronous version of [`sign_hash`](Signer::sign_hash). The default implementation + /// delegates to the synchronous version. #[inline] - async fn sign_hash_async(&self, hash: &B256) -> Result { - if cfg!(feature = "sync") { - // TODO: error not panic - unimplemented!() - } else { - self.sign_hash(hash) - } + async fn sign_hash_async(&self, hash: &B256) -> Result { + self.sign_hash(hash) } /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] - fn sign_message(&self, message: &[u8]) -> Result { - try_block_on!( - self.sign_message_async(message), - self.sign_hash(&eip191_hash_message(message)), - ) + fn sign_message(&self, message: &[u8]) -> Result { + self.sign_hash(&eip191_hash_message(message)) } /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// - /// Asynchronous version of [`sign_message`](Signer::sign_message). + /// Asynchronous version of [`sign_message`](Signer::sign_message). The default implementation + /// delegates to the synchronous version. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] - async fn sign_message_async(&self, message: &[u8]) -> Result { - if cfg!(feature = "sync") { - self.sign_hash_async(&eip191_hash_message(message)).await - } else { - self.sign_message(message) - } + async fn sign_message_async(&self, message: &[u8]) -> Result { + self.sign_message(message) } /// Signs the transaction. #[cfg(TODO)] - fn sign_transaction(&self, message: &TypedTransaction) -> Result { - try_block_on!(self.sign_transaction_async(message), self.sign_hash(&message.sighash())) + #[inline] + fn sign_transaction(&self, message: &TypedTransaction) -> Result { + self.sign_hash(&message.sighash()) } /// Signs the transaction. /// - /// Asynchronous version of [`sign_transaction`](Signer::sign_transaction). + /// Asynchronous version of [`sign_transaction`](Signer::sign_transaction). The default + /// implementation delegates to the synchronous version. #[cfg(TODO)] #[inline] - async fn sign_transaction_async( - &self, - message: &TypedTransaction, - ) -> Result { - if cfg!(feature = "sync") { - self.sign_hash_async(&message.sighash()).await - } else { - self.sign_transaction(message) - } + async fn sign_transaction_async(&self, message: &TypedTransaction) -> Result { + self.sign_transaction(message) } /// Encodes and signs the typed data according to [EIP-712]. @@ -109,19 +74,17 @@ pub trait Signer: Send + Sync { &self, payload: &T, domain: &Eip712Domain, - ) -> Result + ) -> Result where Self: Sized, { - try_block_on!( - self.sign_typed_data_async(payload, domain), - self.sign_hash(&payload.eip712_signing_hash(domain)), - ) + self.sign_hash(&payload.eip712_signing_hash(domain)) } /// Encodes and signs the typed data according to [EIP-712]. /// - /// Asynchronous version of [`sign_typed_data`](Signer::sign_typed_data). + /// Asynchronous version of [`sign_typed_data`](Signer::sign_typed_data). The default + /// implementation delegates to the synchronous version. /// /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 #[cfg(feature = "eip712")] @@ -130,15 +93,11 @@ pub trait Signer: Send + Sync { &self, payload: &T, domain: &Eip712Domain, - ) -> Result + ) -> Result where Self: Sized, { - if cfg!(feature = "sync") { - self.sign_hash_async(&payload.eip712_signing_hash(domain)).await - } else { - self.sign_typed_data(payload, domain) - } + self.sign_typed_data(payload, domain) } /// Returns the signer's Ethereum Address. @@ -165,50 +124,63 @@ pub trait Signer: Send + Sync { #[cfg(test)] mod tests { use super::*; + use assert_matches::assert_matches; - #[cfg(feature = "eip712")] - alloy_sol_types::sol! { - #[derive(Default)] - struct Eip712Data { - uint64 a; - } - } - - struct _ObjectSafe(Box>); - - async fn test_unimplemented_signer(s: &S) { - test_unsized_unimplemented_signer(s).await; + struct _ObjectSafe(Box); + #[tokio::test] + async fn unimplemented() { #[cfg(feature = "eip712")] - { - assert!(s.sign_typed_data(&Eip712Data::default(), &Eip712Domain::default()).is_err()); - assert!(s - .sign_typed_data_async(&Eip712Data::default(), &Eip712Domain::default()) - .await - .is_err()); + alloy_sol_types::sol! { + #[derive(Default)] + struct Eip712Data { + uint64 a; + } } - } - - async fn test_unsized_unimplemented_signer(s: &S) { - assert!(s.sign_hash(&B256::ZERO).is_err()); - assert!(s.sign_hash_async(&B256::ZERO).await.is_err()); - assert!(s.sign_message(&[]).is_err()); - assert!(s.sign_message_async(&[]).await.is_err()); + async fn test_unimplemented_signer(s: &S) { + test_unsized_unimplemented_signer(s).await; + + #[cfg(feature = "eip712")] + { + assert!(s + .sign_typed_data(&Eip712Data::default(), &Eip712Domain::default()) + .is_err()); + assert!(s + .sign_typed_data_async(&Eip712Data::default(), &Eip712Domain::default()) + .await + .is_err()); + } + } - #[cfg(TODO)] - assert!(s.sign_transaction(&TypedTransaction::default()).is_err()); - #[cfg(TODO)] - assert!(s.sign_transaction_async(&TypedTransaction::default()).await.is_err()); - } + async fn test_unsized_unimplemented_signer(s: &S) { + assert_matches!( + s.sign_hash(&B256::ZERO), + Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) + ); + assert_matches!( + s.sign_hash_async(&B256::ZERO).await, + Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) + ); + + assert_matches!( + s.sign_message(&[]), + Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) + ); + assert_matches!( + s.sign_message_async(&[]).await, + Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) + ); + + #[cfg(TODO)] + assert!(s.sign_transaction(&TypedTransaction::default()).is_err()); + #[cfg(TODO)] + assert!(s.sign_transaction_async(&TypedTransaction::default()).await.is_err()); + } - #[tokio::test] - async fn unimplemented() { struct UnimplementedSigner; impl Signer for UnimplementedSigner { - type Error = (); - fn address(&self) -> Address { unimplemented!() } @@ -223,6 +195,6 @@ mod tests { } test_unimplemented_signer(&UnimplementedSigner).await; - test_unsized_unimplemented_signer(&UnimplementedSigner as &dyn Signer).await; + test_unsized_unimplemented_signer(&UnimplementedSigner as &dyn Signer).await; } } diff --git a/crates/signer/src/wallet/mnemonic.rs b/crates/signer/src/wallet/mnemonic.rs index 4b385a0a3b4..d529e9d090e 100644 --- a/crates/signer/src/wallet/mnemonic.rs +++ b/crates/signer/src/wallet/mnemonic.rs @@ -35,14 +35,14 @@ pub struct MnemonicBuilder { } /// Error produced by the mnemonic wallet module -#[derive(Error, Debug)] +#[derive(Debug, Error)] #[allow(missing_copy_implementations)] pub enum MnemonicBuilderError { - /// Error suggests that a phrase (path or words) was expected but not found - #[error("Expected phrase not found")] + /// Error suggests that a phrase (path or words) was expected but not found. + #[error("expected phrase not found")] ExpectedPhraseNotFound, - /// Error suggests that a phrase (path or words) was not expected but found - #[error("Unexpected phrase found")] + /// Error suggests that a phrase (path or words) was not expected but found. + #[error("unexpected phrase found")] UnexpectedPhraseFound, } @@ -172,7 +172,7 @@ impl MnemonicBuilder { #[cfg(test)] mod tests { use super::*; - use crate::coins_bip39::English; + use coins_bip39::English; use tempfile::tempdir; const TEST_DERIVATION_PATH: &str = "m/44'/60'/0'/2/1"; diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 753298536bc..8f9a06f4f52 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -1,16 +1,18 @@ -use crate::{Signature, Signer}; +use crate::{Result, Signature, Signer}; use alloy_primitives::{Address, B256}; use async_trait::async_trait; use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}; use std::fmt; +#[cfg(feature = "mnemonic")] mod mnemonic; +#[cfg(feature = "mnemonic")] pub use mnemonic::MnemonicBuilder; mod private_key; pub use private_key::WalletError; -#[cfg(all(feature = "yubihsm", not(target_arch = "wasm32")))] +#[cfg(feature = "yubihsm")] mod yubi; /// An Ethereum private-public key pair which can be used for signing messages. @@ -57,28 +59,12 @@ pub struct Wallet { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl + Send + Sync> Signer for Wallet { - type Error = WalletError; - #[inline] - fn sign_hash(&self, hash: &B256) -> Result { + fn sign_hash(&self, hash: &B256) -> Result { let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; Ok(Signature::new(recoverable_sig, recovery_id)) } - #[cfg(TODO)] - #[inline] - fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - // rlp (for sighash) must have the same chain id as v in the signature - let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); - let mut tx = tx.clone(); - tx.set_chain_id(chain_id); - - let sighash = tx.sighash(); - let mut sig = self.sign_hash(&sighash)?; - sig.apply_eip155(chain_id); - Ok(sig) - } - #[inline] fn address(&self) -> Address { self.address diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index b4b5feba4d3..f3bff7d479d 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -1,10 +1,8 @@ //! Specific helper functions for loading an offline K256 Private Key stored on disk -use super::{mnemonic::MnemonicBuilderError, Wallet}; +use super::Wallet; use crate::utils::secret_key_to_address; use alloy_primitives::hex; -use coins_bip32::Bip32Error; -use coins_bip39::MnemonicError; use k256::{ ecdsa::{self, SigningKey}, FieldBytes, SecretKey as K256SecretKey, @@ -19,16 +17,6 @@ use {elliptic_curve::rand_core, eth_keystore::KeystoreError, std::path::Path}; /// Error thrown by the Wallet module #[derive(Debug, Error)] pub enum WalletError { - /// Error propagated from the BIP-32 crate - #[error(transparent)] - Bip32Error(#[from] Bip32Error), - /// Error propagated from the BIP-39 crate - #[error(transparent)] - Bip39Error(#[from] MnemonicError), - /// Underlying eth keystore error - #[cfg(feature = "keystore")] - #[error(transparent)] - EthKeystoreError(#[from] KeystoreError), /// Error propagated from k256's ECDSA module #[error(transparent)] EcdsaError(#[from] ecdsa::Error), @@ -38,22 +26,37 @@ pub enum WalletError { /// Error propagated by IO operations #[error(transparent)] IoError(#[from] std::io::Error), + + /// Error propagated from the BIP-32 crate + #[error(transparent)] + #[cfg(feature = "mnemonic")] + Bip32Error(#[from] coins_bip32::Bip32Error), + /// Error propagated from the BIP-39 crate + #[error(transparent)] + #[cfg(feature = "mnemonic")] + Bip39Error(#[from] coins_bip39::MnemonicError), /// Error propagated from the mnemonic builder module. #[error(transparent)] - MnemonicBuilderError(#[from] MnemonicBuilderError), + #[cfg(feature = "mnemonic")] + MnemonicBuilderError(#[from] super::mnemonic::MnemonicBuilderError), + + /// Underlying eth keystore error + #[cfg(feature = "keystore")] + #[error(transparent)] + EthKeystoreError(#[from] KeystoreError), } impl Wallet { /// Creates a new Wallet instance from a raw scalar serialized as a byte array. #[inline] pub fn from_bytes(bytes: &FieldBytes) -> Result { - SigningKey::from_bytes(bytes).map(Self::_new) + SigningKey::from_bytes(bytes).map(Self::new_pk) } /// Creates a new Wallet instance from a raw scalar serialized as a byte slice. #[inline] pub fn from_slice(bytes: &[u8]) -> Result { - SigningKey::from_slice(bytes).map(Self::_new) + SigningKey::from_slice(bytes).map(Self::new_pk) } /// Creates a new random keypair seeded with [`rand::thread_rng()`]. @@ -65,11 +68,11 @@ impl Wallet { /// Creates a new random keypair seeded with the provided RNG. #[inline] pub fn random_with(rng: &mut R) -> Self { - Self::_new(SigningKey::random(rng)) + Self::new_pk(SigningKey::random(rng)) } #[inline] - fn _new(signer: SigningKey) -> Self { + fn new_pk(signer: SigningKey) -> Self { let address = secret_key_to_address(&signer); Self { signer, address, chain_id: 1 } } @@ -145,13 +148,13 @@ impl PartialEq for Wallet { impl From for Wallet { fn from(value: SigningKey) -> Self { - Self::_new(value) + Self::new_pk(value) } } impl From for Wallet { fn from(value: K256SecretKey) -> Self { - Self::_new(value.into()) + Self::new_pk(value.into()) } } @@ -164,30 +167,15 @@ impl FromStr for Wallet { } } -impl TryFrom<&str> for Wallet { - type Error = WalletError; - - fn try_from(value: &str) -> Result { - value.parse() - } -} - -impl TryFrom for Wallet { - type Error = WalletError; - - fn try_from(value: String) -> Result { - value.parse() - } -} - #[cfg(test)] #[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; use crate::{LocalWallet, Signer}; use alloy_primitives::address; - use std::path::Path; - use tempfile::tempdir; + + #[cfg(feature = "keystore")] + use {std::path::Path, tempfile::tempdir}; #[test] fn parse_pk() { @@ -447,25 +435,6 @@ mod tests { assert_eq!(wallet.chain_id, wallet_0x.chain_id); assert_eq!(wallet.signer, wallet_0x.signer); - // Check TryFrom<&str> - let wallet_0x_tryfrom_str: Wallet = - "0x0000000000000000000000000000000000000000000000000000000000000001" - .try_into() - .unwrap(); - assert_eq!(wallet.address, wallet_0x_tryfrom_str.address); - assert_eq!(wallet.chain_id, wallet_0x_tryfrom_str.chain_id); - assert_eq!(wallet.signer, wallet_0x_tryfrom_str.signer); - - // Check TryFrom - let wallet_0x_tryfrom_string: Wallet = - "0x0000000000000000000000000000000000000000000000000000000000000001" - .to_string() - .try_into() - .unwrap(); - assert_eq!(wallet.address, wallet_0x_tryfrom_string.address); - assert_eq!(wallet.chain_id, wallet_0x_tryfrom_string.chain_id); - assert_eq!(wallet.signer, wallet_0x_tryfrom_string.signer); - // Must fail because of `0z` "0z0000000000000000000000000000000000000000000000000000000000000001" .parse::>() From 051419349938a9f1a294111d6dac0be551755f7d Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:49:22 +0100 Subject: [PATCH 20/42] remove chain_id --- crates/signer-aws/src/signer.rs | 121 ++++++++++-------------- crates/signer-ledger/Cargo.toml | 2 +- crates/signer-ledger/src/signer.rs | 79 +++------------- crates/signer-ledger/src/types.rs | 2 +- crates/signer-trezor/src/signer.rs | 37 ++------ crates/signer/src/error.rs | 8 -- crates/signer/src/signer.rs | 78 +++++++-------- crates/signer/src/wallet/mnemonic.rs | 3 +- crates/signer/src/wallet/mod.rs | 27 +----- crates/signer/src/wallet/private_key.rs | 18 +--- crates/signer/src/wallet/yubi.rs | 2 +- 11 files changed, 121 insertions(+), 256 deletions(-) diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index d1fff52e464..08050b94d92 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -36,8 +36,7 @@ use std::fmt; /// let client = aws_sdk_kms::Client::new(&config); /// /// let key_id = "...".to_string(); -/// let chain_id = 1; -/// let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); +/// let signer = AwsSigner::new(client, key_id).await.unwrap(); /// /// let message = vec![0, 1, 2, 3]; /// @@ -48,7 +47,6 @@ use std::fmt; #[derive(Clone)] pub struct AwsSigner { kms: Client, - chain_id: u64, key_id: String, pubkey: VerifyingKey, address: Address, @@ -58,7 +56,6 @@ impl fmt::Debug for AwsSigner { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AwsSigner") .field("key_id", &self.key_id) - .field("chain_id", &self.chain_id) .field("pubkey", &hex::encode(self.pubkey.to_sec1_bytes())) .field("address", &self.address) .finish() @@ -93,53 +90,28 @@ pub enum AwsSignerError { #[async_trait::async_trait] impl Signer for AwsSigner { - #[instrument(err)] + #[inline] async fn sign_hash_async(&self, hash: &B256) -> Result { - self.sign_digest_with_eip155(hash, self.chain_id).await.map_err(alloy_signer::Error::other) - } - - #[cfg(TODO)] - #[instrument(err)] - async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { - let mut tx_with_chain = tx.clone(); - let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); - tx_with_chain.set_chain_id(chain_id); - - let sighash = tx_with_chain.sighash(); - self.sign_digest_with_eip155(sighash, chain_id).await + self.sign_hash(hash).await.map_err(alloy_signer::Error::other) } #[inline] fn address(&self) -> Address { self.address } - - #[inline] - fn chain_id(&self) -> u64 { - self.chain_id - } - - #[inline] - fn set_chain_id(&mut self, chain_id: u64) { - self.chain_id = chain_id; - } } impl AwsSigner { /// Instantiate a new signer from an existing `Client` and key ID. /// /// Retrieves the public key from AWS and calculates the Ethereum address. - #[instrument(skip(kms), err)] - pub async fn new( - kms: Client, - key_id: String, - chain_id: u64, - ) -> Result { + #[instrument(skip(kms), ret)] + pub async fn new(kms: Client, key_id: String) -> Result { let resp = request_get_pubkey(&kms, key_id.clone()).await?; let pubkey = decode_pubkey(resp)?; let address = alloy_signer::utils::public_key_to_address(&pubkey); debug!(?pubkey, %address, "instantiated AWS signer"); - Ok(Self { kms, chain_id, key_id, pubkey, address }) + Ok(Self { kms, key_id, pubkey, address }) } /// Fetch the pubkey associated with a key ID. @@ -148,40 +120,49 @@ impl AwsSigner { } /// Fetch the pubkey associated with this signer's key ID. + #[inline] pub async fn get_pubkey(&self) -> Result { self.get_pubkey_for_key(self.key_id.clone()).await } - /// Sign a digest with the key associated with a key ID. - pub async fn sign_digest_with_key( + /// Signs a hash with the key associated with a key ID. + #[instrument(skip(self), ret)] + pub async fn sign_hash_with_key( &self, key_id: String, - digest: &B256, - ) -> Result { - request_sign_digest(&self.kms, key_id, digest).await.and_then(decode_signature) + hash: &B256, + ) -> Result { + let output = request_sign_hash(&self.kms, key_id, hash).await?; + let sig = decode_signature(output)?; + Ok(sig_from_recovery(sig, hash, &self.pubkey)) } - /// Sign a digest with this signer's key - pub async fn sign_digest(&self, digest: &B256) -> Result { - self.sign_digest_with_key(self.key_id.clone(), digest).await + /// Signs a hash with this signer's key. + #[inline] + pub async fn sign_hash(&self, hash: &B256) -> Result { + self.sign_hash_with_key(self.key_id.clone(), hash).await } - /// Sign a digest with this signer's key and add the eip155 `v` value - /// corresponding to the input chain_id - #[instrument(err, skip(digest), fields(digest = %hex::encode(digest)))] - async fn sign_digest_with_eip155( + #[doc(hidden)] + #[deprecated(note = "use `sign_hash_with_key` instead")] + #[inline] + pub async fn sign_digest_with_key( &self, + key_id: String, digest: &B256, - chain_id: u64, ) -> Result { - let sig = self.sign_digest(digest).await?; - let mut sig = sig_from_digest_bytes_trial_recovery(sig, digest, &self.pubkey); - sig.apply_eip155(chain_id); - Ok(sig) + self.sign_hash_with_key(key_id, digest).await + } + + #[doc(hidden)] + #[deprecated(note = "use `sign_hash` instead")] + #[inline] + pub async fn sign_digest(&self, digest: &B256) -> Result { + self.sign_hash(digest).await } } -#[instrument(skip(kms), err)] +#[instrument(skip(kms), ret)] async fn request_get_pubkey( kms: &Client, key_id: String, @@ -189,15 +170,15 @@ async fn request_get_pubkey( kms.get_public_key().key_id(key_id).send().await.map_err(Into::into) } -#[instrument(skip(kms, digest), fields(digest = %hex::encode(digest)), err)] -async fn request_sign_digest( +#[instrument(skip(kms), ret)] +async fn request_sign_hash( kms: &Client, key_id: String, - digest: &B256, + hash: &B256, ) -> Result { kms.sign() .key_id(key_id) - .message(Blob::new(digest.as_slice())) + .message(Blob::new(hash.as_slice())) .message_type(MessageType::Digest) .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) .send() @@ -217,15 +198,16 @@ fn decode_pubkey(resp: GetPublicKeyOutput) -> Result Result { let raw = resp.signature.as_ref().ok_or(AwsSignerError::SignatureNotFound)?; let sig = ecdsa::Signature::from_der(raw.as_ref())?; - Ok(sig.normalize_s().unwrap_or(sig)) + Ok(sig) } -/// Recover an rsig from a signature under a known key by trial/error. -fn sig_from_digest_bytes_trial_recovery( - sig: ecdsa::Signature, - hash: &B256, - pubkey: &VerifyingKey, -) -> Signature { +/// Gets the recovery ID by trial and error and creates a new [Signature]. +fn sig_from_recovery(sig: ecdsa::Signature, hash: &B256, pubkey: &VerifyingKey) -> Signature { + /// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. + fn check_candidate(signature: &Signature, hash: &B256, pubkey: &VerifyingKey) -> bool { + signature.recover_from_prehash(hash).map_or(false, |key| key == *pubkey) + } + let mut signature = Signature::new(sig, RecoveryId::from_byte(0).unwrap()); if check_candidate(&signature, hash, pubkey) { return signature; @@ -239,11 +221,6 @@ fn sig_from_digest_bytes_trial_recovery( panic!("bad sig"); } -/// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. -fn check_candidate(signature: &Signature, hash: &B256, pubkey: &VerifyingKey) -> bool { - signature.recover_from_prehash(hash).map(|key| key == *pubkey).unwrap_or(false) -} - #[cfg(test)] mod tests { use super::*; @@ -255,12 +232,10 @@ mod tests { let config = aws_config::load_defaults(BehaviorVersion::latest()).await; let client = aws_sdk_kms::Client::new(&config); - let chain_id = 1; - let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); - - let message = vec![0, 1, 2, 3]; + let signer = AwsSigner::new(client, key_id).await.unwrap(); - let sig = signer.sign_message_async(&message).await.unwrap(); + let message = b"hello"; + let sig = signer.sign_message_async(message).await.unwrap(); assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); } } diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml index e46237fdf6d..5033710e034 100644 --- a/crates/signer-ledger/Cargo.toml +++ b/crates/signer-ledger/Cargo.toml @@ -16,7 +16,7 @@ alloy-primitives.workspace = true alloy-signer.workspace = true async-trait.workspace = true -coins-ledger = { version = "0.8.3", default-features = false } +coins-ledger = { version = "0.9.0", default-features = false } futures-executor.workspace = true futures-util.workspace = true semver.workspace = true diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 4ea676a6c7b..ba40a3dcfca 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -27,20 +27,9 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; pub struct LedgerSigner { transport: Mutex, derivation: DerivationType, - pub(crate) chain_id: u64, pub(crate) address: Address, } -impl std::fmt::Display for LedgerSigner { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "LedgerApp. Key at index {} with address {:?} on chain_id {}", - self.derivation, self.address, self.chain_id - ) - } -} - #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for LedgerSigner { @@ -58,13 +47,8 @@ impl Signer for LedgerSigner { } #[cfg(TODO)] - async fn sign_transaction_async(&self, message: &TypedTransaction) -> Result { - let mut tx_with_chain = message.clone(); - if tx_with_chain.chain_id().is_none() { - // in the case we don't have a chain_id, let's use the signer chain id instead - tx_with_chain.set_chain_id(self.chain_id); - } - self.sign_tx(&tx_with_chain).await + async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { + self.sign_tx(&tx).await.map_err(alloy_signer::Error::other) } #[cfg(feature = "eip712")] @@ -81,16 +65,6 @@ impl Signer for LedgerSigner { fn address(&self) -> Address { self.address } - - #[inline] - fn chain_id(&self) -> u64 { - self.chain_id - } - - #[inline] - fn set_chain_id(&mut self, chain_id: u64) { - self.chain_id = chain_id; - } } impl LedgerSigner { @@ -98,27 +72,29 @@ impl LedgerSigner { /// /// # Examples /// - /// ``` + /// ```no_run /// # async fn foo() -> Result<(), Box> { /// use alloy_signer_ledger::{HDPath, Ledger}; /// - /// let ledger = Ledger::new(HDPath::LedgerLive(0), 1).await?; + /// let ledger = Ledger::new(HDPath::LedgerLive(0)).await?; /// # Ok(()) /// # } /// ``` - pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { + #[instrument(err)] + pub async fn new(derivation: DerivationType) -> Result { let transport = Ledger::init().await?; let address = Self::get_address_with_path_transport(&transport, &derivation).await?; - - Ok(Self { transport: Mutex::new(transport), derivation, chain_id, address }) + Ok(Self { transport: Mutex::new(transport), derivation, address }) } - /// Get the account which corresponds to our derivation path + /// Returns the account that corresponds to the current device. + #[inline] pub async fn get_address(&self) -> Result { self.get_address_with_path(&self.derivation).await } - /// Gets the account which corresponds to the provided derivation path + /// Returns the account that corresponds to the provided derivation path. + #[inline] pub async fn get_address_with_path( &self, derivation: &DerivationType, @@ -184,38 +160,9 @@ impl LedgerSigner { /// Signs an Ethereum transaction (requires confirmation on the ledger) #[cfg(TODO)] pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { - let mut tx_with_chain = tx.clone(); - if tx_with_chain.chain_id().is_none() { - // in the case we don't have a chain_id, let's use the signer chain id instead - tx_with_chain.set_chain_id(self.chain_id); - } let mut payload = Self::path_to_bytes(&self.derivation); - payload.extend_from_slice(tx_with_chain.rlp().as_ref()); - + payload.extend_from_slice(tx.rlp().as_ref()); let mut signature = self.sign_payload(INS::SIGN, &payload).await?; - - // modify `v` value of signature to match EIP-155 for chains with large chain ID - // The logic is derived from Ledger's library - // https://github.com/LedgerHQ/ledgerjs/blob/e78aac4327e78301b82ba58d63a72476ecb842fc/packages/hw-app-eth/src/Eth.ts#L300 - let eip155_chain_id = self.chain_id * 2 + 35; - if eip155_chain_id + 1 > 255 { - let one_byte_chain_id = eip155_chain_id % 256; - let ecc_parity = if signature.v > one_byte_chain_id { - signature.v - one_byte_chain_id - } else { - one_byte_chain_id - signature.v - }; - - signature.v = match tx { - TypedTransaction::Eip2930(_) | TypedTransaction::Eip1559(_) => { - (ecc_parity % 2 != 1) as u64 - } - TypedTransaction::Legacy(_) => eip155_chain_id + ecc_parity, - #[cfg(feature = "optimism")] - TypedTransaction::DepositTransaction(_) => 0, - }; - } - Ok(signature) } @@ -245,7 +192,7 @@ impl LedgerSigner { /// Helper function for signing either transaction data, personal messages or EIP712 derived /// structs. - #[instrument(err, skip_all, fields(command = %command, payload = hex::encode(payload)))] + #[instrument(skip_all, fields(command = %command, payload = hex::encode(payload)), ret)] async fn sign_payload(&self, command: INS, payload: &[u8]) -> Result { let transport = self.transport.lock().await; let mut command = APDUCommand { diff --git a/crates/signer-ledger/src/types.rs b/crates/signer-ledger/src/types.rs index 700fcba09af..edab570ff8b 100644 --- a/crates/signer-ledger/src/types.rs +++ b/crates/signer-ledger/src/types.rs @@ -8,8 +8,8 @@ use alloy_primitives::hex; use std::fmt; use thiserror::Error; +/// Ledger wallet type. #[derive(Clone, Debug)] -/// Ledger wallet type pub enum DerivationType { /// Ledger Live-generated HD path LedgerLive(usize), diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index 2c4cc26c83d..2b7e8545035 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -18,7 +18,6 @@ const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; pub struct TrezorSigner { derivation: DerivationType, session_id: Vec, - pub(crate) chain_id: u64, pub(crate) address: Address, } @@ -30,40 +29,21 @@ impl Signer for TrezorSigner { } #[cfg(TODO)] - async fn sign_transaction(&self, message: &TypedTransaction) -> Result { - let mut tx_with_chain = message.clone(); - if tx_with_chain.chain_id().is_none() { - // in the case we don't have a chain_id, let's use the signer chain id instead - tx_with_chain.set_chain_id(self.chain_id); - } - self.sign_tx(&tx_with_chain).await + async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { + self.sign_tx(tx).await } #[inline] fn address(&self) -> Address { self.address } - - #[inline] - fn chain_id(&self) -> u64 { - self.chain_id - } - - #[inline] - fn set_chain_id(&mut self, chain_id: u64) { - self.chain_id = chain_id; - } } impl TrezorSigner { /// Instantiates a new Trezor signer. - pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { - let mut signer = Self { - derivation: derivation.clone(), - chain_id, - address: Address::ZERO, - session_id: vec![], - }; + pub async fn new(derivation: DerivationType) -> Result { + let mut signer = + Self { derivation: derivation.clone(), address: Address::ZERO, session_id: vec![] }; signer.initate_session()?; signer.address = signer.get_address_with_path(&derivation).await?; Ok(signer) @@ -134,7 +114,8 @@ impl TrezorSigner { let transaction = TrezorTransaction::load(tx)?; - let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + // TODO: error when no chain ID? + let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(1); let signature = match tx { TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx( @@ -208,7 +189,7 @@ mod tests { // Replace this with your ETH addresses. async fn test_get_address() { // Instantiate it with the default trezor derivation path - let trezor = TrezorSigner::new(DerivationType::TrezorLive(1), 1).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(1)).await.unwrap(); assert_eq!( trezor.get_address().await.unwrap(), address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), @@ -222,7 +203,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_sign_message() { - let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0)).await.unwrap(); let message = "hello world"; let sig = trezor.sign_message_async(message.as_bytes()).await.unwrap(); let addr = trezor.get_address().await.unwrap(); diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index 1dc9d99317b..46449d78036 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -13,14 +13,6 @@ pub enum Error { /// This operation is not supported by the signer. #[error("operation `{0}` is not supported by the signer")] UnsupportedOperation(UnsupportedSignerOperation), - /// Mismatch between provided transaction chain ID and signer chain ID. - #[error("transaction chain ID ({tx}) does not match the signer's ({signer})")] - TransactionChainIdMismatch { - /// The signer's chain ID. - signer: u64, - /// The chain ID provided by the transaction. - tx: u64, - }, /// [`ecdsa`] error. #[error(transparent)] Ecdsa(#[from] ecdsa::Error), diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 2c4ac3f72a6..8dcb605c29c 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -7,13 +7,19 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// Ethereum signer. /// -/// All provided implementations rely on [`sign_hash`](Signer::sign_hash). If the signer is not able -/// to implement this method, then all other methods must be implemented directly, or they will -/// return [`UnsupportedOperation`](Error::UnsupportedOperation). +/// All provided implementations rely on [`sign_hash`] (or [`sign_hash_async`], which delegates to +/// [`sign_hash`]). If the signer is not able to implement this method, then all other methods will +/// have to be implemented directly, or they will return +/// [`UnsupportedOperation`](Error::UnsupportedOperation). +/// +/// [`sign_hash`]: Signer::sign_hash +/// [`sign_hash_async`]: Signer::sign_hash_async #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait Signer: Send + Sync { /// Signs the hash. + /// + /// The default implementation returns [`UnsupportedOperation`](Error::UnsupportedOperation). #[inline] fn sign_hash(&self, hash: &B256) -> Result { let _ = hash; @@ -23,7 +29,7 @@ pub trait Signer: Send + Sync { /// Signs the hash. /// /// Asynchronous version of [`sign_hash`](Signer::sign_hash). The default implementation - /// delegates to the synchronous version. + /// delegates to the synchronous version; see its documentation for more details. #[inline] async fn sign_hash_async(&self, hash: &B256) -> Result { self.sign_hash(hash) @@ -39,30 +45,50 @@ pub trait Signer: Send + Sync { /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// - /// Asynchronous version of [`sign_message`](Signer::sign_message). The default implementation - /// delegates to the synchronous version. + /// Asynchronous version of [`sign_message`](Signer::sign_message). The default + /// implementation is the same as the synchronous version; see its documentation for more + /// details. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] async fn sign_message_async(&self, message: &[u8]) -> Result { - self.sign_message(message) + self.sign_hash_async(&eip191_hash_message(message)).await } /// Signs the transaction. + /// + /// The default implementation signs the [transaction's signature hash][sighash], and optionally + /// applies [EIP-155] to the signature if a chain ID is present. + /// + /// [sighash]: TypedTransaction::sighash + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[cfg(TODO)] #[inline] - fn sign_transaction(&self, message: &TypedTransaction) -> Result { - self.sign_hash(&message.sighash()) + fn sign_transaction(&self, tx: &TypedTransaction) -> Result { + self.sign_hash(&tx.sighash()).map(|mut sig| { + if let Some(chain_id) = tx.chain_id() { + sig.apply_eip155(chain_id); + } + sig + }) } /// Signs the transaction. /// /// Asynchronous version of [`sign_transaction`](Signer::sign_transaction). The default - /// implementation delegates to the synchronous version. + /// implementation is the same as the synchronous version; see its documentation for more + /// details. #[cfg(TODO)] #[inline] async fn sign_transaction_async(&self, message: &TypedTransaction) -> Result { - self.sign_transaction(message) + self.sign_hash_async(&tx.sighash()) + .map(|mut sig| { + if let Some(chain_id) = tx.chain_id() { + sig.apply_eip155(chain_id); + } + sig + }) + .await } /// Encodes and signs the typed data according to [EIP-712]. @@ -84,7 +110,8 @@ pub trait Signer: Send + Sync { /// Encodes and signs the typed data according to [EIP-712]. /// /// Asynchronous version of [`sign_typed_data`](Signer::sign_typed_data). The default - /// implementation delegates to the synchronous version. + /// implementation is the same as the synchronous version; see its documentation for more + /// details. /// /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 #[cfg(feature = "eip712")] @@ -97,28 +124,11 @@ pub trait Signer: Send + Sync { where Self: Sized, { - self.sign_typed_data(payload, domain) + self.sign_hash_async(&payload.eip712_signing_hash(domain)).await } /// Returns the signer's Ethereum Address. fn address(&self) -> Address; - - /// Returns the signer's chain ID. - fn chain_id(&self) -> u64; - - /// Sets the signer's chain ID. - fn set_chain_id(&mut self, chain_id: u64); - - /// Sets the signer's chain ID and returns `self`. - #[inline] - #[must_use] - fn with_chain_id(mut self, chain_id: u64) -> Self - where - Self: Sized, - { - self.set_chain_id(chain_id); - self - } } #[cfg(test)] @@ -184,14 +194,6 @@ mod tests { fn address(&self) -> Address { unimplemented!() } - - fn chain_id(&self) -> u64 { - unimplemented!() - } - - fn set_chain_id(&mut self, _chain_id: u64) { - unimplemented!() - } } test_unimplemented_signer(&UnimplementedSigner).await; diff --git a/crates/signer/src/wallet/mnemonic.rs b/crates/signer/src/wallet/mnemonic.rs index d529e9d090e..90990e7ecb0 100644 --- a/crates/signer/src/wallet/mnemonic.rs +++ b/crates/signer/src/wallet/mnemonic.rs @@ -164,8 +164,7 @@ impl MnemonicBuilder { let key: &coins_bip32::prelude::SigningKey = derived_priv_key.as_ref(); let signer = SigningKey::from_bytes(&key.to_bytes())?; let address = secret_key_to_address(&signer); - - Ok(Wallet:: { signer, address, chain_id: 1 }) + Ok(Wallet::new_with_signer(signer, address)) } } diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 8f9a06f4f52..d71f3b3ac04 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -31,10 +31,6 @@ mod yubi; /// /// let wallet = LocalWallet::random(); /// -/// // Optionally, the wallet's chain id can be set, in order to use EIP-155 -/// // replay protection with different chains -/// let wallet = wallet.with_chain_id(1337u64); -/// /// // The wallet can be used to sign messages /// let message = b"hello"; /// let signature = wallet.sign_message(message)?; @@ -46,14 +42,12 @@ mod yubi; /// assert_eq!(signature, signature2); /// # Ok::<_, Box>(()) /// ``` -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct Wallet { /// The wallet's private key. pub(crate) signer: D, /// The wallet's address. pub(crate) address: Address, - /// The wallet's chain ID (for EIP-155). - pub(crate) chain_id: u64, } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -69,23 +63,13 @@ impl + Send + Sync> Signer for fn address(&self) -> Address { self.address } - - #[inline] - fn chain_id(&self) -> u64 { - self.chain_id - } - - #[inline] - fn set_chain_id(&mut self, chain_id: u64) { - self.chain_id = chain_id; - } } impl + Send + Sync> Wallet { /// Construct a new wallet with an external [`PrehashSigner`]. #[inline] - pub const fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { - Wallet { signer, address, chain_id } + pub const fn new_with_signer(signer: D, address: Address) -> Self { + Wallet { signer, address } } /// Returns this wallet's signer. @@ -98,9 +82,6 @@ impl + Send + Sync> Wallet { // do not log the signer impl> fmt::Debug for Wallet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Wallet") - .field("address", &self.address) - .field("chain_id", &self.chain_id) - .finish() + f.debug_struct("Wallet").field("address", &self.address).finish() } } diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index f3bff7d479d..2e9702c29c3 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -74,7 +74,7 @@ impl Wallet { #[inline] fn new_pk(signer: SigningKey) -> Self { let address = secret_key_to_address(&signer); - Self { signer, address, chain_id: 1 } + Wallet::new_with_signer(signer, address) } } @@ -138,14 +138,6 @@ impl Wallet { } } -impl PartialEq for Wallet { - fn eq(&self, other: &Self) -> bool { - self.signer.to_bytes().eq(&other.signer.to_bytes()) - && self.address == other.address - && self.chain_id == other.chain_id - } -} - impl From for Wallet { fn from(value: SigningKey) -> Self { Self::new_pk(value) @@ -418,9 +410,7 @@ mod tests { let key_as_bytes = wallet.signer.to_bytes(); let wallet_from_bytes = Wallet::from_bytes(&key_as_bytes).unwrap(); - assert_eq!(wallet.address, wallet_from_bytes.address); - assert_eq!(wallet.chain_id, wallet_from_bytes.chain_id); - assert_eq!(wallet.signer, wallet_from_bytes.signer); + assert_eq!(wallet, wallet_from_bytes); } #[test] @@ -431,9 +421,7 @@ mod tests { // Check FromStr and `0x` let wallet_0x: Wallet = "0x0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); - assert_eq!(wallet.address, wallet_0x.address); - assert_eq!(wallet.chain_id, wallet_0x.chain_id); - assert_eq!(wallet.signer, wallet_0x.signer); + assert_eq!(wallet, wallet_0x); // Must fail because of `0z` "0z0000000000000000000000000000000000000000000000000000000000000001" diff --git a/crates/signer/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs index 3e19243a408..07f6e5a4f09 100644 --- a/crates/signer/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -59,7 +59,7 @@ impl From> for Wallet> { let bytes = pubkey.as_bytes(); debug_assert_eq!(bytes[0], 0x04); let address = raw_public_key_to_address(&bytes[1..]); - Self::new_with_signer(signer, address, 1) + Self::new_with_signer(signer, address) } } From 2e5734d48c82e5088f6cf4c8fed5bb387fd01a9b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:51:42 +0100 Subject: [PATCH 21/42] chore: clippy --- crates/signer-aws/src/signer.rs | 2 +- crates/signer-ledger/src/signer.rs | 2 -- crates/signer-ledger/src/types.rs | 2 +- crates/signer-trezor/src/signer.rs | 2 +- crates/signer/src/error.rs | 7 +++---- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index 08050b94d92..82e29b71524 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -77,7 +77,7 @@ pub enum AwsSignerError { /// [`spki`] error. #[error(transparent)] Spki(#[from] spki::Error), - /// [`hex`] error. + /// [`hex`](mod@hex) error. #[error(transparent)] Hex(#[from] hex::FromHexError), /// Thrown when the AWS KMS API returns a response without a signature. diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index ba40a3dcfca..875b2c30fb6 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -35,8 +35,6 @@ pub struct LedgerSigner { impl Signer for LedgerSigner { #[inline] async fn sign_message_async(&self, message: &[u8]) -> Result { - let message = message.as_ref(); - let mut payload = Self::path_to_bytes(&self.derivation); payload.extend_from_slice(&(message.len() as u32).to_be_bytes()); payload.extend_from_slice(message); diff --git a/crates/signer-ledger/src/types.rs b/crates/signer-ledger/src/types.rs index edab570ff8b..5f014073a6e 100644 --- a/crates/signer-ledger/src/types.rs +++ b/crates/signer-ledger/src/types.rs @@ -39,7 +39,7 @@ pub enum LedgerError { #[error("received an unexpected empty response")] UnexpectedNullResponse, #[error(transparent)] - /// [`hex`] error. + /// [`hex`](mod@hex) error. HexError(#[from] hex::FromHexError), #[error(transparent)] /// [`semver`] error. diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index 2b7e8545035..4a2e6268369 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -29,7 +29,7 @@ impl Signer for TrezorSigner { } #[cfg(TODO)] - async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { + async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { self.sign_tx(tx).await } diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index 46449d78036..292177384ab 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -1,10 +1,9 @@ -use std::fmt; - use alloy_primitives::hex; use k256::ecdsa; +use std::fmt; use thiserror::Error; -/// Result type alias for [`SignerError`]. +/// Result type alias for [`Error`](enum@Error). pub type Result = std::result::Result; /// Generic error type for [`Signer`](crate::Signer) implementations. @@ -16,7 +15,7 @@ pub enum Error { /// [`ecdsa`] error. #[error(transparent)] Ecdsa(#[from] ecdsa::Error), - /// [`hex`] error. + /// [`hex`](mod@hex) error. #[error(transparent)] HexError(#[from] hex::FromHexError), /// Generic error. From 19328b4742dcfbc715051fd3b12f046e8d42b453 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:02:19 +0100 Subject: [PATCH 22/42] feats --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fead3d2e1a2..a1fc6e0bf1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,11 +35,11 @@ alloy-sol-types = { version = "0.5.0", default-features = false, features = ["st alloy-rlp = "0.3" # crypto -elliptic-curve = { version = "0.13.5", default-features = false } -generic-array = { version = "0.14.7", default-features = false } +elliptic-curve = { version = "0.13.5", default-features = false, features = ["std"] } +generic-array = { version = "0.14.7", default-features = false, features = ["std"] } k256 = { version = "0.13.2", default-features = false, features = ["ecdsa", "std"] } -sha2 = { version = "0.10.8", default-features = false } -spki = { version = "0.7.2", default-features = false } +sha2 = { version = "0.10.8", default-features = false, features = ["std"] } +spki = { version = "0.7.2", default-features = false, features = ["std"] } # async async-trait = "0.1.74" From e2709bf57ff0f0aeb4688bd710c7f4e9fe6b335b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:07:01 +0100 Subject: [PATCH 23/42] msrv --- .github/workflows/ci.yml | 10 +++++----- Cargo.toml | 2 +- README.md | 2 +- clippy.toml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3934561434f..ba0fe113948 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,11 @@ jobs: strategy: fail-fast: false matrix: - rust: ["stable", "beta", "nightly", "1.65"] # MSRV + rust: ["stable", "beta", "nightly", "1.68"] # MSRV flags: ["--no-default-features", "", "--all-features"] exclude: - # Skip because some features have highest MSRV. - - rust: "1.65" # MSRV + # Some features have higher MSRV. + - rust: "1.68" # MSRV flags: "--all-features" steps: - uses: actions/checkout@v3 @@ -36,10 +36,10 @@ jobs: cache-on-failure: true # Only run tests on latest stable and above - name: build - if: ${{ matrix.rust == '1.65' }} # MSRV + if: ${{ matrix.rust == '1.68' }} # MSRV run: cargo build --workspace ${{ matrix.flags }} - name: test - if: ${{ matrix.rust != '1.65' }} # MSRV + if: ${{ matrix.rust != '1.68' }} # MSRV run: cargo test --workspace ${{ matrix.flags }} wasm: diff --git a/Cargo.toml b/Cargo.toml index a1fc6e0bf1c..00c8ddc61b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] version = "0.1.0" edition = "2021" -rust-version = "1.65" +rust-version = "1.68" authors = ["Alloy Contributors"] license = "MIT OR Apache-2.0" homepage = "https://github.com/alloy-rs/next" diff --git a/README.md b/README.md index c88bfe77552..8c3522f1791 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ When updating this, also update: Alloy will keep a rolling MSRV (minimum supported rust version) policy of **at least** 6 months. When increasing the MSRV, the new Rust version must have been -released at least six months ago. The current MSRV is 1.65.0. +released at least six months ago. The current MSRV is 1.68. Note that the MSRV is not increased automatically, and only as part of a minor release. diff --git a/clippy.toml b/clippy.toml index 04371125d90..fa19647b105 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.65" +msrv = "1.68" From 3878e88cbaec44d5da05e8c9d7de71b9990d7e7a Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:28:13 +0100 Subject: [PATCH 24/42] deps --- .github/workflows/ci.yml | 7 +++---- Cargo.toml | 2 +- crates/pubsub/Cargo.toml | 2 +- crates/signer-aws/Cargo.toml | 2 +- crates/signer-ledger/Cargo.toml | 5 ++++- crates/signer-ledger/src/lib.rs | 3 +++ crates/signer-trezor/Cargo.toml | 2 +- 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba0fe113948..83327b02d41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,13 +48,12 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown + - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true - - name: check - run: cargo check --workspace --target wasm32-unknown-unknown + - name: cargo hack + run: cargo hack check --target wasm32-unknown-unknown feature-checks: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 00c8ddc61b8..92845daa519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ futures-util = "0.3.29" futures-executor = "0.3.29" hyper = "0.14.27" -tokio = { version = "1.33", features = ["sync", "macros"] } +tokio = "1.33" tower = { version = "0.4.13", features = ["util"] } tracing = "0.1.40" diff --git a/crates/pubsub/Cargo.toml b/crates/pubsub/Cargo.toml index 3d4182b7590..69b2f91e47a 100644 --- a/crates/pubsub/Cargo.toml +++ b/crates/pubsub/Cargo.toml @@ -19,6 +19,6 @@ alloy-transport.workspace = true bimap.workspace = true futures.workspace = true serde_json.workspace = true -tokio.workspace = true +tokio = { workspace = true, features = ["macros", "sync"] } tower.workspace = true tracing.workspace = true diff --git a/crates/signer-aws/Cargo.toml b/crates/signer-aws/Cargo.toml index 70a654943b0..25cb2b44379 100644 --- a/crates/signer-aws/Cargo.toml +++ b/crates/signer-aws/Cargo.toml @@ -22,7 +22,7 @@ spki.workspace = true thiserror.workspace = true tracing.workspace = true -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +[dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } aws-config = { version = "1.0", default-features = false } diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml index 5033710e034..4a4cbb07be2 100644 --- a/crates/signer-ledger/Cargo.toml +++ b/crates/signer-ledger/Cargo.toml @@ -26,7 +26,10 @@ tracing.workspace = true # eip712 alloy-sol-types = { workspace = true, optional = true } -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +# https://github.com/summa-tx/coins/pull/127 +tokio = { workspace = true, features = ["rt"] } + +[dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [features] diff --git a/crates/signer-ledger/src/lib.rs b/crates/signer-ledger/src/lib.rs index 78fdf989c94..0a8032de23f 100644 --- a/crates/signer-ledger/src/lib.rs +++ b/crates/signer-ledger/src/lib.rs @@ -18,6 +18,9 @@ #[macro_use] extern crate tracing; +// https://github.com/summa-tx/coins/pull/127 +use tokio as _; + mod signer; pub use signer::LedgerSigner; diff --git a/crates/signer-trezor/Cargo.toml b/crates/signer-trezor/Cargo.toml index 1a9641d3f8a..dcc62528a97 100644 --- a/crates/signer-trezor/Cargo.toml +++ b/crates/signer-trezor/Cargo.toml @@ -24,5 +24,5 @@ semver.workspace = true thiserror.workspace = true k256.workspace = true -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +[dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } From 49961f2f7592dbc23ff15b06939fb3c5607334ce Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:55:58 +0100 Subject: [PATCH 25/42] wazm --- .github/workflows/ci.yml | 9 ++++++++- crates/signer/src/wallet/mnemonic.rs | 15 ++++++--------- crates/signer/src/wallet/private_key.rs | 4 ---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83327b02d41..76ea3aac76a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,12 +48,19 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable + with: + target: wasm32-unknown-unknown - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 with: cache-on-failure: true - name: cargo hack - run: cargo hack check --target wasm32-unknown-unknown + run: | + cargo hack check --workspace --target wasm32-unknown-unknown \ + --exclude alloy-signer \ + --exclude alloy-signer-aws \ + --exclude alloy-signer-ledger \ + --exclude alloy-signer-trezor feature-checks: runs-on: ubuntu-latest diff --git a/crates/signer/src/wallet/mnemonic.rs b/crates/signer/src/wallet/mnemonic.rs index 90990e7ecb0..0fcd6eb7467 100644 --- a/crates/signer/src/wallet/mnemonic.rs +++ b/crates/signer/src/wallet/mnemonic.rs @@ -5,8 +5,7 @@ use crate::{utils::secret_key_to_address, Wallet, WalletError}; use coins_bip32::path::DerivationPath; use coins_bip39::{Mnemonic, Wordlist}; use k256::ecdsa::SigningKey; -use rand::Rng; -use std::{fs::File, io::Write, marker::PhantomData, path::PathBuf, str::FromStr}; +use std::{marker::PhantomData, path::PathBuf}; use thiserror::Error; const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/"; @@ -87,12 +86,11 @@ impl MnemonicBuilder { /// /// # Examples /// - /// ``` + /// ```no_run /// use alloy_signer::{coins_bip39::English, MnemonicBuilder}; - /// # async fn foo() -> Result<(), Box> { /// - /// let mut rng = rand::thread_rng(); - /// let wallet = MnemonicBuilder::::default().word_count(24).build_random(&mut rng)?; + /// # async fn foo() -> Result<(), Box> { + /// let wallet = MnemonicBuilder::::default().word_count(24).build()?; /// /// # Ok(()) /// # } @@ -110,7 +108,7 @@ impl MnemonicBuilder { /// Sets the derivation path of the child key to be derived. pub fn derivation_path>(mut self, path: T) -> Result { - self.derivation_path = DerivationPath::from_str(path.as_ref())?; + self.derivation_path = path.as_ref().parse()?; Ok(self) } @@ -148,8 +146,7 @@ impl MnemonicBuilder { // Write the mnemonic phrase to storage if a directory has been provided. if let Some(dir) = &self.write_to { - let mut file = File::create(dir.as_path().join(wallet.address.to_string()))?; - file.write_all(mnemonic.to_phrase().as_bytes())?; + std::fs::write(dir.join(wallet.address.to_string()), mnemonic.to_phrase().as_bytes())?; } Ok(wallet) diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index 2e9702c29c3..43e3a217c3e 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -84,7 +84,6 @@ impl Wallet { /// provided directory. Returns a tuple (Wallet, String) of the wallet instance for the /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`, /// the keystore is stored as the stringified UUID. - #[cfg(not(target_arch = "wasm32"))] #[inline] pub fn new_keystore( dir: P, @@ -102,7 +101,6 @@ impl Wallet { } /// Decrypts an encrypted JSON from the provided path to construct a Wallet instance - #[cfg(not(target_arch = "wasm32"))] #[inline] pub fn decrypt_keystore(keypath: P, password: S) -> Result where @@ -117,7 +115,6 @@ impl Wallet { /// provided directory. Returns a tuple (Wallet, String) of the wallet instance for the /// keystore with its random UUID. Accepts an optional name for the keystore file. If `None`, /// the keystore is stored as the stringified UUID. - #[cfg(not(target_arch = "wasm32"))] #[inline] pub fn encrypt_keystore( keypath: P, @@ -160,7 +157,6 @@ impl FromStr for Wallet { } #[cfg(test)] -#[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; use crate::{LocalWallet, Signer}; From 2ec2b606c27e6d6bfd1e2125b0b05414b387a79d Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:57:57 +0100 Subject: [PATCH 26/42] stop failing --- crates/signer/src/wallet/mnemonic.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/signer/src/wallet/mnemonic.rs b/crates/signer/src/wallet/mnemonic.rs index 0fcd6eb7467..b0f32517740 100644 --- a/crates/signer/src/wallet/mnemonic.rs +++ b/crates/signer/src/wallet/mnemonic.rs @@ -5,6 +5,7 @@ use crate::{utils::secret_key_to_address, Wallet, WalletError}; use coins_bip32::path::DerivationPath; use coins_bip39::{Mnemonic, Wordlist}; use k256::ecdsa::SigningKey; +use rand::Rng; use std::{marker::PhantomData, path::PathBuf}; use thiserror::Error; From a8a946bed3f9019b839f135e7320a8b2d4df1bce Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:03:26 +0100 Subject: [PATCH 27/42] asynctrait --- crates/signer-aws/src/signer.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index 82e29b71524..d3c2344468e 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -1,5 +1,6 @@ use alloy_primitives::{hex, Address, B256}; use alloy_signer::{Result, Signature, Signer}; +use async_trait::async_trait; use aws_sdk_kms::{ error::SdkError, operation::{ @@ -88,7 +89,8 @@ pub enum AwsSignerError { PublicKeyNotFound, } -#[async_trait::async_trait] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for AwsSigner { #[inline] async fn sign_hash_async(&self, hash: &B256) -> Result { From ec9cf0291096b0c2709fc80339869359361a7f51 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:11:37 +0100 Subject: [PATCH 28/42] tracing --- crates/signer-ledger/src/signer.rs | 1 + crates/signer-trezor/Cargo.toml | 1 + crates/signer-trezor/src/lib.rs | 3 +++ crates/signer-trezor/src/signer.rs | 19 +++++++++++++++++-- crates/signer/Cargo.toml | 1 - 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 875b2c30fb6..87d0339f69a 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -45,6 +45,7 @@ impl Signer for LedgerSigner { } #[cfg(TODO)] + #[inline] async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { self.sign_tx(&tx).await.map_err(alloy_signer::Error::other) } diff --git a/crates/signer-trezor/Cargo.toml b/crates/signer-trezor/Cargo.toml index dcc62528a97..b2f47165cea 100644 --- a/crates/signer-trezor/Cargo.toml +++ b/crates/signer-trezor/Cargo.toml @@ -23,6 +23,7 @@ async-trait.workspace = true semver.workspace = true thiserror.workspace = true k256.workspace = true +tracing.workspace = true [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/signer-trezor/src/lib.rs b/crates/signer-trezor/src/lib.rs index 01ad48d192f..37bb3e3344e 100644 --- a/crates/signer-trezor/src/lib.rs +++ b/crates/signer-trezor/src/lib.rs @@ -15,6 +15,9 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#[macro_use] +extern crate tracing; + // TODO: Needed to pin version. use protobuf as _; diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index 4a2e6268369..34cd5804d42 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -1,7 +1,8 @@ use super::types::{DerivationType, TrezorError}; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{hex, Address, U256}; use alloy_signer::{Result, Signature, Signer}; use async_trait::async_trait; +use std::fmt; use trezor_client::client::Trezor; // we need firmware that supports EIP-1559 and EIP-712 @@ -14,21 +15,32 @@ const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; /// /// Note that this signer only supports asynchronous operations. Calling a non-asynchronous method /// will always return an error. -#[derive(Debug)] pub struct TrezorSigner { derivation: DerivationType, session_id: Vec, pub(crate) address: Address, } +impl fmt::Debug for TrezorSigner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrezorSigner") + .field("derivation", &self.derivation) + .field("session_id", &hex::encode(&self.session_id)) + .field("address", &self.address) + .finish() + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for TrezorSigner { + #[inline] async fn sign_message_async(&self, message: &[u8]) -> Result { self.sign_message_(message).await.map_err(alloy_signer::Error::other) } #[cfg(TODO)] + #[inline] async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { self.sign_tx(tx).await } @@ -41,6 +53,7 @@ impl Signer for TrezorSigner { impl TrezorSigner { /// Instantiates a new Trezor signer. + #[instrument(ret)] pub async fn new(derivation: DerivationType) -> Result { let mut signer = Self { derivation: derivation.clone(), address: Address::ZERO, session_id: vec![] }; @@ -96,6 +109,7 @@ impl TrezorSigner { } /// Gets the account which corresponds to the provided derivation path + #[instrument(ret)] pub async fn get_address_with_path( &self, derivation: &DerivationType, @@ -149,6 +163,7 @@ impl TrezorSigner { Ok(Signature { r: signature.r, s: signature.s, v: signature.v }) } + #[instrument(skip(message), fields(message=hex::encode(message)), ret)] async fn sign_message_(&self, message: &[u8]) -> Result { let mut client = self.get_client()?; let apath = Self::convert_path(&self.derivation); diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index ac76a6888bd..7c3b8b023ba 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -39,7 +39,6 @@ yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional assert_matches.workspace = true serde_json.workspace = true tempfile.workspace = true -tracing-subscriber.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } # need to enable features for tests From 26bf3b61dba54ca6adb8893567edf8a36bde0294 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:38:36 +0100 Subject: [PATCH 29/42] update ledger --- crates/signer-ledger/Cargo.toml | 6 +----- crates/signer-ledger/src/lib.rs | 3 --- crates/signer-ledger/src/signer.rs | 30 ++++++++++-------------------- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml index 4a4cbb07be2..a7923400b63 100644 --- a/crates/signer-ledger/Cargo.toml +++ b/crates/signer-ledger/Cargo.toml @@ -16,8 +16,7 @@ alloy-primitives.workspace = true alloy-signer.workspace = true async-trait.workspace = true -coins-ledger = { version = "0.9.0", default-features = false } -futures-executor.workspace = true +coins-ledger = { version = "0.9.1", default-features = false } futures-util.workspace = true semver.workspace = true thiserror.workspace = true @@ -26,9 +25,6 @@ tracing.workspace = true # eip712 alloy-sol-types = { workspace = true, optional = true } -# https://github.com/summa-tx/coins/pull/127 -tokio = { workspace = true, features = ["rt"] } - [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/signer-ledger/src/lib.rs b/crates/signer-ledger/src/lib.rs index 0a8032de23f..78fdf989c94 100644 --- a/crates/signer-ledger/src/lib.rs +++ b/crates/signer-ledger/src/lib.rs @@ -18,9 +18,6 @@ #[macro_use] extern crate tracing; -// https://github.com/summa-tx/coins/pull/127 -use tokio as _; - mod signer; pub use signer::LedgerSigner; diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 87d0339f69a..fd6c21c36fa 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -9,10 +9,6 @@ use coins_ledger::{ transports::{Ledger, LedgerAsync}, }; use futures_util::lock::Mutex; -use tracing::field; - -// TODO: Ledger futures aren't Send. -use futures_executor::block_on; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; @@ -118,7 +114,7 @@ impl LedgerSigner { }; debug!("Dispatching get_address request to ethereum app"); - let answer = block_on(transport.exchange(&command))?; + let answer = transport.exchange(&command).await?; let result = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?; let address = { @@ -134,7 +130,7 @@ impl LedgerSigner { } /// Returns the semver of the Ethereum ledger app - pub async fn version(&self) -> Result { + pub async fn version(&self) -> Result { let transport = self.transport.lock().await; let command = APDUCommand { @@ -146,13 +142,13 @@ impl LedgerSigner { }; debug!("Dispatching get_version"); - let answer = block_on(transport.exchange(&command))?; + let answer = transport.exchange(&command).await?; let data = answer.data().ok_or(LedgerError::UnexpectedNullResponse)?; - if data.len() != 4 { + let &[_flags, major, minor, patch] = data else { return Err(LedgerError::ShortResponse { got: data.len(), expected: 4 }); - } - let version = format!("{}.{}.{}", data[1], data[2], data[3]); - debug!(version, "Retrieved version from device"); + }; + let version = semver::Version::new(major as u64, minor as u64, patch as u64); + debug!(%version, "Retrieved version from device"); Ok(version) } @@ -175,7 +171,7 @@ impl LedgerSigner { // https://github.com/LedgerHQ/app-ethereum/issues/105#issuecomment-765316999 const EIP712_MIN_VERSION: &str = ">=1.6.0"; let req = semver::VersionReq::parse(EIP712_MIN_VERSION).unwrap(); - let version = semver::Version::parse(&self.version().await?)?; + let version = self.version().await?; // Enforce app version is greater than EIP712_MIN_VERSION if !req.matches(&version) { @@ -209,17 +205,12 @@ impl LedgerSigner { (0..=255).rev().find(|i| payload.len() % i != 3).expect("true for any length"); // Iterate in 255 byte chunks - let span = debug_span!("send_loop", index = field::Empty, chunk = field::Empty).entered(); - for (index, chunk) in payload.chunks(chunk_size).enumerate() { - if !span.is_disabled() { - span.record("index", index); - span.record("chunk", hex::encode(chunk)); - } + for chunk in payload.chunks(chunk_size) { command.data = APDUData::new(chunk); debug!("Dispatching packet to device"); - let ans = block_on(transport.exchange(&command))?; + let ans = transport.exchange(&command).await?; let data = ans.data().ok_or(LedgerError::UnexpectedNullResponse)?; debug!(response = hex::encode(data), "Received response from device"); answer = Some(ans); @@ -227,7 +218,6 @@ impl LedgerSigner { // We need more data command.p1 = P1::MORE as u8; } - drop(span); drop(transport); let answer = answer.unwrap(); From 447982c6b7a313cc0e73f92ca73624e3e95c8d95 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:40:59 +0100 Subject: [PATCH 30/42] Revert "remove chain_id" This reverts commit 051419349938a9f1a294111d6dac0be551755f7d. --- crates/signer-aws/src/signer.rs | 121 ++++++++++++++---------- crates/signer-ledger/src/signer.rs | 70 +++++++++++--- crates/signer-ledger/src/types.rs | 2 +- crates/signer-trezor/src/signer.rs | 28 ++++-- crates/signer/src/error.rs | 8 ++ crates/signer/src/signer.rs | 78 ++++++++------- crates/signer/src/wallet/mnemonic.rs | 3 +- crates/signer/src/wallet/mod.rs | 27 +++++- crates/signer/src/wallet/private_key.rs | 18 +++- crates/signer/src/wallet/yubi.rs | 2 +- 10 files changed, 241 insertions(+), 116 deletions(-) diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index d3c2344468e..44216ff9bbf 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -37,7 +37,8 @@ use std::fmt; /// let client = aws_sdk_kms::Client::new(&config); /// /// let key_id = "...".to_string(); -/// let signer = AwsSigner::new(client, key_id).await.unwrap(); +/// let chain_id = 1; +/// let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); /// /// let message = vec![0, 1, 2, 3]; /// @@ -48,6 +49,7 @@ use std::fmt; #[derive(Clone)] pub struct AwsSigner { kms: Client, + chain_id: u64, key_id: String, pubkey: VerifyingKey, address: Address, @@ -57,6 +59,7 @@ impl fmt::Debug for AwsSigner { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AwsSigner") .field("key_id", &self.key_id) + .field("chain_id", &self.chain_id) .field("pubkey", &hex::encode(self.pubkey.to_sec1_bytes())) .field("address", &self.address) .finish() @@ -92,28 +95,53 @@ pub enum AwsSignerError { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for AwsSigner { - #[inline] + #[instrument(err)] async fn sign_hash_async(&self, hash: &B256) -> Result { - self.sign_hash(hash).await.map_err(alloy_signer::Error::other) + self.sign_digest_with_eip155(hash, self.chain_id).await.map_err(alloy_signer::Error::other) + } + + #[cfg(TODO)] + #[instrument(err)] + async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { + let mut tx_with_chain = tx.clone(); + let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); + tx_with_chain.set_chain_id(chain_id); + + let sighash = tx_with_chain.sighash(); + self.sign_digest_with_eip155(sighash, chain_id).await } #[inline] fn address(&self) -> Address { self.address } + + #[inline] + fn chain_id(&self) -> u64 { + self.chain_id + } + + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; + } } impl AwsSigner { /// Instantiate a new signer from an existing `Client` and key ID. /// /// Retrieves the public key from AWS and calculates the Ethereum address. - #[instrument(skip(kms), ret)] - pub async fn new(kms: Client, key_id: String) -> Result { + #[instrument(skip(kms), err)] + pub async fn new( + kms: Client, + key_id: String, + chain_id: u64, + ) -> Result { let resp = request_get_pubkey(&kms, key_id.clone()).await?; let pubkey = decode_pubkey(resp)?; let address = alloy_signer::utils::public_key_to_address(&pubkey); debug!(?pubkey, %address, "instantiated AWS signer"); - Ok(Self { kms, key_id, pubkey, address }) + Ok(Self { kms, chain_id, key_id, pubkey, address }) } /// Fetch the pubkey associated with a key ID. @@ -122,49 +150,40 @@ impl AwsSigner { } /// Fetch the pubkey associated with this signer's key ID. - #[inline] pub async fn get_pubkey(&self) -> Result { self.get_pubkey_for_key(self.key_id.clone()).await } - /// Signs a hash with the key associated with a key ID. - #[instrument(skip(self), ret)] - pub async fn sign_hash_with_key( + /// Sign a digest with the key associated with a key ID. + pub async fn sign_digest_with_key( &self, key_id: String, - hash: &B256, - ) -> Result { - let output = request_sign_hash(&self.kms, key_id, hash).await?; - let sig = decode_signature(output)?; - Ok(sig_from_recovery(sig, hash, &self.pubkey)) + digest: &B256, + ) -> Result { + request_sign_digest(&self.kms, key_id, digest).await.and_then(decode_signature) } - /// Signs a hash with this signer's key. - #[inline] - pub async fn sign_hash(&self, hash: &B256) -> Result { - self.sign_hash_with_key(self.key_id.clone(), hash).await + /// Sign a digest with this signer's key + pub async fn sign_digest(&self, digest: &B256) -> Result { + self.sign_digest_with_key(self.key_id.clone(), digest).await } - #[doc(hidden)] - #[deprecated(note = "use `sign_hash_with_key` instead")] - #[inline] - pub async fn sign_digest_with_key( + /// Sign a digest with this signer's key and add the eip155 `v` value + /// corresponding to the input chain_id + #[instrument(err, skip(digest), fields(digest = %hex::encode(digest)))] + async fn sign_digest_with_eip155( &self, - key_id: String, digest: &B256, + chain_id: u64, ) -> Result { - self.sign_hash_with_key(key_id, digest).await - } - - #[doc(hidden)] - #[deprecated(note = "use `sign_hash` instead")] - #[inline] - pub async fn sign_digest(&self, digest: &B256) -> Result { - self.sign_hash(digest).await + let sig = self.sign_digest(digest).await?; + let mut sig = sig_from_digest_bytes_trial_recovery(sig, digest, &self.pubkey); + sig.apply_eip155(chain_id); + Ok(sig) } } -#[instrument(skip(kms), ret)] +#[instrument(skip(kms), err)] async fn request_get_pubkey( kms: &Client, key_id: String, @@ -172,15 +191,15 @@ async fn request_get_pubkey( kms.get_public_key().key_id(key_id).send().await.map_err(Into::into) } -#[instrument(skip(kms), ret)] -async fn request_sign_hash( +#[instrument(skip(kms, digest), fields(digest = %hex::encode(digest)), err)] +async fn request_sign_digest( kms: &Client, key_id: String, - hash: &B256, + digest: &B256, ) -> Result { kms.sign() .key_id(key_id) - .message(Blob::new(hash.as_slice())) + .message(Blob::new(digest.as_slice())) .message_type(MessageType::Digest) .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) .send() @@ -200,16 +219,15 @@ fn decode_pubkey(resp: GetPublicKeyOutput) -> Result Result { let raw = resp.signature.as_ref().ok_or(AwsSignerError::SignatureNotFound)?; let sig = ecdsa::Signature::from_der(raw.as_ref())?; - Ok(sig) + Ok(sig.normalize_s().unwrap_or(sig)) } -/// Gets the recovery ID by trial and error and creates a new [Signature]. -fn sig_from_recovery(sig: ecdsa::Signature, hash: &B256, pubkey: &VerifyingKey) -> Signature { - /// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. - fn check_candidate(signature: &Signature, hash: &B256, pubkey: &VerifyingKey) -> bool { - signature.recover_from_prehash(hash).map_or(false, |key| key == *pubkey) - } - +/// Recover an rsig from a signature under a known key by trial/error. +fn sig_from_digest_bytes_trial_recovery( + sig: ecdsa::Signature, + hash: &B256, + pubkey: &VerifyingKey, +) -> Signature { let mut signature = Signature::new(sig, RecoveryId::from_byte(0).unwrap()); if check_candidate(&signature, hash, pubkey) { return signature; @@ -223,6 +241,11 @@ fn sig_from_recovery(sig: ecdsa::Signature, hash: &B256, pubkey: &VerifyingKey) panic!("bad sig"); } +/// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. +fn check_candidate(signature: &Signature, hash: &B256, pubkey: &VerifyingKey) -> bool { + signature.recover_from_prehash(hash).map(|key| key == *pubkey).unwrap_or(false) +} + #[cfg(test)] mod tests { use super::*; @@ -234,10 +257,12 @@ mod tests { let config = aws_config::load_defaults(BehaviorVersion::latest()).await; let client = aws_sdk_kms::Client::new(&config); - let signer = AwsSigner::new(client, key_id).await.unwrap(); + let chain_id = 1; + let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap(); + + let message = vec![0, 1, 2, 3]; - let message = b"hello"; - let sig = signer.sign_message_async(message).await.unwrap(); + let sig = signer.sign_message_async(&message).await.unwrap(); assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); } } diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index fd6c21c36fa..ce62ba60d13 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -23,9 +23,20 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; pub struct LedgerSigner { transport: Mutex, derivation: DerivationType, + pub(crate) chain_id: u64, pub(crate) address: Address, } +impl std::fmt::Display for LedgerSigner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LedgerApp. Key at index {} with address {:?} on chain_id {}", + self.derivation, self.address, self.chain_id + ) + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for LedgerSigner { @@ -60,6 +71,16 @@ impl Signer for LedgerSigner { fn address(&self) -> Address { self.address } + + #[inline] + fn chain_id(&self) -> u64 { + self.chain_id + } + + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; + } } impl LedgerSigner { @@ -67,29 +88,27 @@ impl LedgerSigner { /// /// # Examples /// - /// ```no_run + /// ``` /// # async fn foo() -> Result<(), Box> { /// use alloy_signer_ledger::{HDPath, Ledger}; /// - /// let ledger = Ledger::new(HDPath::LedgerLive(0)).await?; + /// let ledger = Ledger::new(HDPath::LedgerLive(0), 1).await?; /// # Ok(()) /// # } /// ``` - #[instrument(err)] - pub async fn new(derivation: DerivationType) -> Result { + pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { let transport = Ledger::init().await?; let address = Self::get_address_with_path_transport(&transport, &derivation).await?; - Ok(Self { transport: Mutex::new(transport), derivation, address }) + + Ok(Self { transport: Mutex::new(transport), derivation, chain_id, address }) } - /// Returns the account that corresponds to the current device. - #[inline] + /// Get the account which corresponds to our derivation path pub async fn get_address(&self) -> Result { self.get_address_with_path(&self.derivation).await } - /// Returns the account that corresponds to the provided derivation path. - #[inline] + /// Gets the account which corresponds to the provided derivation path pub async fn get_address_with_path( &self, derivation: &DerivationType, @@ -155,9 +174,38 @@ impl LedgerSigner { /// Signs an Ethereum transaction (requires confirmation on the ledger) #[cfg(TODO)] pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { + let mut tx_with_chain = tx.clone(); + if tx_with_chain.chain_id().is_none() { + // in the case we don't have a chain_id, let's use the signer chain id instead + tx_with_chain.set_chain_id(self.chain_id); + } let mut payload = Self::path_to_bytes(&self.derivation); - payload.extend_from_slice(tx.rlp().as_ref()); + payload.extend_from_slice(tx_with_chain.rlp().as_ref()); + let mut signature = self.sign_payload(INS::SIGN, &payload).await?; + + // modify `v` value of signature to match EIP-155 for chains with large chain ID + // The logic is derived from Ledger's library + // https://github.com/LedgerHQ/ledgerjs/blob/e78aac4327e78301b82ba58d63a72476ecb842fc/packages/hw-app-eth/src/Eth.ts#L300 + let eip155_chain_id = self.chain_id * 2 + 35; + if eip155_chain_id + 1 > 255 { + let one_byte_chain_id = eip155_chain_id % 256; + let ecc_parity = if signature.v > one_byte_chain_id { + signature.v - one_byte_chain_id + } else { + one_byte_chain_id - signature.v + }; + + signature.v = match tx { + TypedTransaction::Eip2930(_) | TypedTransaction::Eip1559(_) => { + (ecc_parity % 2 != 1) as u64 + } + TypedTransaction::Legacy(_) => eip155_chain_id + ecc_parity, + #[cfg(feature = "optimism")] + TypedTransaction::DepositTransaction(_) => 0, + }; + } + Ok(signature) } @@ -187,7 +235,7 @@ impl LedgerSigner { /// Helper function for signing either transaction data, personal messages or EIP712 derived /// structs. - #[instrument(skip_all, fields(command = %command, payload = hex::encode(payload)), ret)] + #[instrument(err, skip_all, fields(command = %command, payload = hex::encode(payload)))] async fn sign_payload(&self, command: INS, payload: &[u8]) -> Result { let transport = self.transport.lock().await; let mut command = APDUCommand { diff --git a/crates/signer-ledger/src/types.rs b/crates/signer-ledger/src/types.rs index 5f014073a6e..8270e4053a9 100644 --- a/crates/signer-ledger/src/types.rs +++ b/crates/signer-ledger/src/types.rs @@ -8,8 +8,8 @@ use alloy_primitives::hex; use std::fmt; use thiserror::Error; -/// Ledger wallet type. #[derive(Clone, Debug)] +/// Ledger wallet type pub enum DerivationType { /// Ledger Live-generated HD path LedgerLive(usize), diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index 34cd5804d42..edb632abc00 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -18,6 +18,7 @@ const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1"; pub struct TrezorSigner { derivation: DerivationType, session_id: Vec, + pub(crate) chain_id: u64, pub(crate) address: Address, } @@ -49,14 +50,28 @@ impl Signer for TrezorSigner { fn address(&self) -> Address { self.address } + + #[inline] + fn chain_id(&self) -> u64 { + self.chain_id + } + + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; + } } impl TrezorSigner { /// Instantiates a new Trezor signer. #[instrument(ret)] - pub async fn new(derivation: DerivationType) -> Result { - let mut signer = - Self { derivation: derivation.clone(), address: Address::ZERO, session_id: vec![] }; + pub async fn new(derivation: DerivationType, chain_id: u64) -> Result { + let mut signer = Self { + derivation: derivation.clone(), + chain_id, + address: Address::ZERO, + session_id: vec![], + }; signer.initate_session()?; signer.address = signer.get_address_with_path(&derivation).await?; Ok(signer) @@ -128,8 +143,7 @@ impl TrezorSigner { let transaction = TrezorTransaction::load(tx)?; - // TODO: error when no chain ID? - let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(1); + let chain_id = tx.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); let signature = match tx { TypedTransaction::Eip2930(_) | TypedTransaction::Legacy(_) => client.ethereum_sign_tx( @@ -204,7 +218,7 @@ mod tests { // Replace this with your ETH addresses. async fn test_get_address() { // Instantiate it with the default trezor derivation path - let trezor = TrezorSigner::new(DerivationType::TrezorLive(1)).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(1), 1).await.unwrap(); assert_eq!( trezor.get_address().await.unwrap(), address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), @@ -218,7 +232,7 @@ mod tests { #[tokio::test] #[ignore] async fn test_sign_message() { - let trezor = TrezorSigner::new(DerivationType::TrezorLive(0)).await.unwrap(); + let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); let message = "hello world"; let sig = trezor.sign_message_async(message.as_bytes()).await.unwrap(); let addr = trezor.get_address().await.unwrap(); diff --git a/crates/signer/src/error.rs b/crates/signer/src/error.rs index 292177384ab..24b8fa36be3 100644 --- a/crates/signer/src/error.rs +++ b/crates/signer/src/error.rs @@ -12,6 +12,14 @@ pub enum Error { /// This operation is not supported by the signer. #[error("operation `{0}` is not supported by the signer")] UnsupportedOperation(UnsupportedSignerOperation), + /// Mismatch between provided transaction chain ID and signer chain ID. + #[error("transaction chain ID ({tx}) does not match the signer's ({signer})")] + TransactionChainIdMismatch { + /// The signer's chain ID. + signer: u64, + /// The chain ID provided by the transaction. + tx: u64, + }, /// [`ecdsa`] error. #[error(transparent)] Ecdsa(#[from] ecdsa::Error), diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 8dcb605c29c..2c4ac3f72a6 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -7,19 +7,13 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// Ethereum signer. /// -/// All provided implementations rely on [`sign_hash`] (or [`sign_hash_async`], which delegates to -/// [`sign_hash`]). If the signer is not able to implement this method, then all other methods will -/// have to be implemented directly, or they will return -/// [`UnsupportedOperation`](Error::UnsupportedOperation). -/// -/// [`sign_hash`]: Signer::sign_hash -/// [`sign_hash_async`]: Signer::sign_hash_async +/// All provided implementations rely on [`sign_hash`](Signer::sign_hash). If the signer is not able +/// to implement this method, then all other methods must be implemented directly, or they will +/// return [`UnsupportedOperation`](Error::UnsupportedOperation). #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait Signer: Send + Sync { /// Signs the hash. - /// - /// The default implementation returns [`UnsupportedOperation`](Error::UnsupportedOperation). #[inline] fn sign_hash(&self, hash: &B256) -> Result { let _ = hash; @@ -29,7 +23,7 @@ pub trait Signer: Send + Sync { /// Signs the hash. /// /// Asynchronous version of [`sign_hash`](Signer::sign_hash). The default implementation - /// delegates to the synchronous version; see its documentation for more details. + /// delegates to the synchronous version. #[inline] async fn sign_hash_async(&self, hash: &B256) -> Result { self.sign_hash(hash) @@ -45,50 +39,30 @@ pub trait Signer: Send + Sync { /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// - /// Asynchronous version of [`sign_message`](Signer::sign_message). The default - /// implementation is the same as the synchronous version; see its documentation for more - /// details. + /// Asynchronous version of [`sign_message`](Signer::sign_message). The default implementation + /// delegates to the synchronous version. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] async fn sign_message_async(&self, message: &[u8]) -> Result { - self.sign_hash_async(&eip191_hash_message(message)).await + self.sign_message(message) } /// Signs the transaction. - /// - /// The default implementation signs the [transaction's signature hash][sighash], and optionally - /// applies [EIP-155] to the signature if a chain ID is present. - /// - /// [sighash]: TypedTransaction::sighash - /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[cfg(TODO)] #[inline] - fn sign_transaction(&self, tx: &TypedTransaction) -> Result { - self.sign_hash(&tx.sighash()).map(|mut sig| { - if let Some(chain_id) = tx.chain_id() { - sig.apply_eip155(chain_id); - } - sig - }) + fn sign_transaction(&self, message: &TypedTransaction) -> Result { + self.sign_hash(&message.sighash()) } /// Signs the transaction. /// /// Asynchronous version of [`sign_transaction`](Signer::sign_transaction). The default - /// implementation is the same as the synchronous version; see its documentation for more - /// details. + /// implementation delegates to the synchronous version. #[cfg(TODO)] #[inline] async fn sign_transaction_async(&self, message: &TypedTransaction) -> Result { - self.sign_hash_async(&tx.sighash()) - .map(|mut sig| { - if let Some(chain_id) = tx.chain_id() { - sig.apply_eip155(chain_id); - } - sig - }) - .await + self.sign_transaction(message) } /// Encodes and signs the typed data according to [EIP-712]. @@ -110,8 +84,7 @@ pub trait Signer: Send + Sync { /// Encodes and signs the typed data according to [EIP-712]. /// /// Asynchronous version of [`sign_typed_data`](Signer::sign_typed_data). The default - /// implementation is the same as the synchronous version; see its documentation for more - /// details. + /// implementation delegates to the synchronous version. /// /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 #[cfg(feature = "eip712")] @@ -124,11 +97,28 @@ pub trait Signer: Send + Sync { where Self: Sized, { - self.sign_hash_async(&payload.eip712_signing_hash(domain)).await + self.sign_typed_data(payload, domain) } /// Returns the signer's Ethereum Address. fn address(&self) -> Address; + + /// Returns the signer's chain ID. + fn chain_id(&self) -> u64; + + /// Sets the signer's chain ID. + fn set_chain_id(&mut self, chain_id: u64); + + /// Sets the signer's chain ID and returns `self`. + #[inline] + #[must_use] + fn with_chain_id(mut self, chain_id: u64) -> Self + where + Self: Sized, + { + self.set_chain_id(chain_id); + self + } } #[cfg(test)] @@ -194,6 +184,14 @@ mod tests { fn address(&self) -> Address { unimplemented!() } + + fn chain_id(&self) -> u64 { + unimplemented!() + } + + fn set_chain_id(&mut self, _chain_id: u64) { + unimplemented!() + } } test_unimplemented_signer(&UnimplementedSigner).await; diff --git a/crates/signer/src/wallet/mnemonic.rs b/crates/signer/src/wallet/mnemonic.rs index b0f32517740..e07e12105cd 100644 --- a/crates/signer/src/wallet/mnemonic.rs +++ b/crates/signer/src/wallet/mnemonic.rs @@ -162,7 +162,8 @@ impl MnemonicBuilder { let key: &coins_bip32::prelude::SigningKey = derived_priv_key.as_ref(); let signer = SigningKey::from_bytes(&key.to_bytes())?; let address = secret_key_to_address(&signer); - Ok(Wallet::new_with_signer(signer, address)) + + Ok(Wallet:: { signer, address, chain_id: 1 }) } } diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index d71f3b3ac04..8f9a06f4f52 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -31,6 +31,10 @@ mod yubi; /// /// let wallet = LocalWallet::random(); /// +/// // Optionally, the wallet's chain id can be set, in order to use EIP-155 +/// // replay protection with different chains +/// let wallet = wallet.with_chain_id(1337u64); +/// /// // The wallet can be used to sign messages /// let message = b"hello"; /// let signature = wallet.sign_message(message)?; @@ -42,12 +46,14 @@ mod yubi; /// assert_eq!(signature, signature2); /// # Ok::<_, Box>(()) /// ``` -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone)] pub struct Wallet { /// The wallet's private key. pub(crate) signer: D, /// The wallet's address. pub(crate) address: Address, + /// The wallet's chain ID (for EIP-155). + pub(crate) chain_id: u64, } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -63,13 +69,23 @@ impl + Send + Sync> Signer for fn address(&self) -> Address { self.address } + + #[inline] + fn chain_id(&self) -> u64 { + self.chain_id + } + + #[inline] + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; + } } impl + Send + Sync> Wallet { /// Construct a new wallet with an external [`PrehashSigner`]. #[inline] - pub const fn new_with_signer(signer: D, address: Address) -> Self { - Wallet { signer, address } + pub const fn new_with_signer(signer: D, address: Address, chain_id: u64) -> Self { + Wallet { signer, address, chain_id } } /// Returns this wallet's signer. @@ -82,6 +98,9 @@ impl + Send + Sync> Wallet { // do not log the signer impl> fmt::Debug for Wallet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Wallet").field("address", &self.address).finish() + f.debug_struct("Wallet") + .field("address", &self.address) + .field("chain_id", &self.chain_id) + .finish() } } diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index 43e3a217c3e..2625611bae9 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -74,7 +74,7 @@ impl Wallet { #[inline] fn new_pk(signer: SigningKey) -> Self { let address = secret_key_to_address(&signer); - Wallet::new_with_signer(signer, address) + Self { signer, address, chain_id: 1 } } } @@ -135,6 +135,14 @@ impl Wallet { } } +impl PartialEq for Wallet { + fn eq(&self, other: &Self) -> bool { + self.signer.to_bytes().eq(&other.signer.to_bytes()) + && self.address == other.address + && self.chain_id == other.chain_id + } +} + impl From for Wallet { fn from(value: SigningKey) -> Self { Self::new_pk(value) @@ -406,7 +414,9 @@ mod tests { let key_as_bytes = wallet.signer.to_bytes(); let wallet_from_bytes = Wallet::from_bytes(&key_as_bytes).unwrap(); - assert_eq!(wallet, wallet_from_bytes); + assert_eq!(wallet.address, wallet_from_bytes.address); + assert_eq!(wallet.chain_id, wallet_from_bytes.chain_id); + assert_eq!(wallet.signer, wallet_from_bytes.signer); } #[test] @@ -417,7 +427,9 @@ mod tests { // Check FromStr and `0x` let wallet_0x: Wallet = "0x0000000000000000000000000000000000000000000000000000000000000001".parse().unwrap(); - assert_eq!(wallet, wallet_0x); + assert_eq!(wallet.address, wallet_0x.address); + assert_eq!(wallet.chain_id, wallet_0x.chain_id); + assert_eq!(wallet.signer, wallet_0x.signer); // Must fail because of `0z` "0z0000000000000000000000000000000000000000000000000000000000000001" diff --git a/crates/signer/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs index 07f6e5a4f09..3e19243a408 100644 --- a/crates/signer/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -59,7 +59,7 @@ impl From> for Wallet> { let bytes = pubkey.as_bytes(); debug_assert_eq!(bytes[0], 0x04); let address = raw_public_key_to_address(&bytes[1..]); - Self::new_with_signer(signer, address) + Self::new_with_signer(signer, address, 1) } } From 493538bedda37a1a8b3152fbee94891dd125179f Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:11:05 +0100 Subject: [PATCH 31/42] split signer into two traits --- crates/signer-ledger/src/signer.rs | 8 +- crates/signer-trezor/src/signer.rs | 8 +- crates/signer/README.md | 2 +- crates/signer/src/lib.rs | 2 +- crates/signer/src/signer.rs | 151 ++++++++++++------------ crates/signer/src/wallet/mod.rs | 18 ++- crates/signer/src/wallet/private_key.rs | 2 +- crates/signer/src/wallet/yubi.rs | 2 +- 8 files changed, 104 insertions(+), 89 deletions(-) diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index ce62ba60d13..24a8843f543 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -1,7 +1,7 @@ //! Ledger Ethereum app wrapper. use crate::types::{DerivationType, LedgerError, INS, P1, P1_FIRST, P2}; -use alloy_primitives::{hex, Address}; +use alloy_primitives::{hex, Address, B256}; use alloy_signer::{Result, Signature, Signer}; use async_trait::async_trait; use coins_ledger::{ @@ -40,6 +40,12 @@ impl std::fmt::Display for LedgerSigner { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for LedgerSigner { + async fn sign_hash_async(&self, _hash: &B256) -> Result { + Err(alloy_signer::Error::UnsupportedOperation( + alloy_signer::UnsupportedSignerOperation::SignHash, + )) + } + #[inline] async fn sign_message_async(&self, message: &[u8]) -> Result { let mut payload = Self::path_to_bytes(&self.derivation); diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index edb632abc00..6b791dafd1f 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -1,5 +1,5 @@ use super::types::{DerivationType, TrezorError}; -use alloy_primitives::{hex, Address, U256}; +use alloy_primitives::{hex, Address, B256, U256}; use alloy_signer::{Result, Signature, Signer}; use async_trait::async_trait; use std::fmt; @@ -35,6 +35,12 @@ impl fmt::Debug for TrezorSigner { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for TrezorSigner { + async fn sign_hash_async(&self, _hash: &B256) -> Result { + Err(alloy_signer::Error::UnsupportedOperation( + alloy_signer::UnsupportedSignerOperation::SignHash, + )) + } + #[inline] async fn sign_message_async(&self, message: &[u8]) -> Result { self.sign_message_(message).await.map_err(alloy_signer::Error::other) diff --git a/crates/signer/README.md b/crates/signer/README.md index a3f0b188b2d..93961831c88 100644 --- a/crates/signer/README.md +++ b/crates/signer/README.md @@ -42,7 +42,7 @@ signature.verify("hello world", wallet.address()).unwrap(); Sign an Ethereum prefixed message ([EIP-712](https://eips.ethereum.org/EIPS/eip-712)): ```rust -use alloy_signer::{LocalWallet, Signer}; +use alloy_signer::{LocalWallet, Signer, SignerSync}; let message = "Some data"; let wallet = LocalWallet::random(); diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index ea5d50bbd6a..989bd20c534 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -22,7 +22,7 @@ mod signature; pub use signature::Signature; mod signer; -pub use signer::Signer; +pub use signer::{Signer, SignerSync}; mod wallet; #[cfg(feature = "mnemonic")] diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 2c4ac3f72a6..26e5ab9d6fa 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -1,11 +1,11 @@ -use crate::{Error, Result, Signature, UnsupportedSignerOperation}; +use crate::{Result, Signature}; use alloy_primitives::{eip191_hash_message, Address, B256}; use async_trait::async_trait; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; -/// Ethereum signer. +/// Asynchronous Ethereum signer. /// /// All provided implementations rely on [`sign_hash`](Signer::sign_hash). If the signer is not able /// to implement this method, then all other methods must be implemented directly, or they will @@ -13,56 +13,22 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait Signer: Send + Sync { - /// Signs the hash. - #[inline] - fn sign_hash(&self, hash: &B256) -> Result { - let _ = hash; - Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) - } - - /// Signs the hash. - /// - /// Asynchronous version of [`sign_hash`](Signer::sign_hash). The default implementation - /// delegates to the synchronous version. - #[inline] - async fn sign_hash_async(&self, hash: &B256) -> Result { - self.sign_hash(hash) - } + /// Signs the given hash. + async fn sign_hash_async(&self, hash: &B256) -> Result; /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] - fn sign_message(&self, message: &[u8]) -> Result { - self.sign_hash(&eip191_hash_message(message)) - } - - /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. - /// - /// Asynchronous version of [`sign_message`](Signer::sign_message). The default implementation - /// delegates to the synchronous version. - /// - /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 - #[inline] async fn sign_message_async(&self, message: &[u8]) -> Result { - self.sign_message(message) + self.sign_hash_async(&eip191_hash_message(message)).await } /// Signs the transaction. #[cfg(TODO)] #[inline] - fn sign_transaction(&self, message: &TypedTransaction) -> Result { - self.sign_hash(&message.sighash()) - } - - /// Signs the transaction. - /// - /// Asynchronous version of [`sign_transaction`](Signer::sign_transaction). The default - /// implementation delegates to the synchronous version. - #[cfg(TODO)] - #[inline] async fn sign_transaction_async(&self, message: &TypedTransaction) -> Result { - self.sign_transaction(message) + self.sign_hash_async(&message.sighash()).await } /// Encodes and signs the typed data according to [EIP-712]. @@ -70,25 +36,6 @@ pub trait Signer: Send + Sync { /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 #[cfg(feature = "eip712")] #[inline] - fn sign_typed_data( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result - where - Self: Sized, - { - self.sign_hash(&payload.eip712_signing_hash(domain)) - } - - /// Encodes and signs the typed data according to [EIP-712]. - /// - /// Asynchronous version of [`sign_typed_data`](Signer::sign_typed_data). The default - /// implementation delegates to the synchronous version. - /// - /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 - #[cfg(feature = "eip712")] - #[inline] async fn sign_typed_data_async( &self, payload: &T, @@ -97,7 +44,7 @@ pub trait Signer: Send + Sync { where Self: Sized, { - self.sign_typed_data(payload, domain) + self.sign_hash_async(&payload.eip712_signing_hash(domain)).await } /// Returns the signer's Ethereum Address. @@ -121,12 +68,46 @@ pub trait Signer: Send + Sync { } } +/// Synchronous Ethereum signer. +pub trait SignerSync { + /// Signs the given hash. + fn sign_hash(&self, hash: &B256) -> Result; + + /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. + /// + /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 + #[inline] + fn sign_message(&self, message: &[u8]) -> Result { + self.sign_hash(&eip191_hash_message(message)) + } + + /// Signs the transaction. + #[cfg(TODO)] + #[inline] + fn sign_transaction(&self, message: &TypedTransaction) -> Result { + self.sign_hash(&message.sighash()) + } + + /// Encodes and signs the typed data according to [EIP-712]. + /// + /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 + #[cfg(feature = "eip712")] + #[inline] + fn sign_typed_data(&self, payload: &T, domain: &Eip712Domain) -> Result + where + Self: Sized, + { + self.sign_hash(&payload.eip712_signing_hash(domain)) + } +} + #[cfg(test)] mod tests { use super::*; + use crate::{Error, UnsupportedSignerOperation}; use assert_matches::assert_matches; - struct _ObjectSafe(Box); + struct _ObjectSafe(Box, Box); #[tokio::test] async fn unimplemented() { @@ -138,49 +119,58 @@ mod tests { } } - async fn test_unimplemented_signer(s: &S) { + async fn test_unimplemented_signer(s: &S) { test_unsized_unimplemented_signer(s).await; + test_unsized_unimplemented_signer_sync(s); #[cfg(feature = "eip712")] - { - assert!(s - .sign_typed_data(&Eip712Data::default(), &Eip712Domain::default()) - .is_err()); - assert!(s - .sign_typed_data_async(&Eip712Data::default(), &Eip712Domain::default()) - .await - .is_err()); - } + assert!(s.sign_typed_data(&Eip712Data::default(), &Eip712Domain::default()).is_err()); + #[cfg(feature = "eip712")] + assert!(s + .sign_typed_data_async(&Eip712Data::default(), &Eip712Domain::default()) + .await + .is_err()); } async fn test_unsized_unimplemented_signer(s: &S) { assert_matches!( - s.sign_hash(&B256::ZERO), + s.sign_hash_async(&B256::ZERO).await, Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); + assert_matches!( - s.sign_hash_async(&B256::ZERO).await, + s.sign_message_async(&[]).await, Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); + #[cfg(TODO)] + assert!(s.sign_transaction_async(&TypedTransaction::default()).await.is_err()); + } + + fn test_unsized_unimplemented_signer_sync(s: &S) { assert_matches!( - s.sign_message(&[]), + s.sign_hash(&B256::ZERO), Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); + assert_matches!( - s.sign_message_async(&[]).await, + s.sign_message(&[]), Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); #[cfg(TODO)] assert!(s.sign_transaction(&TypedTransaction::default()).is_err()); - #[cfg(TODO)] - assert!(s.sign_transaction_async(&TypedTransaction::default()).await.is_err()); } struct UnimplementedSigner; + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] + #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for UnimplementedSigner { + async fn sign_hash_async(&self, _hash: &B256) -> Result { + Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) + } + fn address(&self) -> Address { unimplemented!() } @@ -194,7 +184,14 @@ mod tests { } } + impl SignerSync for UnimplementedSigner { + fn sign_hash(&self, _hash: &B256) -> Result { + Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) + } + } + test_unimplemented_signer(&UnimplementedSigner).await; test_unsized_unimplemented_signer(&UnimplementedSigner as &dyn Signer).await; + test_unsized_unimplemented_signer_sync(&UnimplementedSigner as &dyn SignerSync); } } diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 8f9a06f4f52..009137ebcf3 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -1,4 +1,4 @@ -use crate::{Result, Signature, Signer}; +use crate::{Result, Signature, Signer, SignerSync}; use alloy_primitives::{Address, B256}; use async_trait::async_trait; use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}; @@ -27,7 +27,7 @@ mod yubi; /// prefix the message being hashed with the `Ethereum Signed Message` domain separator. /// /// ``` -/// use alloy_signer::{LocalWallet, Signer}; +/// use alloy_signer::{LocalWallet, Signer, SignerSync}; /// /// let wallet = LocalWallet::random(); /// @@ -59,10 +59,8 @@ pub struct Wallet { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl + Send + Sync> Signer for Wallet { - #[inline] - fn sign_hash(&self, hash: &B256) -> Result { - let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; - Ok(Signature::new(recoverable_sig, recovery_id)) + async fn sign_hash_async(&self, hash: &B256) -> Result { + self.sign_hash(hash) } #[inline] @@ -81,6 +79,14 @@ impl + Send + Sync> Signer for } } +impl> SignerSync for Wallet { + #[inline] + fn sign_hash(&self, hash: &B256) -> Result { + let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; + Ok(Signature::new(recoverable_sig, recovery_id)) + } +} + impl + Send + Sync> Wallet { /// Construct a new wallet with an external [`PrehashSigner`]. #[inline] diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index 2625611bae9..31cc484af27 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -167,7 +167,7 @@ impl FromStr for Wallet { #[cfg(test)] mod tests { use super::*; - use crate::{LocalWallet, Signer}; + use crate::{LocalWallet, SignerSync}; use alloy_primitives::address; #[cfg(feature = "keystore")] diff --git a/crates/signer/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs index 3e19243a408..182883b76f1 100644 --- a/crates/signer/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -66,7 +66,7 @@ impl From> for Wallet> { #[cfg(test)] mod tests { use super::*; - use crate::Signer; + use crate::{Signer, SignerSync}; use alloy_primitives::{address, hex}; #[test] From 579717db1eff14f3081b1a0c806c301fb404fe45 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:18:04 +0100 Subject: [PATCH 32/42] make async the default method name --- crates/signer-aws/src/signer.rs | 8 ++-- crates/signer-ledger/src/signer.rs | 8 ++-- crates/signer-trezor/src/signer.rs | 8 ++-- crates/signer/README.md | 2 +- crates/signer/src/signer.rs | 54 ++++++++++++++----------- crates/signer/src/wallet/mod.rs | 10 ++--- crates/signer/src/wallet/private_key.rs | 10 ++--- crates/signer/src/wallet/yubi.rs | 4 +- 8 files changed, 55 insertions(+), 49 deletions(-) diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index 44216ff9bbf..16c3b671f30 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -42,7 +42,7 @@ use std::fmt; /// /// let message = vec![0, 1, 2, 3]; /// -/// let sig = signer.sign_message_async(&message).await.unwrap(); +/// let sig = signer.sign_message(&message).await.unwrap(); /// assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); /// # } /// ``` @@ -96,13 +96,13 @@ pub enum AwsSignerError { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for AwsSigner { #[instrument(err)] - async fn sign_hash_async(&self, hash: &B256) -> Result { + async fn sign_hash(&self, hash: &B256) -> Result { self.sign_digest_with_eip155(hash, self.chain_id).await.map_err(alloy_signer::Error::other) } #[cfg(TODO)] #[instrument(err)] - async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { + async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { let mut tx_with_chain = tx.clone(); let chain_id = tx_with_chain.chain_id().map(|id| id.as_u64()).unwrap_or(self.chain_id); tx_with_chain.set_chain_id(chain_id); @@ -262,7 +262,7 @@ mod tests { let message = vec![0, 1, 2, 3]; - let sig = signer.sign_message_async(&message).await.unwrap(); + let sig = signer.sign_message(&message).await.unwrap(); assert_eq!(sig.recover_address_from_msg(message).unwrap(), signer.address()); } } diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 24a8843f543..9da9b7dc117 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -40,14 +40,14 @@ impl std::fmt::Display for LedgerSigner { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for LedgerSigner { - async fn sign_hash_async(&self, _hash: &B256) -> Result { + async fn sign_hash(&self, _hash: &B256) -> Result { Err(alloy_signer::Error::UnsupportedOperation( alloy_signer::UnsupportedSignerOperation::SignHash, )) } #[inline] - async fn sign_message_async(&self, message: &[u8]) -> Result { + async fn sign_message(&self, message: &[u8]) -> Result { let mut payload = Self::path_to_bytes(&self.derivation); payload.extend_from_slice(&(message.len() as u32).to_be_bytes()); payload.extend_from_slice(message); @@ -59,13 +59,13 @@ impl Signer for LedgerSigner { #[cfg(TODO)] #[inline] - async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { + async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { self.sign_tx(&tx).await.map_err(alloy_signer::Error::other) } #[cfg(feature = "eip712")] #[inline] - async fn sign_typed_data_async( + async fn sign_typed_data( &self, payload: &T, domain: &Eip712Domain, diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index 6b791dafd1f..a841ff2d137 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -35,20 +35,20 @@ impl fmt::Debug for TrezorSigner { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for TrezorSigner { - async fn sign_hash_async(&self, _hash: &B256) -> Result { + async fn sign_hash(&self, _hash: &B256) -> Result { Err(alloy_signer::Error::UnsupportedOperation( alloy_signer::UnsupportedSignerOperation::SignHash, )) } #[inline] - async fn sign_message_async(&self, message: &[u8]) -> Result { + async fn sign_message(&self, message: &[u8]) -> Result { self.sign_message_(message).await.map_err(alloy_signer::Error::other) } #[cfg(TODO)] #[inline] - async fn sign_transaction_async(&self, tx: &TypedTransaction) -> Result { + async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { self.sign_tx(tx).await } @@ -240,7 +240,7 @@ mod tests { async fn test_sign_message() { let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); let message = "hello world"; - let sig = trezor.sign_message_async(message.as_bytes()).await.unwrap(); + let sig = trezor.sign_message(message.as_bytes()).await.unwrap(); let addr = trezor.get_address().await.unwrap(); assert_eq!(sig.recover_address_from_msg(message).unwrap(), addr); } diff --git a/crates/signer/README.md b/crates/signer/README.md index 93961831c88..81a7718c45b 100644 --- a/crates/signer/README.md +++ b/crates/signer/README.md @@ -48,7 +48,7 @@ let message = "Some data"; let wallet = LocalWallet::random(); // Sign the message -let signature = wallet.sign_message(message.as_bytes())?; +let signature = wallet.sign_message_sync(message.as_bytes())?; // Recover the signer from the message let recovered = signature.recover_address_from_msg(message)?; diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 26e5ab9d6fa..daeade3815e 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -14,21 +14,21 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait Signer: Send + Sync { /// Signs the given hash. - async fn sign_hash_async(&self, hash: &B256) -> Result; + async fn sign_hash(&self, hash: &B256) -> Result; /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] - async fn sign_message_async(&self, message: &[u8]) -> Result { - self.sign_hash_async(&eip191_hash_message(message)).await + async fn sign_message(&self, message: &[u8]) -> Result { + self.sign_hash(&eip191_hash_message(message)).await } /// Signs the transaction. #[cfg(TODO)] #[inline] - async fn sign_transaction_async(&self, message: &TypedTransaction) -> Result { - self.sign_hash_async(&message.sighash()).await + async fn sign_transaction(&self, message: &TypedTransaction) -> Result { + self.sign_hash(&message.sighash()).await } /// Encodes and signs the typed data according to [EIP-712]. @@ -36,7 +36,7 @@ pub trait Signer: Send + Sync { /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 #[cfg(feature = "eip712")] #[inline] - async fn sign_typed_data_async( + async fn sign_typed_data( &self, payload: &T, domain: &Eip712Domain, @@ -44,7 +44,7 @@ pub trait Signer: Send + Sync { where Self: Sized, { - self.sign_hash_async(&payload.eip712_signing_hash(domain)).await + self.sign_hash(&payload.eip712_signing_hash(domain)).await } /// Returns the signer's Ethereum Address. @@ -71,21 +71,21 @@ pub trait Signer: Send + Sync { /// Synchronous Ethereum signer. pub trait SignerSync { /// Signs the given hash. - fn sign_hash(&self, hash: &B256) -> Result; + fn sign_hash_sync(&self, hash: &B256) -> Result; /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] - fn sign_message(&self, message: &[u8]) -> Result { - self.sign_hash(&eip191_hash_message(message)) + fn sign_message_sync(&self, message: &[u8]) -> Result { + self.sign_hash_sync(&eip191_hash_message(message)) } /// Signs the transaction. #[cfg(TODO)] #[inline] - fn sign_transaction(&self, message: &TypedTransaction) -> Result { - self.sign_hash(&message.sighash()) + fn sign_transaction_sync(&self, message: &TypedTransaction) -> Result { + self.sign_hash_sync(&message.sighash()) } /// Encodes and signs the typed data according to [EIP-712]. @@ -93,11 +93,15 @@ pub trait SignerSync { /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 #[cfg(feature = "eip712")] #[inline] - fn sign_typed_data(&self, payload: &T, domain: &Eip712Domain) -> Result + fn sign_typed_data_sync( + &self, + payload: &T, + domain: &Eip712Domain, + ) -> Result where Self: Sized, { - self.sign_hash(&payload.eip712_signing_hash(domain)) + self.sign_hash_sync(&payload.eip712_signing_hash(domain)) } } @@ -124,42 +128,44 @@ mod tests { test_unsized_unimplemented_signer_sync(s); #[cfg(feature = "eip712")] - assert!(s.sign_typed_data(&Eip712Data::default(), &Eip712Domain::default()).is_err()); + assert!(s + .sign_typed_data_sync(&Eip712Data::default(), &Eip712Domain::default()) + .is_err()); #[cfg(feature = "eip712")] assert!(s - .sign_typed_data_async(&Eip712Data::default(), &Eip712Domain::default()) + .sign_typed_data(&Eip712Data::default(), &Eip712Domain::default()) .await .is_err()); } async fn test_unsized_unimplemented_signer(s: &S) { assert_matches!( - s.sign_hash_async(&B256::ZERO).await, + s.sign_hash(&B256::ZERO).await, Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); assert_matches!( - s.sign_message_async(&[]).await, + s.sign_message(&[]).await, Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); #[cfg(TODO)] - assert!(s.sign_transaction_async(&TypedTransaction::default()).await.is_err()); + assert!(s.sign_transaction(&TypedTransaction::default()).await.is_err()); } fn test_unsized_unimplemented_signer_sync(s: &S) { assert_matches!( - s.sign_hash(&B256::ZERO), + s.sign_hash_sync(&B256::ZERO), Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); assert_matches!( - s.sign_message(&[]), + s.sign_message_sync(&[]), Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); #[cfg(TODO)] - assert!(s.sign_transaction(&TypedTransaction::default()).is_err()); + assert!(s.sign_transaction_sync(&TypedTransaction::default()).is_err()); } struct UnimplementedSigner; @@ -167,7 +173,7 @@ mod tests { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for UnimplementedSigner { - async fn sign_hash_async(&self, _hash: &B256) -> Result { + async fn sign_hash(&self, _hash: &B256) -> Result { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) } @@ -185,7 +191,7 @@ mod tests { } impl SignerSync for UnimplementedSigner { - fn sign_hash(&self, _hash: &B256) -> Result { + fn sign_hash_sync(&self, _hash: &B256) -> Result { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) } } diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 009137ebcf3..6eac8ada5b3 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -37,12 +37,12 @@ mod yubi; /// /// // The wallet can be used to sign messages /// let message = b"hello"; -/// let signature = wallet.sign_message(message)?; +/// let signature = wallet.sign_message_sync(message)?; /// assert_eq!(signature.recover_address_from_msg(&message[..]).unwrap(), wallet.address()); /// /// // LocalWallet is clonable: /// let wallet_clone = wallet.clone(); -/// let signature2 = wallet_clone.sign_message(message)?; +/// let signature2 = wallet_clone.sign_message_sync(message)?; /// assert_eq!(signature, signature2); /// # Ok::<_, Box>(()) /// ``` @@ -59,8 +59,8 @@ pub struct Wallet { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl + Send + Sync> Signer for Wallet { - async fn sign_hash_async(&self, hash: &B256) -> Result { - self.sign_hash(hash) + async fn sign_hash(&self, hash: &B256) -> Result { + self.sign_hash_sync(hash) } #[inline] @@ -81,7 +81,7 @@ impl + Send + Sync> Signer for impl> SignerSync for Wallet { #[inline] - fn sign_hash(&self, hash: &B256) -> Result { + fn sign_hash_sync(&self, hash: &B256) -> Result { let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; Ok(Signature::new(recoverable_sig, recovery_id)) } diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index 31cc484af27..623515868ed 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -194,14 +194,14 @@ mod tests { fn test_encrypted_json_keystore(key: Wallet, uuid: &str, dir: &Path) { // sign a message using the given key let message = "Some data"; - let signature = key.sign_message(message.as_bytes()).unwrap(); + let signature = key.sign_message_sync(message.as_bytes()).unwrap(); // read from the encrypted JSON keystore and decrypt it, while validating that the // signatures produced by both the keys should match let path = Path::new(dir).join(uuid); let key2 = Wallet::::decrypt_keystore(path.clone(), "randpsswd").unwrap(); - let signature2 = key2.sign_message(message.as_bytes()).unwrap(); + let signature2 = key2.sign_message_sync(message.as_bytes()).unwrap(); assert_eq!(signature, signature2); std::fs::remove_file(&path).unwrap(); @@ -245,7 +245,7 @@ mod tests { let address = key.address; // sign a message - let signature = key.sign_message(message.as_bytes()).unwrap(); + let signature = key.sign_message_sync(message.as_bytes()).unwrap(); // ecrecover via the message will hash internally let recovered = signature.recover_address_from_msg(message).unwrap(); @@ -386,9 +386,9 @@ mod tests { }; let wallet = Wallet::random(); let hash = foo_bar.eip712_signing_hash(&domain); - let sig = wallet.sign_typed_data(&foo_bar, &domain).unwrap(); + let sig = wallet.sign_typed_data_sync(&foo_bar, &domain).unwrap(); assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), wallet.address()); - assert_eq!(wallet.sign_hash(&hash).unwrap(), sig); + assert_eq!(wallet.sign_hash_sync(&hash).unwrap(), sig); } #[test] diff --git a/crates/signer/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs index 182883b76f1..e430d27aaa6 100644 --- a/crates/signer/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -85,7 +85,7 @@ mod tests { ); let msg = "Some data"; - let sig = wallet.sign_message(msg.as_bytes()).unwrap(); + let sig = wallet.sign_message_sync(msg.as_bytes()).unwrap(); assert_eq!(sig.recover_address_from_msg(msg).unwrap(), wallet.address()); assert_eq!(wallet.address(), address!("2DE2C386082Cff9b28D62E60983856CE1139eC49")); } @@ -102,7 +102,7 @@ mod tests { ); let msg = "Some data"; - let sig = wallet.sign_message(msg.as_bytes()).unwrap(); + let sig = wallet.sign_message_sync(msg.as_bytes()).unwrap(); assert_eq!(sig.recover_address_from_msg(msg).unwrap(), wallet.address()); } } From f814a92a8dc5dd273458193574bf6cdfcd319991 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:41:16 +0100 Subject: [PATCH 33/42] docs --- crates/signer/src/signer.rs | 17 ++++++++++++++--- crates/signer/src/wallet/mod.rs | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index daeade3815e..36caa734403 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -7,9 +7,12 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// Asynchronous Ethereum signer. /// -/// All provided implementations rely on [`sign_hash`](Signer::sign_hash). If the signer is not able -/// to implement this method, then all other methods must be implemented directly, or they will -/// return [`UnsupportedOperation`](Error::UnsupportedOperation). +/// All provided implementations rely on [`sign_hash`](Signer::sign_hash). A signer may not always +/// be able to implement this method, in which case it should return +/// [`UnsupportedOperation`](crate::Error::UnsupportedOperation), and implement all the signing +/// methods directly. +/// +/// Synchronous signers should implement both this trait and [`SignerSync`]. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait Signer: Send + Sync { @@ -69,6 +72,14 @@ pub trait Signer: Send + Sync { } /// Synchronous Ethereum signer. +/// +/// All provided implementations rely on [`sign_hash_sync`](SignerSync::sign_hash_sync). A signer +/// may not always be able to implement this method, in which case it should return +/// [`UnsupportedOperation`](crate::Error::UnsupportedOperation), and implement all the signing +/// methods directly. +/// +/// Synchronous signers should also implement [`Signer`], as they are always able to by delegating +/// the asynchronous methods to the synchronous ones. pub trait SignerSync { /// Signs the given hash. fn sign_hash_sync(&self, hash: &B256) -> Result; diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 6eac8ada5b3..ed44b100953 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -59,6 +59,7 @@ pub struct Wallet { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl + Send + Sync> Signer for Wallet { + #[inline] async fn sign_hash(&self, hash: &B256) -> Result { self.sign_hash_sync(hash) } From d701a79a1f87a5a638fe90a3c1bfa5b2c1b52a3b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:43:34 +0100 Subject: [PATCH 34/42] make signature copy --- crates/signer/src/signature.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/signer/src/signature.rs b/crates/signer/src/signature.rs index 9bb3112c1da..9cdc598f2d1 100644 --- a/crates/signer/src/signature.rs +++ b/crates/signer/src/signature.rs @@ -11,8 +11,7 @@ use std::str::FromStr; /// /// This is a wrapper around [`ecdsa::Signature`] and a [`RecoveryId`] to provide public key /// recovery functionality. -#[derive(Clone, Debug, PartialEq, Eq)] -#[allow(missing_copy_implementations)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Signature { /// The inner ECDSA signature. inner: ecdsa::Signature, From 61911f6f71af5e5fd5ce9336dcbbd55690aee513 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:47:38 +0100 Subject: [PATCH 35/42] wallet error module --- crates/signer/src/signature.rs | 6 ++-- crates/signer/src/wallet/error.rs | 35 +++++++++++++++++++++++ crates/signer/src/wallet/mod.rs | 4 ++- crates/signer/src/wallet/private_key.rs | 37 ++----------------------- 4 files changed, 43 insertions(+), 39 deletions(-) create mode 100644 crates/signer/src/wallet/error.rs diff --git a/crates/signer/src/signature.rs b/crates/signer/src/signature.rs index 9cdc598f2d1..b2d9e57be47 100644 --- a/crates/signer/src/signature.rs +++ b/crates/signer/src/signature.rs @@ -208,8 +208,8 @@ impl Signature { self.set_v(to_eip155_v(self.recid.to_byte(), chain_id)); } - /// Recovers a [`VerifyingKey`] from this signature and the given message by first hashing the - /// message with Keccak-256. + /// Recovers an [`Address`] from this signature and the given message by first prefixing and + /// hashing the message according to [EIP-191](eip191_hash_message). #[inline] pub fn recover_address_from_msg>( &self, @@ -218,7 +218,7 @@ impl Signature { self.recover_from_msg(msg).map(|pubkey| public_key_to_address(&pubkey)) } - /// Recovers a [`VerifyingKey`] from this signature and the given prehashed message. + /// Recovers an [`Address`] from this signature and the given prehashed message. #[inline] pub fn recover_address_from_prehash(&self, prehash: &B256) -> Result { self.recover_from_prehash(prehash).map(|pubkey| public_key_to_address(&pubkey)) diff --git a/crates/signer/src/wallet/error.rs b/crates/signer/src/wallet/error.rs new file mode 100644 index 00000000000..3fc7e28cf93 --- /dev/null +++ b/crates/signer/src/wallet/error.rs @@ -0,0 +1,35 @@ +use alloy_primitives::hex; +use k256::ecdsa; +use thiserror::Error; + +/// Error thrown by the Wallet module. +#[derive(Debug, Error)] +pub enum WalletError { + /// [`ecdsa`] error. + #[error(transparent)] + EcdsaError(#[from] ecdsa::Error), + /// [`hex`](mod@hex) error. + #[error(transparent)] + HexError(#[from] hex::FromHexError), + /// [`std::io`] error. + #[error(transparent)] + IoError(#[from] std::io::Error), + + /// [`coins_bip32`] error. + #[error(transparent)] + #[cfg(feature = "mnemonic")] + Bip32Error(#[from] coins_bip32::Bip32Error), + /// [`coins_bip39`] error. + #[error(transparent)] + #[cfg(feature = "mnemonic")] + Bip39Error(#[from] coins_bip39::MnemonicError), + /// [`mnemonic`](super::mnemonic) error. + #[error(transparent)] + #[cfg(feature = "mnemonic")] + MnemonicBuilderError(#[from] super::mnemonic::MnemonicBuilderError), + + /// [`eth_keystore`] error. + #[cfg(feature = "keystore")] + #[error(transparent)] + EthKeystoreError(#[from] eth_keystore::KeystoreError), +} diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index ed44b100953..1bc067625e3 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -4,13 +4,15 @@ use async_trait::async_trait; use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}; use std::fmt; +mod error; +pub use error::WalletError; + #[cfg(feature = "mnemonic")] mod mnemonic; #[cfg(feature = "mnemonic")] pub use mnemonic::MnemonicBuilder; mod private_key; -pub use private_key::WalletError; #[cfg(feature = "yubihsm")] mod yubi; diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index 623515868ed..f5e662f6ce4 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -1,6 +1,6 @@ //! Specific helper functions for loading an offline K256 Private Key stored on disk -use super::Wallet; +use super::{Wallet, WalletError}; use crate::utils::secret_key_to_address; use alloy_primitives::hex; use k256::{ @@ -9,42 +9,9 @@ use k256::{ }; use rand::{CryptoRng, Rng}; use std::str::FromStr; -use thiserror::Error; #[cfg(feature = "keystore")] -use {elliptic_curve::rand_core, eth_keystore::KeystoreError, std::path::Path}; - -/// Error thrown by the Wallet module -#[derive(Debug, Error)] -pub enum WalletError { - /// Error propagated from k256's ECDSA module - #[error(transparent)] - EcdsaError(#[from] ecdsa::Error), - /// Error propagated from the hex crate. - #[error(transparent)] - HexError(#[from] hex::FromHexError), - /// Error propagated by IO operations - #[error(transparent)] - IoError(#[from] std::io::Error), - - /// Error propagated from the BIP-32 crate - #[error(transparent)] - #[cfg(feature = "mnemonic")] - Bip32Error(#[from] coins_bip32::Bip32Error), - /// Error propagated from the BIP-39 crate - #[error(transparent)] - #[cfg(feature = "mnemonic")] - Bip39Error(#[from] coins_bip39::MnemonicError), - /// Error propagated from the mnemonic builder module. - #[error(transparent)] - #[cfg(feature = "mnemonic")] - MnemonicBuilderError(#[from] super::mnemonic::MnemonicBuilderError), - - /// Underlying eth keystore error - #[cfg(feature = "keystore")] - #[error(transparent)] - EthKeystoreError(#[from] KeystoreError), -} +use {elliptic_curve::rand_core, std::path::Path}; impl Wallet { /// Creates a new Wallet instance from a raw scalar serialized as a byte array. From 05d19783444ca47d0cdb98bd4385a68bcd801091 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:18:42 +0100 Subject: [PATCH 36/42] docs --- crates/signer/src/wallet/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/signer/src/wallet/error.rs b/crates/signer/src/wallet/error.rs index 3fc7e28cf93..f62dc2c7fba 100644 --- a/crates/signer/src/wallet/error.rs +++ b/crates/signer/src/wallet/error.rs @@ -23,7 +23,7 @@ pub enum WalletError { #[error(transparent)] #[cfg(feature = "mnemonic")] Bip39Error(#[from] coins_bip39::MnemonicError), - /// [`mnemonic`](super::mnemonic) error. + /// [`MnemonicBuilder`](super::mnemonic::MnemonicBuilder) error. #[error(transparent)] #[cfg(feature = "mnemonic")] MnemonicBuilderError(#[from] super::mnemonic::MnemonicBuilderError), From 15bee3b54e983f6ae09cbabdea3666464ccc8df9 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 7 Dec 2023 21:04:26 +0100 Subject: [PATCH 37/42] update ledger tests --- crates/signer-ledger/src/signer.rs | 44 +++++++++++++++++------------- crates/signer/src/signer.rs | 4 +-- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 9da9b7dc117..d3a20875d67 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -307,33 +307,38 @@ impl LedgerSigner { } } -#[cfg(all(test, feature = "ledger"))] +#[cfg(test)] mod tests { use super::*; - use crate::Signer; - use alloy_primitives::{hex, Address, I256, U256}; - use std::str::FromStr; + use alloy_primitives::address; + + // Replace this with your ETH address. + const MY_ADDRESS: Address = address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); + const DTYPE: DerivationType = DerivationType::LedgerLive(0); + + async fn init_ledger() -> LedgerSigner { + match LedgerSigner::new(DTYPE, 1).await { + Ok(ledger) => ledger, + Err(e) => panic!("{e:?}\n{e}"), + } + } #[tokio::test] #[ignore] - // Replace this with your ETH addresses. async fn test_get_address() { - // Instantiate it with the default ledger derivation path - let ledger = LedgerSigner::new(DerivationType::LedgerLive(0), 1).await.unwrap(); - assert_eq!( - ledger.get_address().await.unwrap(), - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() - ); + let ledger = init_ledger().await; + assert_eq!(ledger.get_address().await.unwrap(), MY_ADDRESS); assert_eq!( ledger.get_address_with_path(&DerivationType::Legacy(0)).await.unwrap(), - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap() + MY_ADDRESS, ); } #[tokio::test] #[ignore] + #[cfg(TODO)] async fn test_sign_tx() { - let ledger = LedgerSigner::new(DerivationType::LedgerLive(0), 1).await.unwrap(); + let ledger = init_ledger().await; // approve uni v2 router 0xff let data = hex::decode("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(); @@ -352,19 +357,20 @@ mod tests { #[tokio::test] #[ignore] async fn test_version() { - let ledger = LedgerSigner::new(DerivationType::LedgerLive(0), 1).await.unwrap(); - + let ledger = init_ledger().await; let version = ledger.version().await.unwrap(); - assert_eq!(version, "1.3.7"); + eprintln!("{version}"); + assert!(version.major >= 1); } #[tokio::test] #[ignore] async fn test_sign_message() { - let ledger = LedgerSigner::new(DerivationType::Legacy(0), 1).await.unwrap(); + let ledger = init_ledger().await; let message = "hello world"; - let sig = ledger.sign_message(message).await.unwrap(); + let sig = ledger.sign_message(message.as_bytes()).await.unwrap(); let addr = ledger.get_address().await.unwrap(); - sig.verify(message, addr).unwrap(); + assert_eq!(addr, MY_ADDRESS); + assert_eq!(sig.recover_address_from_msg(message.as_bytes()).unwrap(), MY_ADDRESS); } } diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 36caa734403..85eb85d3cd0 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -161,7 +161,7 @@ mod tests { ); #[cfg(TODO)] - assert!(s.sign_transaction(&TypedTransaction::default()).await.is_err()); + assert!(s.sign_transaction(&Default::default()).await.is_err()); } fn test_unsized_unimplemented_signer_sync(s: &S) { @@ -176,7 +176,7 @@ mod tests { ); #[cfg(TODO)] - assert!(s.sign_transaction_sync(&TypedTransaction::default()).is_err()); + assert!(s.sign_transaction_sync(&Default::default()).is_err()); } struct UnimplementedSigner; From e58bcaeb24b0dbafec22d31330d9d9baca841af2 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 7 Dec 2023 21:07:22 +0100 Subject: [PATCH 38/42] update ledger tests again --- crates/signer-ledger/src/signer.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index d3a20875d67..96c1a713b73 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -310,13 +310,13 @@ impl LedgerSigner { #[cfg(test)] mod tests { use super::*; - use alloy_primitives::address; - // Replace this with your ETH address. - const MY_ADDRESS: Address = address!("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); - const DTYPE: DerivationType = DerivationType::LedgerLive(0); + fn my_address() -> Address { + std::env::var("LEDGER_ADDRESS").unwrap().parse().unwrap() + } async fn init_ledger() -> LedgerSigner { + const DTYPE: DerivationType = DerivationType::LedgerLive(0); match LedgerSigner::new(DTYPE, 1).await { Ok(ledger) => ledger, Err(e) => panic!("{e:?}\n{e}"), @@ -327,10 +327,10 @@ mod tests { #[ignore] async fn test_get_address() { let ledger = init_ledger().await; - assert_eq!(ledger.get_address().await.unwrap(), MY_ADDRESS); + assert_eq!(ledger.get_address().await.unwrap(), my_address()); assert_eq!( ledger.get_address_with_path(&DerivationType::Legacy(0)).await.unwrap(), - MY_ADDRESS, + my_address(), ); } @@ -370,7 +370,7 @@ mod tests { let message = "hello world"; let sig = ledger.sign_message(message.as_bytes()).await.unwrap(); let addr = ledger.get_address().await.unwrap(); - assert_eq!(addr, MY_ADDRESS); - assert_eq!(sig.recover_address_from_msg(message.as_bytes()).unwrap(), MY_ADDRESS); + assert_eq!(addr, my_address()); + assert_eq!(sig.recover_address_from_msg(message.as_bytes()).unwrap(), my_address()); } } From a36d51a4091fb8b80dfd610bb131324ae4f2b902 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 7 Dec 2023 21:08:57 +0100 Subject: [PATCH 39/42] use serial --- Cargo.toml | 1 + crates/signer-ledger/Cargo.toml | 1 + crates/signer-ledger/src/signer.rs | 22 +++++++++++++--------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5c36012f436..e4a4e9681a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,5 +68,6 @@ serde_json = "1.0" serde_with = "3.4" home = "0.5" semver = "1.0" +serial_test = "2.0" thiserror = "1.0" url = "2.4" diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml index a7923400b63..9bbd0d597f5 100644 --- a/crates/signer-ledger/Cargo.toml +++ b/crates/signer-ledger/Cargo.toml @@ -27,6 +27,7 @@ alloy-sol-types = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +serial_test.workspace = true [features] eip712 = ["alloy-signer/eip712", "dep:alloy-sol-types"] diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 96c1a713b73..efbd03a5e6c 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -324,6 +324,7 @@ mod tests { } #[tokio::test] + #[serial_test::serial] #[ignore] async fn test_get_address() { let ledger = init_ledger().await; @@ -335,6 +336,17 @@ mod tests { } #[tokio::test] + #[serial_test::serial] + #[ignore] + async fn test_version() { + let ledger = init_ledger().await; + let version = ledger.version().await.unwrap(); + eprintln!("{version}"); + assert!(version.major >= 1); + } + + #[tokio::test] + #[serial_test::serial] #[ignore] #[cfg(TODO)] async fn test_sign_tx() { @@ -355,15 +367,7 @@ mod tests { } #[tokio::test] - #[ignore] - async fn test_version() { - let ledger = init_ledger().await; - let version = ledger.version().await.unwrap(); - eprintln!("{version}"); - assert!(version.major >= 1); - } - - #[tokio::test] + #[serial_test::serial] #[ignore] async fn test_sign_message() { let ledger = init_ledger().await; From 4cdc70685e14c909614a08bf921ffe5f181f7516 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Thu, 7 Dec 2023 21:10:45 +0100 Subject: [PATCH 40/42] fix --- crates/signer-ledger/src/signer.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index efbd03a5e6c..910453ca812 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -311,12 +311,13 @@ impl LedgerSigner { mod tests { use super::*; + const DTYPE: DerivationType = DerivationType::LedgerLive(0); + fn my_address() -> Address { std::env::var("LEDGER_ADDRESS").unwrap().parse().unwrap() } async fn init_ledger() -> LedgerSigner { - const DTYPE: DerivationType = DerivationType::LedgerLive(0); match LedgerSigner::new(DTYPE, 1).await { Ok(ledger) => ledger, Err(e) => panic!("{e:?}\n{e}"), @@ -329,10 +330,7 @@ mod tests { async fn test_get_address() { let ledger = init_ledger().await; assert_eq!(ledger.get_address().await.unwrap(), my_address()); - assert_eq!( - ledger.get_address_with_path(&DerivationType::Legacy(0)).await.unwrap(), - my_address(), - ); + assert_eq!(ledger.get_address_with_path(&DTYPE).await.unwrap(), my_address(),); } #[tokio::test] From aded01f6e781e9710d0ba7dbfb2aa3a8a501adc4 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:45:05 +0100 Subject: [PATCH 41/42] todos --- crates/signer-aws/src/signer.rs | 2 +- crates/signer-ledger/Cargo.toml | 1 + crates/signer-ledger/src/signer.rs | 9 ++++----- crates/signer-ledger/src/types.rs | 10 +++++++--- crates/signer-trezor/src/signer.rs | 12 ++++++------ crates/signer-trezor/src/types.rs | 2 +- crates/signer/src/signature.rs | 2 +- crates/signer/src/signer.rs | 8 ++++---- crates/signer/src/wallet/private_key.rs | 6 +++--- 9 files changed, 28 insertions(+), 24 deletions(-) diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index 16c3b671f30..059ab3e8f0d 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -100,7 +100,7 @@ impl Signer for AwsSigner { self.sign_digest_with_eip155(hash, self.chain_id).await.map_err(alloy_signer::Error::other) } - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction #[instrument(err)] async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { let mut tx_with_chain = tx.clone(); diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml index 9bbd0d597f5..a9d8b45edaf 100644 --- a/crates/signer-ledger/Cargo.toml +++ b/crates/signer-ledger/Cargo.toml @@ -18,6 +18,7 @@ alloy-signer.workspace = true async-trait.workspace = true coins-ledger = { version = "0.9.1", default-features = false } futures-util.workspace = true +k256.workspace = true semver.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index 910453ca812..3b51731a84d 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -57,7 +57,7 @@ impl Signer for LedgerSigner { .map_err(alloy_signer::Error::other) } - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction #[inline] async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { self.sign_tx(&tx).await.map_err(alloy_signer::Error::other) @@ -178,7 +178,7 @@ impl LedgerSigner { } /// Signs an Ethereum transaction (requires confirmation on the ledger) - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { let mut tx_with_chain = tx.clone(); if tx_with_chain.chain_id().is_none() { @@ -280,8 +280,7 @@ impl LedgerSigner { return Err(LedgerError::ShortResponse { got: data.len(), expected: 65 }); } - // TODO: don't unwrap - let sig = Signature::from_bytes(&data[1..], data[0] as u64).unwrap(); + let sig = Signature::from_bytes(&data[1..], data[0] as u64)?; debug!(?sig, "Received signature from device"); Ok(sig) } @@ -346,7 +345,7 @@ mod tests { #[tokio::test] #[serial_test::serial] #[ignore] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction async fn test_sign_tx() { let ledger = init_ledger().await; diff --git a/crates/signer-ledger/src/types.rs b/crates/signer-ledger/src/types.rs index 8270e4053a9..e25a27d423e 100644 --- a/crates/signer-ledger/src/types.rs +++ b/crates/signer-ledger/src/types.rs @@ -5,6 +5,7 @@ #![allow(clippy::upper_case_acronyms)] use alloy_primitives::hex; +use k256::ecdsa; use std::fmt; use thiserror::Error; @@ -35,15 +36,18 @@ pub enum LedgerError { /// Underlying Ledger transport error. #[error(transparent)] LedgerError(#[from] coins_ledger::errors::LedgerError), - /// Device response was unexpectedly none + /// Device response was unexpectedly empty. #[error("received an unexpected empty response")] UnexpectedNullResponse, - #[error(transparent)] /// [`hex`](mod@hex) error. - HexError(#[from] hex::FromHexError), #[error(transparent)] + HexError(#[from] hex::FromHexError), /// [`semver`] error. + #[error(transparent)] SemVerError(#[from] semver::Error), + /// [`ecdsa`] error. + #[error(transparent)] + Ecdsa(#[from] ecdsa::Error), /// Thrown when trying to sign using EIP-712 with an incompatible Ledger Ethereum app. #[error("Ledger Ethereum app requires at least version {0}")] UnsupportedAppVersion(&'static str), diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index a841ff2d137..dfc91a19e73 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -46,7 +46,7 @@ impl Signer for TrezorSigner { self.sign_message_(message).await.map_err(alloy_signer::Error::other) } - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction #[inline] async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { self.sign_tx(tx).await @@ -141,7 +141,7 @@ impl TrezorSigner { } /// Signs an Ethereum transaction (requires confirmation on the Trezor) - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction pub async fn sign_tx(&self, tx: &TypedTransaction) -> Result { let mut client = self.get_client()?; @@ -247,7 +247,7 @@ mod tests { #[tokio::test] #[ignore] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction async fn test_sign_tx() { let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); @@ -267,7 +267,7 @@ mod tests { #[tokio::test] #[ignore] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction async fn test_sign_big_data_tx() { let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); @@ -286,7 +286,7 @@ mod tests { #[tokio::test] #[ignore] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction async fn test_sign_empty_txes() { // Contract creation (empty `to`), requires data. // To test without the data field, we need to specify a `to` address. @@ -322,7 +322,7 @@ mod tests { #[tokio::test] #[ignore] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction async fn test_sign_eip1559_tx() { let trezor = TrezorSigner::new(DerivationType::TrezorLive(0), 1).await.unwrap(); diff --git a/crates/signer-trezor/src/types.rs b/crates/signer-trezor/src/types.rs index abb33be09f2..c1a0e9fe297 100644 --- a/crates/signer-trezor/src/types.rs +++ b/crates/signer-trezor/src/types.rs @@ -76,7 +76,7 @@ impl TrezorTransaction { trimmed_value[value.leading_zeros() / 8..].to_vec() } - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction pub fn load(tx: &TypedTransaction) -> Result { let to: String = match tx.to() { Some(v) => match v { diff --git a/crates/signer/src/signature.rs b/crates/signer/src/signature.rs index b2d9e57be47..e60f38deabf 100644 --- a/crates/signer/src/signature.rs +++ b/crates/signer/src/signature.rs @@ -276,7 +276,7 @@ mod tests { use std::str::FromStr; #[test] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: Transaction fn can_recover_tx_sender() { // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f let tx_rlp = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap(); diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index 85eb85d3cd0..dad630d26fb 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -28,7 +28,7 @@ pub trait Signer: Send + Sync { } /// Signs the transaction. - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction #[inline] async fn sign_transaction(&self, message: &TypedTransaction) -> Result { self.sign_hash(&message.sighash()).await @@ -93,7 +93,7 @@ pub trait SignerSync { } /// Signs the transaction. - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction #[inline] fn sign_transaction_sync(&self, message: &TypedTransaction) -> Result { self.sign_hash_sync(&message.sighash()) @@ -160,7 +160,7 @@ mod tests { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction assert!(s.sign_transaction(&Default::default()).await.is_err()); } @@ -175,7 +175,7 @@ mod tests { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction assert!(s.sign_transaction_sync(&Default::default()).is_err()); } diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index f5e662f6ce4..6f5e22d3d0f 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -224,7 +224,7 @@ mod tests { } #[tokio::test] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction async fn signs_tx() { // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction @@ -249,7 +249,7 @@ mod tests { } #[tokio::test] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction async fn signs_tx_empty_chain_id() { // retrieved test vector from: // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction @@ -280,7 +280,7 @@ mod tests { } #[test] - #[cfg(TODO)] + #[cfg(TODO)] // TODO: TypedTransaction fn signs_tx_empty_chain_id_sync() { let chain_id = 1337u64; // retrieved test vector from: From e06356a608a6bcaca6fd0166d181af6203b85bf3 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:09:21 +0100 Subject: [PATCH 42/42] auto_impl --- Cargo.toml | 1 + crates/signer/Cargo.toml | 1 + crates/signer/src/signer.rs | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 1d25c1d9411..d522d4eff3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ tracing-subscriber = "0.3.18" tempfile = "3.8" +auto_impl = "1.1" assert_matches = "1.5" base64 = "0.21" bimap = "0.6" diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 7c3b8b023ba..53a5eb2d7f7 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -16,6 +16,7 @@ alloy-primitives.workspace = true # TODO # alloy-rpc-types.workspace = true +auto_impl.workspace = true elliptic-curve.workspace = true k256.workspace = true rand.workspace = true diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index dad630d26fb..319f3b31cfa 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -1,6 +1,7 @@ use crate::{Result, Signature}; use alloy_primitives::{eip191_hash_message, Address, B256}; use async_trait::async_trait; +use auto_impl::auto_impl; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; @@ -15,6 +16,7 @@ use alloy_sol_types::{Eip712Domain, SolStruct}; /// Synchronous signers should implement both this trait and [`SignerSync`]. #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[auto_impl(&mut, Box)] pub trait Signer: Send + Sync { /// Signs the given hash. async fn sign_hash(&self, hash: &B256) -> Result; @@ -62,6 +64,7 @@ pub trait Signer: Send + Sync { /// Sets the signer's chain ID and returns `self`. #[inline] #[must_use] + #[auto_impl(keep_default_for(&mut, Box))] fn with_chain_id(mut self, chain_id: u64) -> Self where Self: Sized, @@ -80,6 +83,7 @@ pub trait Signer: Send + Sync { /// /// Synchronous signers should also implement [`Signer`], as they are always able to by delegating /// the asynchronous methods to the synchronous ones. +#[auto_impl(&, &mut, Box, Rc, Arc)] pub trait SignerSync { /// Signs the given hash. fn sign_hash_sync(&self, hash: &B256) -> Result;