diff --git a/crates/interfaces/src/test_utils/generators.rs b/crates/interfaces/src/test_utils/generators.rs index d7e17aee63..8d8e3bd480 100644 --- a/crates/interfaces/src/test_utils/generators.rs +++ b/crates/interfaces/src/test_utils/generators.rs @@ -1,9 +1,9 @@ use rand::{distributions::uniform::SampleRange, seq::SliceRandom, thread_rng, Rng}; use reth_primitives::{ - proofs, Account, Address, Bytes, Header, SealedBlock, SealedHeader, Signature, StorageEntry, - Transaction, TransactionKind, TransactionSigned, TxLegacy, H160, H256, U256, + proofs, sign_message, Account, Address, Bytes, Header, SealedBlock, SealedHeader, Signature, + StorageEntry, Transaction, TransactionKind, TransactionSigned, TxLegacy, H160, H256, U256, }; -use secp256k1::{KeyPair, Message as SecpMessage, Secp256k1, SecretKey}; +use secp256k1::{KeyPair, Message as SecpMessage, Secp256k1, SecretKey, SECP256K1}; use std::{collections::BTreeMap, ops::Sub}; // TODO(onbjerg): Maybe we should split this off to its own crate, or move the helpers to the @@ -72,21 +72,6 @@ pub fn random_signed_tx() -> TransactionSigned { TransactionSigned::from_transaction_and_signature(tx, signature) } -/// Signs message with the given secret key. -/// Returns the corresponding signature. -pub fn sign_message(secret: H256, message: H256) -> Result { - let secp = Secp256k1::new(); - let sec = SecretKey::from_slice(secret.as_ref())?; - let s = secp.sign_ecdsa_recoverable(&SecpMessage::from_slice(&message[..])?, &sec); - let (rec_id, data) = s.serialize_compact(); - - Ok(Signature { - r: U256::try_from_be_slice(&data[..32]).unwrap(), - s: U256::try_from_be_slice(&data[32..64]).unwrap(), - odd_y_parity: rec_id.to_i32() != 0, - }) -} - /// Generate a random block filled with signed transactions (generated using /// [random_signed_tx]). If no transaction count is provided, the number of transactions /// will be random, otherwise the provided count will be used. diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index b6b60c3762..88eca2ff07 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -64,10 +64,10 @@ pub use revm_primitives::JumpMap; pub use serde_helper::JsonU256; pub use storage::{StorageEntry, StorageTrieEntry}; pub use transaction::{ - AccessList, AccessListItem, AccessListWithGasUsed, FromRecoveredTransaction, - IntoRecoveredTransaction, InvalidTransactionError, Signature, Transaction, TransactionKind, - TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxLegacy, TxType, - EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, LEGACY_TX_TYPE_ID, + util::secp256k1::sign_message, AccessList, AccessListItem, AccessListWithGasUsed, + FromRecoveredTransaction, IntoRecoveredTransaction, InvalidTransactionError, Signature, + Transaction, TransactionKind, TransactionSigned, TransactionSignedEcRecovered, TxEip1559, + TxEip2930, TxLegacy, TxType, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, LEGACY_TX_TYPE_ID, }; pub use withdrawal::Withdrawal; diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 15213623ba..3ae06b947e 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -14,7 +14,7 @@ mod access_list; mod error; mod signature; mod tx_type; -mod util; +pub(crate) mod util; /// Legacy transaction. #[main_codec] diff --git a/crates/primitives/src/transaction/signature.rs b/crates/primitives/src/transaction/signature.rs index 468855b906..e6bde0e4a0 100644 --- a/crates/primitives/src/transaction/signature.rs +++ b/crates/primitives/src/transaction/signature.rs @@ -99,6 +99,17 @@ impl Signature { // errors and we care only if recovery is passing or not. secp256k1::recover(&sig, hash.as_fixed_bytes()).ok() } + + /// Turn this signature into its byte + /// (hex) representation. + pub fn to_bytes(&self) -> [u8; 65] { + let mut sig = [0u8; 65]; + sig[..32].copy_from_slice(&self.r.to_be_bytes::<32>()); + sig[32..64].copy_from_slice(&self.s.to_be_bytes::<32>()); + let v = u8::from(self.odd_y_parity) + 27; + sig[64] = v; + sig + } } #[cfg(test)] diff --git a/crates/primitives/src/transaction/util.rs b/crates/primitives/src/transaction/util.rs index 5bb11d41c5..f2d471c304 100644 --- a/crates/primitives/src/transaction/util.rs +++ b/crates/primitives/src/transaction/util.rs @@ -1,11 +1,16 @@ use crate::{keccak256, Address}; pub(crate) mod secp256k1 { + use crate::Signature; + use super::*; use ::secp256k1::{ ecdsa::{RecoverableSignature, RecoveryId}, - Error, Message, SECP256K1, + Message, SecretKey, SECP256K1, }; + use revm_primitives::{B256, U256}; + + pub(crate) use ::secp256k1::Error; /// secp256k1 signer recovery pub(crate) fn recover(sig: &[u8; 65], msg: &[u8; 32]) -> Result { @@ -16,6 +21,21 @@ pub(crate) mod secp256k1 { let hash = keccak256(&public.serialize_uncompressed()[1..]); Ok(Address::from_slice(&hash[12..])) } + + /// Signs message with the given secret key. + /// Returns the corresponding signature. + pub fn sign_message(secret: B256, message: B256) -> Result { + let sec = SecretKey::from_slice(secret.as_ref())?; + let s = SECP256K1.sign_ecdsa_recoverable(&Message::from_slice(&message[..])?, &sec); + let (rec_id, data) = s.serialize_compact(); + + let signature = Signature { + r: U256::try_from_be_slice(&data[..32]).expect("The slice has at most 32 bytes"), + s: U256::try_from_be_slice(&data[32..64]).expect("The slice has at most 32 bytes"), + odd_y_parity: rec_id.to_i32() != 0, + }; + Ok(signature) + } } #[cfg(test)] mod tests { diff --git a/crates/rpc/rpc-builder/tests/it/http.rs b/crates/rpc/rpc-builder/tests/it/http.rs index 6d4cc66df0..876f4ce901 100644 --- a/crates/rpc/rpc-builder/tests/it/http.rs +++ b/crates/rpc/rpc-builder/tests/it/http.rs @@ -78,6 +78,11 @@ where EthApiClient::block_uncles_count_by_number(client, block_number).await.unwrap(); EthApiClient::uncle_by_block_hash_and_index(client, hash, index).await.unwrap(); EthApiClient::uncle_by_block_number_and_index(client, block_number, index).await.unwrap(); + EthApiClient::sign(client, address, bytes.clone()).await.unwrap_err(); + EthApiClient::sign_typed_data(client, address, jsonrpsee::core::JsonValue::Null) + .await + .unwrap_err(); + EthApiClient::create_access_list(client, call_request.clone(), None).await.unwrap(); EthApiClient::transaction_by_hash(client, tx_hash).await.unwrap(); EthApiClient::transaction_by_block_hash_and_index(client, hash, index).await.unwrap(); EthApiClient::transaction_by_block_number_and_index(client, block_number, index).await.unwrap(); @@ -112,18 +117,9 @@ where assert!(is_unimplemented( EthApiClient::send_transaction(client, transaction_request).await.err().unwrap() )); - assert!(is_unimplemented( - EthApiClient::sign(client, address, bytes.clone()).await.err().unwrap() - )); assert!(is_unimplemented( EthApiClient::sign_transaction(client, call_request.clone()).await.err().unwrap() )); - assert!(is_unimplemented( - EthApiClient::sign_typed_data(client, address, jsonrpsee::core::JsonValue::Null) - .await - .err() - .unwrap() - )); } async fn test_basic_debug_calls(client: &C) diff --git a/crates/rpc/rpc/Cargo.toml b/crates/rpc/rpc/Cargo.toml index 949fb3313e..0323eac566 100644 --- a/crates/rpc/rpc/Cargo.toml +++ b/crates/rpc/rpc/Cargo.toml @@ -26,7 +26,7 @@ reth-tasks = { path = "../../tasks" } # eth revm = { version = "3.0.0", features = ["optional_block_gas_limit"] } -ethers-core = { git = "https://github.com/gakonst/ethers-rs" } +ethers-core = { git = "https://github.com/gakonst/ethers-rs", features = ["eip712"] } # rpc jsonrpsee = { version = "0.16" } @@ -46,7 +46,7 @@ bytes = "1.4" secp256k1 = { version = "0.26.0", features = [ "global-context", "rand-std", - "recovery", + "recovery" ] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/crates/rpc/rpc/src/eth/api/mod.rs b/crates/rpc/rpc/src/eth/api/mod.rs index 826ae34c90..dac35c88db 100644 --- a/crates/rpc/rpc/src/eth/api/mod.rs +++ b/crates/rpc/rpc/src/eth/api/mod.rs @@ -19,6 +19,7 @@ use std::{num::NonZeroUsize, ops::Deref, sync::Arc}; mod block; mod call; mod server; +mod sign; mod state; mod transactions; pub use transactions::{EthTransactions, TransactionSource}; diff --git a/crates/rpc/rpc/src/eth/api/server.rs b/crates/rpc/rpc/src/eth/api/server.rs index 4e676713a4..41f6383a3a 100644 --- a/crates/rpc/rpc/src/eth/api/server.rs +++ b/crates/rpc/rpc/src/eth/api/server.rs @@ -360,8 +360,8 @@ where } /// Handler for: `eth_sign` - async fn sign(&self, _address: Address, _message: Bytes) -> Result { - Err(internal_rpc_err("unimplemented")) + async fn sign(&self, address: Address, message: Bytes) -> Result { + Ok(EthApi::sign(self, address, message).await?) } /// Handler for: `eth_signTransaction` @@ -370,8 +370,8 @@ where } /// Handler for: `eth_signTypedData` - async fn sign_typed_data(&self, _address: Address, _data: Value) -> Result { - Err(internal_rpc_err("unimplemented")) + async fn sign_typed_data(&self, address: Address, data: Value) -> Result { + Ok(EthApi::sign_typed_data(self, data, address).await?) } /// Handler for: `eth_getProof` diff --git a/crates/rpc/rpc/src/eth/api/sign.rs b/crates/rpc/rpc/src/eth/api/sign.rs new file mode 100644 index 0000000000..edd03e4655 --- /dev/null +++ b/crates/rpc/rpc/src/eth/api/sign.rs @@ -0,0 +1,41 @@ +//! Contains RPC handler implementations specific to sign endpoints +use crate::{ + eth::{ + error::{EthResult, SignError}, + signer::EthSigner, + }, + EthApi, +}; +use ethers_core::types::transaction::eip712::TypedData; +use reth_primitives::{Address, Bytes}; +use serde_json::Value; +use std::ops::Deref; + +impl EthApi { + pub(crate) async fn sign(&self, account: Address, message: Bytes) -> EthResult { + let signer = self.find_signer(&account)?; + let signature = signer.sign(account, &message).await?; + let bytes = hex::encode(signature.to_bytes()).as_bytes().into(); + Ok(bytes) + } + + pub(crate) async fn sign_typed_data(&self, data: Value, account: Address) -> EthResult { + let signer = self.find_signer(&account)?; + let data = serde_json::from_value::(data).map_err(|_| SignError::TypedData)?; + let signature = signer.sign_typed_data(account, &data)?; + let bytes = hex::encode(signature.to_bytes()).as_bytes().into(); + Ok(bytes) + } + + pub(crate) fn find_signer( + &self, + account: &Address, + ) -> Result<&(dyn EthSigner + 'static), SignError> { + self.inner + .signers + .iter() + .find(|signer| signer.is_signer_for(account)) + .map(|signer| signer.deref()) + .ok_or(SignError::NoAccount) + } +} diff --git a/crates/rpc/rpc/src/eth/error.rs b/crates/rpc/rpc/src/eth/error.rs index 3aa76da580..c95abea25b 100644 --- a/crates/rpc/rpc/src/eth/error.rs +++ b/crates/rpc/rpc/src/eth/error.rs @@ -52,6 +52,9 @@ pub enum EthApiError { /// Other internal error #[error(transparent)] Internal(#[from] reth_interfaces::Error), + /// Error related to signing + #[error(transparent)] + Signing(#[from] SignError), } impl From for RpcError { @@ -65,6 +68,7 @@ impl From for RpcError { EthApiError::ConflictingRequestGasPrice { .. } | EthApiError::ConflictingRequestGasPriceAndTipSet { .. } | EthApiError::RequestLegacyGasPriceAndTipSet { .. } | + EthApiError::Signing(_) | EthApiError::BothStateAndStateDiffInOverride(_) => { rpc_err(INVALID_PARAMS_CODE, error.to_string(), None) } @@ -312,6 +316,20 @@ impl From for EthApiError { } } +/// Errors returned from a sign request. +#[derive(Debug, thiserror::Error)] +pub enum SignError { + /// Error occured while trying to sign data. + #[error("Could not sign")] + CouldNotSign, + /// Signer for requested account not found. + #[error("Unknown account")] + NoAccount, + /// TypedData has invalid format. + #[error("Given typed data is not valid")] + TypedData, +} + /// Returns the revert reason from the `revm::TransactOut` data, if it's an abi encoded String. /// /// **Note:** it's assumed the `out` buffer starts with the call's signature diff --git a/crates/rpc/rpc/src/eth/signer.rs b/crates/rpc/rpc/src/eth/signer.rs index 2c16e74866..90a2bb4dee 100644 --- a/crates/rpc/rpc/src/eth/signer.rs +++ b/crates/rpc/rpc/src/eth/signer.rs @@ -1,11 +1,17 @@ //! An abstraction over ethereum signers. -use jsonrpsee::core::RpcResult as Result; -use reth_primitives::{Address, Signature, TransactionSigned}; +use crate::eth::error::SignError; +use ethers_core::{ + types::transaction::eip712::{Eip712, TypedData}, + utils::hash_message, +}; +use reth_primitives::{sign_message, Address, Signature, TransactionSigned, H256}; use reth_rpc_types::TypedTransactionRequest; use secp256k1::SecretKey; use std::collections::HashMap; +type Result = std::result::Result; + /// An Ethereum Signer used via RPC. #[async_trait::async_trait] pub(crate) trait EthSigner: Send + Sync { @@ -26,6 +32,9 @@ pub(crate) trait EthSigner: Send + Sync { request: TypedTransactionRequest, address: &Address, ) -> Result; + + /// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait. + fn sign_typed_data(&self, address: Address, payload: &TypedData) -> Result; } /// Holds developer keys @@ -34,6 +43,16 @@ pub(crate) struct DevSigner { accounts: HashMap, } +impl DevSigner { + fn get_key(&self, account: Address) -> Result<&SecretKey> { + self.accounts.get(&account).ok_or(SignError::NoAccount) + } + fn sign_hash(&self, hash: H256, account: Address) -> Result { + let secret = self.get_key(account)?; + let signature = sign_message(H256::from_slice(secret.as_ref()), hash); + signature.map_err(|_| SignError::CouldNotSign) + } +} #[async_trait::async_trait] impl EthSigner for DevSigner { fn accounts(&self) -> Vec
{ @@ -44,8 +63,11 @@ impl EthSigner for DevSigner { self.accounts.contains_key(addr) } - async fn sign(&self, _address: Address, _message: &[u8]) -> Result { - todo!() + async fn sign(&self, address: Address, message: &[u8]) -> Result { + // Hash message according to EIP 191: + // https://ethereum.org/es/developers/docs/apis/json-rpc/#eth_sign + let hash = hash_message(message).into(); + self.sign_hash(hash, address) } fn sign_transaction( @@ -55,4 +77,131 @@ impl EthSigner for DevSigner { ) -> Result { todo!() } + + fn sign_typed_data(&self, address: Address, payload: &TypedData) -> Result { + let encoded: H256 = payload.encode_eip712().map_err(|_| SignError::TypedData)?.into(); + self.sign_hash(encoded, address) + } +} +#[cfg(test)] +mod test { + use super::*; + use reth_primitives::U256; + use std::str::FromStr; + fn build_signer() -> DevSigner { + let addresses = vec![]; + let secret = + SecretKey::from_str("4646464646464646464646464646464646464646464646464646464646464646") + .unwrap(); + let accounts = HashMap::from([(Address::default(), secret)]); + DevSigner { addresses, accounts } + } + + #[tokio::test] + async fn test_sign_type_data() { + let eip_712_example = serde_json::json!( + r#"{ + "types": { + "EIP712Domain": [ + { + "name": "name", + "type": "string" + }, + { + "name": "version", + "type": "string" + }, + { + "name": "chainId", + "type": "uint256" + }, + { + "name": "verifyingContract", + "type": "address" + } + ], + "Person": [ + { + "name": "name", + "type": "string" + }, + { + "name": "wallet", + "type": "address" + } + ], + "Mail": [ + { + "name": "from", + "type": "Person" + }, + { + "name": "to", + "type": "Person" + }, + { + "name": "contents", + "type": "string" + } + ] + }, + "primaryType": "Mail", + "domain": { + "name": "Ether Mail", + "version": "1", + "chainId": 1, + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents": "Hello, Bob!" + } + }"# + ); + let data: TypedData = serde_json::from_value(eip_712_example).unwrap(); + let signer = build_signer(); + let sig = signer.sign_typed_data(Address::default(), &data).unwrap(); + let expected = Signature { + r: U256::from_str_radix( + "5318aee9942b84885761bb20e768372b76e7ee454fc4d39b59ce07338d15a06c", + 16, + ) + .unwrap(), + s: U256::from_str_radix( + "5e585a2f4882ec3228a9303244798b47a9102e4be72f48159d890c73e4511d79", + 16, + ) + .unwrap(), + odd_y_parity: false, + }; + assert_eq!(sig, expected) + } + + #[tokio::test] + async fn test_signer() { + let message = b"Test message"; + let signer = build_signer(); + let sig = signer.sign(Address::default(), message).await.unwrap(); + let expected = Signature { + r: U256::from_str_radix( + "54313da7432e4058b8d22491b2e7dbb19c7186c35c24155bec0820a8a2bfe0c1", + 16, + ) + .unwrap(), + s: U256::from_str_radix( + "687250f11a3d4435004c04a4cb60e846bc27997271d67f21c6c8170f17a25e10", + 16, + ) + .unwrap(), + odd_y_parity: true, + }; + assert_eq!(sig, expected) + } }