feat(rpc): eth_sign* (#1665)

Co-authored-by: lambdaclass-user <github@lambdaclass.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Tomás <tomas.gruner@lambdaclass.com>
This commit is contained in:
Francisco Krause Arnim
2023-03-14 22:03:38 -03:00
committed by GitHub
parent 4725c4d776
commit a688fdb38d
12 changed files with 264 additions and 43 deletions

View File

@@ -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<Signature, secp256k1::Error> {
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.

View File

@@ -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;

View File

@@ -14,7 +14,7 @@ mod access_list;
mod error;
mod signature;
mod tx_type;
mod util;
pub(crate) mod util;
/// Legacy transaction.
#[main_codec]

View File

@@ -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)]

View File

@@ -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<Address, Error> {
@@ -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<Signature, secp256k1::Error> {
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 {

View File

@@ -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<C>(client: &C)

View File

@@ -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"

View File

@@ -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};

View File

@@ -360,8 +360,8 @@ where
}
/// Handler for: `eth_sign`
async fn sign(&self, _address: Address, _message: Bytes) -> Result<Bytes> {
Err(internal_rpc_err("unimplemented"))
async fn sign(&self, address: Address, message: Bytes) -> Result<Bytes> {
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<Bytes> {
Err(internal_rpc_err("unimplemented"))
async fn sign_typed_data(&self, address: Address, data: Value) -> Result<Bytes> {
Ok(EthApi::sign_typed_data(self, data, address).await?)
}
/// Handler for: `eth_getProof`

View File

@@ -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<Client, Pool, Network> EthApi<Client, Pool, Network> {
pub(crate) async fn sign(&self, account: Address, message: Bytes) -> EthResult<Bytes> {
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<Bytes> {
let signer = self.find_signer(&account)?;
let data = serde_json::from_value::<TypedData>(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)
}
}

View File

@@ -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<EthApiError> for RpcError {
@@ -65,6 +68,7 @@ impl From<EthApiError> 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<PoolError> 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

View File

@@ -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<T> = std::result::Result<T, SignError>;
/// 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<TransactionSigned>;
/// Encodes and signs the typed data according EIP-712. Payload must implement Eip712 trait.
fn sign_typed_data(&self, address: Address, payload: &TypedData) -> Result<Signature>;
}
/// Holds developer keys
@@ -34,6 +43,16 @@ pub(crate) struct DevSigner {
accounts: HashMap<Address, SecretKey>,
}
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<Signature> {
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<Address> {
@@ -44,8 +63,11 @@ impl EthSigner for DevSigner {
self.accounts.contains_key(addr)
}
async fn sign(&self, _address: Address, _message: &[u8]) -> Result<Signature> {
todo!()
async fn sign(&self, address: Address, message: &[u8]) -> Result<Signature> {
// 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<TransactionSigned> {
todo!()
}
fn sign_typed_data(&self, address: Address, payload: &TypedData) -> Result<Signature> {
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)
}
}