feat(primitive): Signer recovery (#179)

* feat(consensus): Signer recovery and tx validation

* Signature hash and use seckp256k1 over k256

* use deref_more for transactions

* cleanup and fix for eip1559 hash

* fix hash calculation on decoding
This commit is contained in:
rakita
2022-11-09 18:11:32 +01:00
committed by GitHub
parent 836ad6aaee
commit 9e35d58b05
11 changed files with 358 additions and 79 deletions

2
Cargo.lock generated
View File

@ -3327,6 +3327,7 @@ dependencies = [
"arbitrary",
"bytes",
"crc",
"derive_more",
"ethers-core",
"hex",
"hex-literal",
@ -3334,6 +3335,7 @@ dependencies = [
"parity-scale-codec",
"reth-codecs",
"reth-rlp",
"secp256k1",
"serde",
"serde_json",
"sucds",

View File

@ -15,10 +15,12 @@ pub struct Config {
pub london_hard_fork_block: BlockNumber,
/// The Merge/Paris hard fork block number
pub paris_hard_fork_block: BlockNumber,
/// Blockchain identifier introduced in EIP-155: Simple replay attack protection
pub chain_id: u64,
}
impl Default for Config {
fn default() -> Self {
Self { london_hard_fork_block: 12965000, paris_hard_fork_block: 15537394 }
Self { london_hard_fork_block: 12965000, paris_hard_fork_block: 15537394, chain_id: 1 }
}
}

View File

@ -1,7 +1,9 @@
//! ALl functions for verification of block
use crate::{config, Config};
use reth_interfaces::{consensus::Error, provider::HeaderProvider, Result as RethResult};
use reth_primitives::{BlockLocked, SealedHeader, TransactionSigned};
use reth_primitives::{
Account, Address, BlockLocked, SealedHeader, Transaction, TransactionSigned,
};
use std::time::SystemTime;
/// Validate header standalone
@ -34,10 +36,81 @@ pub fn validate_header_standalone(
/// Validate transactions standlone
pub fn validate_transactions_standalone(
_transactions: &[TransactionSigned],
_config: &Config,
transaction: &Transaction,
config: &Config,
) -> Result<(), Error> {
// TODO
let chain_id = match transaction {
Transaction::Legacy { chain_id, .. } => *chain_id,
Transaction::Eip2930 { chain_id, .. } => Some(*chain_id),
Transaction::Eip1559 { chain_id, max_fee_per_gas, max_priority_fee_per_gas, .. } => {
// EIP-1559: add more constraints to the tx validation
// https://github.com/ethereum/EIPs/pull/3594
if max_priority_fee_per_gas > max_fee_per_gas {
return Err(Error::TransactionPriorityFeeMoreThenMaxFee)
}
Some(*chain_id)
}
};
if let Some(chain_id) = chain_id {
if chain_id != config.chain_id {
return Err(Error::TransactionChainId)
}
}
// signature validation?
Ok(())
}
/// Validate transaction in regards to header
/// Only parametar from header that effects transaction is base_fee
pub fn validate_transaction_regarding_header(
transaction: &Transaction,
base_fee: Option<u64>,
) -> Result<(), Error> {
// check basefee and few checks that are related to that.
// https://github.com/ethereum/EIPs/pull/3594
if let Some(base_fee_per_gas) = base_fee {
if transaction.max_fee_per_gas() < base_fee_per_gas {
return Err(Error::TransactionMaxFeeLessThenBaseFee)
}
}
Ok(())
}
/// Account provider
pub trait AccountProvider {
/// Get basic account information.
fn basic_account(&self, address: Address) -> reth_interfaces::Result<Option<Account>>;
}
/// Validate transaction in regards of State
pub fn validate_transaction_regarding_state<AP: AccountProvider>(
_transaction: &TransactionSigned,
_config: &Config,
_account_provider: &AP,
) -> Result<(), Error> {
// sanity check: if account has a bytecode. This is not allowed.s
// check nonce
// gas_price*gas_limit+value < account.balance
// let max_gas_cost = U512::from(message.gas_limit())
// * U512::from(ethereum_types::U256::from(message.max_fee_per_gas().to_be_bytes()));
// // See YP, Eq (57) in Section 6.2 "Execution"
// let v0 = max_gas_cost +
// U512::from(ethereum_types::U256::from(message.value().to_be_bytes()));
// let available_balance =
// ethereum_types::U256::from(self.state.get_balance(sender)?.to_be_bytes()).into();
// if available_balance < v0 {
// return Err(TransactionValidationError::Validation(
// BadTransactionError::InsufficientFunds {
// account: sender,
// available: available_balance,
// required: v0,
// },
// ));
// }
Ok(())
}

View File

@ -49,6 +49,8 @@ impl Executor {
// create receipt
// bloom filter from logs
// Sum of the transactions gas limit and the gas utilized in this block prior
// Receipt outcome EIP-658: Embedding transaction status code in receipts
// EIP-658 supperseeded EIP-98 in Byzantium fork
}

View File

@ -41,7 +41,6 @@ pub enum Error {
TimestampIsInPast { parent_timestamp: u64, timestamp: u64 },
#[error("Block timestamp {timestamp:?} is in future in comparison of our clock time {present_timestamp:?}")]
TimestampIsInFuture { timestamp: u64, present_timestamp: u64 },
// TODO make better error msg :)
#[error("Child gas_limit {child_gas_limit:?} max increase is {parent_gas_limit}/1024")]
GasLimitInvalidIncrease { parent_gas_limit: u64, child_gas_limit: u64 },
#[error("Child gas_limit {child_gas_limit:?} max decrease is {parent_gas_limit}/1024")]
@ -50,4 +49,10 @@ pub enum Error {
BaseFeeMissing,
#[error("Block base fee ({got:?}) is different then expected: ({expected:?})")]
BaseFeeDiff { expected: u64, got: u64 },
#[error("Transaction eip1559 priority fee is more then max fee")]
TransactionPriorityFeeMoreThenMaxFee,
#[error("Transaction chain_id does not match")]
TransactionChainId,
#[error("Transation max fee is less them block base fee")]
TransactionMaxFeeLessThenBaseFee,
}

View File

@ -395,10 +395,10 @@ mod test {
};
// checking tx by tx for easier debugging if there are any regressions
for (expected, decoded) in
for (decoded, expected) in
decoded_transactions.message.0.iter().zip(expected_transactions.message.0.iter())
{
assert_eq!(expected, decoded);
assert_eq!(decoded, expected);
}
assert_eq!(decoded_transactions, expected_transactions);

View File

@ -17,6 +17,9 @@ ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features =
parity-scale-codec = { version = "3.2.1", features = ["derive", "bytes"] }
tiny-keccak = { version = "2.0", features = ["keccak"] }
# crypto
secp256k1 = { version = "0.24.0", default-features = false, features = ["alloc", "recovery"] }
#used for forkid
crc = "1"
maplit = "1"
@ -29,6 +32,9 @@ sucds = "0.5.0"
arbitrary = { version = "1.1.7", features = ["derive"], optional = true}
hex = "0.4"
hex-literal = "0.3"
derive_more = "0.99"
[dev-dependencies]
arbitrary = { version = "1.1.7", features = ["derive"]}

View File

@ -31,7 +31,8 @@ pub use log::Log;
pub use receipt::Receipt;
pub use storage::StorageEntry;
pub use transaction::{
AccessList, AccessListItem, Signature, Transaction, TransactionKind, TransactionSigned, TxType,
AccessList, AccessListItem, Signature, Transaction, TransactionKind, TransactionSigned,
TransactionSignedEcRecovered, TxType,
};
/// Block hash.
@ -46,6 +47,8 @@ pub type BlockID = H256;
pub type TxHash = H256;
/// TxNumber is sequence number of all existing transactions
pub type TxNumber = u64;
/// Chain identifier type, introduced in EIP-155
pub type ChainId = u64;
/// Storage Key
pub type StorageKey = H256;

View File

@ -1,15 +1,16 @@
mod access_list;
mod signature;
mod tx_type;
mod util;
use crate::{Address, Bytes, TxHash, U256};
use crate::{Address, Bytes, ChainId, TxHash, H256, U256};
pub use access_list::{AccessList, AccessListItem};
use bytes::Buf;
use bytes::{Buf, BytesMut};
use derive_more::{AsRef, Deref};
use ethers_core::utils::keccak256;
use reth_codecs::main_codec;
use reth_rlp::{length_of_length, Decodable, DecodeError, Encodable, Header, EMPTY_STRING_CODE};
pub use signature::Signature;
use std::ops::Deref;
pub use tx_type::TxType;
/// Raw Transaction.
@ -20,7 +21,7 @@ pub enum Transaction {
/// Legacy transaciton.
Legacy {
/// Added as EIP-155: Simple replay attack protection
chain_id: Option<u64>,
chain_id: Option<ChainId>,
/// A scalar value equal to the number of transactions sent by the sender; formally Tn.
nonce: u64,
/// A scalar value equal to the number of
@ -51,7 +52,7 @@ pub enum Transaction {
/// Transaction with AccessList. https://eips.ethereum.org/EIPS/eip-2930
Eip2930 {
/// Added as EIP-155: Simple replay attack protection
chain_id: u64,
chain_id: ChainId,
/// A scalar value equal to the number of transactions sent by the sender; formally Tn.
nonce: u64,
/// A scalar value equal to the number of
@ -129,12 +130,12 @@ pub enum Transaction {
}
impl Transaction {
/// Heavy operation that return hash over rlp encoded transaction.
/// It is only used for signature signing.
pub fn signature_hash(&self) -> TxHash {
let mut encoded = Vec::with_capacity(self.length());
self.encode(&mut encoded);
keccak256(encoded).into()
/// Heavy operation that return signature hash over rlp encoded transaction.
/// It is only for signature signing or signer recovery.
pub fn signature_hash(&self) -> H256 {
let mut buf = BytesMut::new();
self.encode(&mut buf);
keccak256(&buf).into()
}
/// Sets the transaction's chain id to the provided value.
@ -174,6 +175,16 @@ impl Transaction {
}
}
/// Max fee per gas for eip1559 transaction, for legacy transactions this is gas_limit
pub fn max_fee_per_gas(&self) -> u64 {
match self {
Transaction::Legacy { gas_limit, .. } | Transaction::Eip2930 { gas_limit, .. } => {
*gas_limit
}
Transaction::Eip1559 { max_fee_per_gas, .. } => *max_fee_per_gas,
}
}
/// Get the transaction's input field.
pub fn input(&self) -> &Bytes {
match self {
@ -432,9 +443,11 @@ impl Decodable for TransactionKind {
/// Signed transaction.
#[main_codec]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Deref)]
pub struct TransactionSigned {
/// Raw transaction info
#[deref]
#[as_ref]
pub transaction: Transaction,
/// Transaction hash
pub hash: TxHash,
@ -442,20 +455,6 @@ pub struct TransactionSigned {
pub signature: Signature,
}
impl AsRef<Transaction> for TransactionSigned {
fn as_ref(&self) -> &Transaction {
&self.transaction
}
}
impl Deref for TransactionSigned {
type Target = Transaction;
fn deref(&self) -> &Self::Target {
&self.transaction
}
}
impl Encodable for TransactionSigned {
fn length(&self) -> usize {
let len = self.payload_len();
@ -465,41 +464,7 @@ impl Encodable for TransactionSigned {
}
fn encode(&self, out: &mut dyn bytes::BufMut) {
if let Transaction::Legacy { chain_id, .. } = self.transaction {
let header = Header { list: true, payload_length: self.payload_len() };
header.encode(out);
self.transaction.encode_fields(out);
if let Some(id) = chain_id {
self.signature.encode_eip155_inner(out, id);
} else {
// if the transaction has no chain id then it is a pre-EIP-155 transaction
self.signature.encode_inner_legacy(out);
}
} else {
let header = Header { list: false, payload_length: self.payload_len() };
header.encode(out);
match self.transaction {
Transaction::Eip2930 { .. } => {
out.put_u8(1);
let list_header = Header { list: true, payload_length: self.inner_tx_len() };
list_header.encode(out);
}
Transaction::Eip1559 { .. } => {
out.put_u8(2);
let list_header = Header { list: true, payload_length: self.inner_tx_len() };
list_header.encode(out);
}
Transaction::Legacy { .. } => {
unreachable!("Legacy transaction should be handled above")
}
}
self.transaction.encode_fields(out);
self.signature.odd_y_parity.encode(out);
self.signature.r.encode(out);
self.signature.s.encode(out);
}
self.encode_inner(out, true);
}
}
@ -512,6 +477,10 @@ impl Decodable for TransactionSigned {
let first_header = Header::decode(buf)?;
// if the transaction is encoded as a string then it is a typed transaction
if !first_header.list {
// Bytes that are going to be used to create a hash of transaction.
// For eip2728 types transaction header is not used inside hash
let original_encoding = *buf;
let tx_type = *buf
.first()
.ok_or(DecodeError::Custom("typed tx cannot be decoded from an empty slice"))?;
@ -555,8 +524,7 @@ impl Decodable for TransactionSigned {
};
let mut signed = TransactionSigned { transaction, hash: Default::default(), signature };
let tx_length = first_header.payload_length + first_header.length();
signed.hash = keccak256(&original_encoding[..tx_length]).into();
signed.hash = keccak256(&original_encoding[..first_header.payload_length]).into();
Ok(signed)
} else {
let mut transaction = Transaction::Legacy {
@ -592,13 +560,79 @@ impl TransactionSigned {
self.hash
}
/// Recover signer from signature and hash.
pub fn recover_signer(&self) -> Option<Address> {
let signature_hash = self.signature_hash();
self.signature.recover_signer(signature_hash)
}
/// Devour Self, recover signer and return [`TransactionSignedEcRecovered`]
pub fn into_ecrecovered(self) -> Option<TransactionSignedEcRecovered> {
let signer = self.recover_signer()?;
Some(TransactionSignedEcRecovered { signed_transaction: self, signer })
}
/// try to recover signer and return [`TransactionSignedEcRecovered`]
pub fn try_ecrecovered(&self) -> Option<TransactionSignedEcRecovered> {
let signer = self.recover_signer()?;
Some(TransactionSignedEcRecovered { signed_transaction: self.clone(), signer })
}
/// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating
/// hash that for eip2728 does not require rlp header
fn encode_inner(&self, out: &mut dyn bytes::BufMut, with_header: bool) {
if let Transaction::Legacy { chain_id, .. } = self.transaction {
let header = Header { list: true, payload_length: self.payload_len() };
header.encode(out);
self.transaction.encode_fields(out);
if let Some(id) = chain_id {
self.signature.encode_eip155_inner(out, id);
} else {
// if the transaction has no chain id then it is a pre-EIP-155 transaction
self.signature.encode_inner_legacy(out);
}
} else {
if with_header {
let header = Header { list: false, payload_length: self.payload_len() };
header.encode(out);
}
match self.transaction {
Transaction::Eip2930 { .. } => {
out.put_u8(1);
let list_header = Header { list: true, payload_length: self.inner_tx_len() };
list_header.encode(out);
}
Transaction::Eip1559 { .. } => {
out.put_u8(2);
let list_header = Header { list: true, payload_length: self.inner_tx_len() };
list_header.encode(out);
}
Transaction::Legacy { .. } => {
unreachable!("Legacy transaction should be handled above")
}
}
self.transaction.encode_fields(out);
self.signature.odd_y_parity.encode(out);
self.signature.r.encode(out);
self.signature.s.encode(out);
}
}
/// Calculate transaction hash, eip2728 transaction does not contain rlp header and start with
/// tx type.
pub fn recalculate_hash(&self) -> H256 {
let mut buf = Vec::new();
self.encode_inner(&mut buf, false);
keccak256(&buf).into()
}
/// Create a new signed transaction from a transaction and its signature.
/// This will also calculate the transaction hash using its encoding.
pub fn from_transaction_and_signature(transaction: Transaction, signature: Signature) -> Self {
let mut initial_tx = Self { transaction, hash: Default::default(), signature };
let mut buf = Vec::new();
initial_tx.encode(&mut buf);
initial_tx.hash = keccak256(&buf).into();
initial_tx.hash = initial_tx.recalculate_hash();
initial_tx
}
@ -634,13 +668,42 @@ impl TransactionSigned {
}
}
/// Signed transaction with recovered signer.
#[main_codec]
#[derive(Debug, Clone, PartialEq, Eq, Hash, AsRef, Deref)]
pub struct TransactionSignedEcRecovered {
/// Signed transaction
#[deref]
#[as_ref]
signed_transaction: TransactionSigned,
/// Signer of the transaction
signer: Address,
}
impl TransactionSignedEcRecovered {
/// Signer of transaction recovered from signature
pub fn signer(&self) -> Address {
self.signer
}
/// Transform back to [`TransactionSigned`]
pub fn into_signed(self) -> TransactionSigned {
self.signed_transaction
}
/// Create [`TransactionSignedEcRecovered`] from [`TransactionSigned`] and [`Address`].
pub fn from_signed_transaction(signed_transaction: TransactionSigned, signer: Address) -> Self {
Self { signed_transaction, signer }
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::{
transaction::{signature::Signature, TransactionKind},
Address, Bytes, Transaction, TransactionSigned, H256, U256,
AccessList, Address, Bytes, Transaction, TransactionSigned, H256, U256,
};
use bytes::BytesMut;
use ethers_core::utils::hex;
@ -648,7 +711,6 @@ mod tests {
#[test]
fn test_decode_create() {
// panic!("not implemented");
// tests that a contract creation tx encodes and decodes properly
let request = Transaction::Eip2930 {
chain_id: 1u64,
@ -844,4 +906,80 @@ mod tests {
let expected = TransactionSigned::from_transaction_and_signature(expected, signature);
assert_eq!(expected, TransactionSigned::decode(bytes_fifth).unwrap());
}
#[test]
fn decode_raw_tx_and_recover_signer() {
use crate::hex_literal::hex;
// transaction is from ropsten
let hash: H256 =
hex!("559fb34c4a7f115db26cbf8505389475caaab3df45f5c7a0faa4abfa3835306c").into();
let signer: Address = hex!("641c5d790f862a58ec7abcfd644c0442e9c201b3").into();
let raw =hex!("f88b8212b085028fa6ae00830f424094aad593da0c8116ef7d2d594dd6a63241bccfc26c80a48318b64b000000000000000000000000641c5d790f862a58ec7abcfd644c0442e9c201b32aa0a6ef9e170bca5ffb7ac05433b13b7043de667fbb0b4a5e45d3b54fb2d6efcc63a0037ec2c05c3d60c5f5f78244ce0a3859e3a18a36c61efb061b383507d3ce19d2");
let mut pointer = raw.as_ref();
let tx = TransactionSigned::decode(&mut pointer).unwrap();
assert_eq!(tx.hash(), hash, "Expected same hash");
assert_eq!(tx.recover_signer(), Some(signer), "Recovering signer should pass.");
}
#[test]
fn recover_signer_legacy() {
use crate::hex_literal::hex;
let signer: Address = hex!("398137383b3d25c92898c656696e41950e47316b").into();
let hash: H256 =
hex!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0").into();
let tx = Transaction::Legacy {
chain_id: Some(1),
nonce: 0x18,
gas_price: 0xfa56ea00,
gas_limit: 119902,
to: TransactionKind::Call( hex!("06012c8cf97bead5deae237070f9587f8e7a266d").into()),
value: 0x1c6bf526340000u64.into(),
input: hex!("f7d8c88300000000000000000000000000000000000000000000000000000000000cee6100000000000000000000000000000000000000000000000000000000000ac3e1").into(),
};
let sig = Signature {
r: hex!("2a378831cf81d99a3f06a18ae1b6ca366817ab4d88a70053c41d7a8f0368e031").into(),
s: hex!("450d831a05b6e418724436c05c155e0a1b7b921015d0fbc2f667aed709ac4fb5").into(),
odd_y_parity: false,
};
let signed_tx = TransactionSigned::from_transaction_and_signature(tx, sig);
assert_eq!(signed_tx.hash(), hash, "Expected same hash");
assert_eq!(signed_tx.recover_signer(), Some(signer), "Recovering signer should pass.");
}
#[test]
fn recover_signer_eip1559() {
use crate::hex_literal::hex;
let signer: Address = hex!("dd6b8b3dc6b7ad97db52f08a275ff4483e024cea").into();
let hash: H256 =
hex!("0ec0b6a2df4d87424e5f6ad2a654e27aaeb7dac20ae9e8385cc09087ad532ee0").into();
let tx = Transaction::Eip1559 {
chain_id: 1,
nonce: 0x42,
gas_limit: 44386,
to: TransactionKind::Call( hex!("6069a6c32cf691f5982febae4faf8a6f3ab2f0f6").into()),
value: 0.into(),
input: hex!("a22cb4650000000000000000000000005eee75727d804a2b13038928d36f8b188945a57a0000000000000000000000000000000000000000000000000000000000000000").into(),
max_fee_per_gas: 0x4a817c800,
max_priority_fee_per_gas: 0x3b9aca00,
access_list: AccessList::default(),
};
let sig = Signature {
r: hex!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565").into(),
s: hex!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1").into(),
odd_y_parity: false,
};
let signed_tx = TransactionSigned::from_transaction_and_signature(tx, sig);
assert_eq!(signed_tx.hash(), hash, "Expected same hash");
assert_eq!(signed_tx.recover_signer(), Some(signer), "Recovering signer should pass.");
}
}

View File

@ -1,8 +1,7 @@
use crate::{transaction::util::secp256k1, Address, H256, U256};
use reth_codecs::main_codec;
use reth_rlp::{Decodable, DecodeError, Encodable};
use crate::U256;
/// r, s: Values corresponding to the signature of the
/// transaction and used to determine the sender of
/// the transaction; formally Tr and Ts. This is expanded in Appendix F of yellow paper.
@ -68,4 +67,17 @@ impl Signature {
Ok((Signature { r, s, odd_y_parity }, None))
}
}
/// Recover signature from hash.
pub(crate) fn recover_signer(&self, hash: H256) -> Option<Address> {
let mut sig: [u8; 65] = [0; 65];
self.r.to_big_endian(&mut sig[0..32]);
self.s.to_big_endian(&mut sig[32..64]);
sig[64] = self.odd_y_parity as u8;
// NOTE: we are removing error from underlying crypto library as it will restrain primitive
// errors and we care only if recovery is passing or not.
secp256k1::recover(&sig, hash.as_fixed_bytes()).ok()
}
}

View File

@ -0,0 +1,36 @@
use crate::{keccak256, Address};
pub(crate) mod secp256k1 {
use ::secp256k1::{
ecdsa::{RecoverableSignature, RecoveryId},
Error, Message, Secp256k1,
};
use super::*;
/// secp256k1 signer recovery
pub(crate) fn recover(sig: &[u8; 65], msg: &[u8; 32]) -> Result<Address, Error> {
let sig =
RecoverableSignature::from_compact(&sig[0..64], RecoveryId::from_i32(sig[64] as i32)?)?;
let secp = Secp256k1::new();
let public = secp.recover_ecdsa(&Message::from_slice(&msg[..32])?, &sig)?;
let hash = keccak256(&public.serialize_uncompressed()[1..]);
Ok(Address::from_slice(&hash[12..]))
}
}
#[cfg(test)]
mod tests {
use super::secp256k1;
use crate::{hex_literal::hex, Address};
#[test]
fn sanity_ecrecover_call() {
let sig = hex!("650acf9d3f5f0a2c799776a1254355d5f4061762a237396a99a0e0e3fc2bcd6729514a0dacb2e623ac4abd157cb18163ff942280db4d5caad66ddf941ba12e0300");
let hash = hex!("47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad");
let out: Address = hex!("c08b5542d177ac6686946920409741463a15dddb").into();
assert_eq!(secp256k1::recover(&sig, &hash), Ok(out));
}
}