From 6e004e77802e0895c8861ab8382c635f99429299 Mon Sep 17 00:00:00 2001 From: Dan Cline Date: Fri, 8 Apr 2022 22:05:16 -0400 Subject: [PATCH] feat(core): implemented signed transaction RLP decoding (#1096) * feat(core): implement signed transaction decoding * add geth signed transaction test vectors * add signed tx decoding CHANGELOG entry --- CHANGELOG.md | 1 + ethers-core/Cargo.toml | 2 +- ethers-core/src/types/transaction/eip1559.rs | 62 ++++++++--- ethers-core/src/types/transaction/eip2718.rs | 102 ++++++++++++++++-- ethers-core/src/types/transaction/eip2930.rs | 54 +++++----- ethers-core/src/types/transaction/request.rs | 79 ++++++++++++-- ethers-core/src/types/transaction/response.rs | 7 +- 7 files changed, 246 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edba032b..07c18d83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Pass compilation time as additional argument to `Reporter::on_solc_success` [1098](https://github.com/gakonst/ethers-rs/pull/1098) - Fix aws signer bug which maps un-normalized signature to error if no normalization occurs (in `aws::utils::decode_signature`) +- Implement signed transaction RLP decoding [#1096](https://github.com/gakonst/ethers-rs/pull/1096) - `Transaction::from` will default to `Address::zero()`. Add `recover_from` and `recover_from_mut` methods for recovering the sender from signature, and also setting the same on tx [1075](https://github.com/gakonst/ethers-rs/pull/1075). diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index b95adb50..bd11e44e 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/gakonst/ethers-rs" keywords = ["ethereum", "web3", "celo", "ethers"] [dependencies] -rlp = { version = "0.5.0", default-features = false } +rlp = { version = "0.5.0", default-features = false, features = ["std"] } ethabi = { version = "17.0.0", default-features = false, features = ["full-serde", "rlp"] } arrayvec = { version = "0.7.2", default-features = false } rlp-derive = { version = "0.1.0", default-features = false } diff --git a/ethers-core/src/types/transaction/eip1559.rs b/ethers-core/src/types/transaction/eip1559.rs index cea028c9..2634ff1d 100644 --- a/ethers-core/src/types/transaction/eip1559.rs +++ b/ethers-core/src/types/transaction/eip1559.rs @@ -1,14 +1,29 @@ use super::{decode_to, eip2930::AccessList, normalize_v, rlp_opt}; use crate::{ - types::{Address, Bytes, NameOrAddress, Signature, Transaction, H256, U256, U64}, + types::{ + Address, Bytes, NameOrAddress, Signature, SignatureError, Transaction, H256, U256, U64, + }, utils::keccak256, }; use rlp::{Decodable, DecoderError, RlpStream}; +use thiserror::Error; /// EIP-1559 transactions have 9 fields const NUM_TX_FIELDS: usize = 9; use serde::{Deserialize, Serialize}; + +/// An error involving an EIP1559 transaction request. +#[derive(Debug, Error)] +pub enum Eip1559RequestError { + /// When decoding a transaction request from RLP + #[error(transparent)] + DecodingError(#[from] rlp::DecoderError), + /// When recovering the address from a signature + #[error(transparent)] + RecoveryError(#[from] SignatureError), +} + /// Parameters for sending a transaction #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Eip1559TransactionRequest { @@ -188,38 +203,53 @@ impl Eip1559TransactionRequest { /// Decodes fields of the request starting at the RLP offset passed. Increments the offset for /// each element parsed. #[inline] - fn decode_base_rlp(&mut self, rlp: &rlp::Rlp, offset: &mut usize) -> Result<(), DecoderError> { - self.chain_id = Some(rlp.val_at(*offset)?); + pub fn decode_base_rlp(rlp: &rlp::Rlp, offset: &mut usize) -> Result { + let mut tx = Self::new(); + tx.chain_id = Some(rlp.val_at(*offset)?); *offset += 1; - self.nonce = Some(rlp.val_at(*offset)?); + tx.nonce = Some(rlp.val_at(*offset)?); *offset += 1; - self.max_priority_fee_per_gas = Some(rlp.val_at(*offset)?); + tx.max_priority_fee_per_gas = Some(rlp.val_at(*offset)?); *offset += 1; - self.max_fee_per_gas = Some(rlp.val_at(*offset)?); + tx.max_fee_per_gas = Some(rlp.val_at(*offset)?); *offset += 1; - self.gas = Some(rlp.val_at(*offset)?); + tx.gas = Some(rlp.val_at(*offset)?); *offset += 1; - self.to = decode_to(rlp, offset)?; - self.value = Some(rlp.val_at(*offset)?); + tx.to = decode_to(rlp, offset)?; + tx.value = Some(rlp.val_at(*offset)?); *offset += 1; let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; - self.data = match data.len() { + tx.data = match data.len() { 0 => None, _ => Some(Bytes::from(data.to_vec())), }; *offset += 1; - self.access_list = rlp.val_at(*offset)?; + tx.access_list = rlp.val_at(*offset)?; *offset += 1; - Ok(()) + Ok(tx) + } + + /// Decodes the given RLP into a transaction, attempting to decode its signature as well. + pub fn decode_signed_rlp(rlp: &rlp::Rlp) -> Result<(Self, Signature), Eip1559RequestError> { + let mut offset = 0; + let mut txn = Self::decode_base_rlp(rlp, &mut offset)?; + + let v = rlp.at(offset)?.as_val()?; + offset += 1; + let r = rlp.at(offset)?.as_val()?; + offset += 1; + let s = rlp.at(offset)?.as_val()?; + + let sig = Signature { r, s, v }; + txn.from = Some(sig.recover(txn.sighash())?); + + Ok((txn, sig)) } } impl Decodable for Eip1559TransactionRequest { fn decode(rlp: &rlp::Rlp) -> Result { - let mut txn = Eip1559TransactionRequest::new(); - let mut offset = 0; - txn.decode_base_rlp(rlp, &mut offset)?; - Ok(txn) + Self::decode_base_rlp(rlp, &mut 0) } } diff --git a/ethers-core/src/types/transaction/eip2718.rs b/ethers-core/src/types/transaction/eip2718.rs index a64ef657..92e08f10 100644 --- a/ethers-core/src/types/transaction/eip2718.rs +++ b/ethers-core/src/types/transaction/eip2718.rs @@ -1,6 +1,7 @@ use super::{ - eip1559::Eip1559TransactionRequest, + eip1559::{Eip1559RequestError, Eip1559TransactionRequest}, eip2930::{AccessList, Eip2930TransactionRequest}, + request::RequestError, }; use crate::{ types::{ @@ -8,7 +9,9 @@ use crate::{ }, utils::keccak256, }; +use rlp::Decodable; use serde::{Deserialize, Serialize}; +use thiserror::Error; /// The TypedTransaction enum represents all Ethereum transaction types. /// @@ -36,6 +39,26 @@ pub enum TypedTransaction { Eip1559(Eip1559TransactionRequest), } +/// An error involving a typed transaction request. +#[derive(Debug, Error)] +pub enum TypedTransactionError { + /// When decoding a signed legacy transaction + #[error(transparent)] + LegacyError(#[from] RequestError), + /// When decoding a signed Eip1559 transaction + #[error(transparent)] + Eip1559Error(#[from] Eip1559RequestError), + /// Error decoding the transaction type from the transaction's RLP encoding + #[error(transparent)] + TypeDecodingError(#[from] rlp::DecoderError), + /// Missing transaction type when decoding from RLP + #[error("Missing transaction type when decoding")] + MissingTransactionType, + /// Missing transaction payload when decoding from RLP + #[error("Missing transaction payload when decoding")] + MissingTransactionPayload, +} + #[cfg(feature = "legacy")] impl Default for TypedTransaction { fn default() -> Self { @@ -265,15 +288,51 @@ impl TypedTransaction { _ => None, } } + + /// Hashes the transaction's data with the included signature. + pub fn hash(&self, signature: &Signature) -> H256 { + keccak256(&self.rlp_signed(signature).as_ref()).into() + } + + /// Decodes a signed TypedTransaction from a rlp encoded byte stream + pub fn decode_signed(rlp: &rlp::Rlp) -> Result<(Self, Signature), TypedTransactionError> { + let tx_type: Option = match rlp.is_data() { + true => Ok(Some(rlp.data()?.into())), + false => Err(TypedTransactionError::MissingTransactionType), + }?; + + let rest = rlp::Rlp::new( + rlp.as_raw().get(1..).ok_or(TypedTransactionError::MissingTransactionPayload)?, + ); + + match tx_type { + Some(x) if x == U64::from(1u64) => { + // EIP-2930 (0x01) + let decoded_request = Eip2930TransactionRequest::decode_signed_rlp(&rest)?; + Ok((Self::Eip2930(decoded_request.0), decoded_request.1)) + } + Some(x) if x == U64::from(2u64) => { + // EIP-1559 (0x02) + let decoded_request = Eip1559TransactionRequest::decode_signed_rlp(&rest)?; + Ok((Self::Eip1559(decoded_request.0), decoded_request.1)) + } + _ => { + // Legacy (0x00) + // use the original rlp + let decoded_request = TransactionRequest::decode_signed_rlp(&rest)?; + Ok((Self::Legacy(decoded_request.0), decoded_request.1)) + } + } + } } -/// Get a TypedTransaction directly from an rlp encoded byte stream -impl rlp::Decodable for TypedTransaction { +/// Get a TypedTransaction directly from a rlp encoded byte stream +impl Decodable for TypedTransaction { fn decode(rlp: &rlp::Rlp) -> Result { let tx_type: Option = match rlp.is_data() { - true => Some(rlp.data().unwrap().into()), - false => None, - }; + true => Ok(Some(rlp.data()?.into())), + false => Ok(None), + }?; let rest = rlp::Rlp::new( rlp.as_raw().get(1..).ok_or(rlp::DecoderError::Custom("no transaction payload"))?, ); @@ -439,6 +498,37 @@ mod tests { assert_eq!(expected, actual); } + #[test] + fn test_signed_tx_decode() { + let expected_tx = Eip1559TransactionRequest::new() + .from(Address::from_str("0x27519a1d088898e04b12f9fb9733267a5e61481e").unwrap()) + .chain_id(1u64) + .nonce(0u64) + .max_priority_fee_per_gas(413047990155u64) + .max_fee_per_gas(768658734568u64) + .gas(184156u64) + .to(Address::from_str("0x0aa7420c43b8c1a7b165d216948870c8ecfe1ee1").unwrap()) + .value(200000000000000000u64) + .data( + Bytes::from_str( + "0x6ecd23060000000000000000000000000000000000000000000000000000000000000002", + ) + .unwrap(), + ); + + let expected_envelope = TypedTransaction::Eip1559(expected_tx); + let typed_tx_hex = hex::decode("02f899018085602b94278b85b2f7a17de88302cf5c940aa7420c43b8c1a7b165d216948870c8ecfe1ee18802c68af0bb140000a46ecd23060000000000000000000000000000000000000000000000000000000000000002c080a0c5f35bf1cc6ab13053e33b1af7400c267be17218aeadcdb4ae3eefd4795967e8a04f6871044dd6368aea8deecd1c29f55b5531020f5506502e3f79ad457051bc4a").unwrap(); + + let tx_rlp = rlp::Rlp::new(typed_tx_hex.as_slice()); + let (actual_tx, signature) = TypedTransaction::decode_signed(&tx_rlp).unwrap(); + assert_eq!(expected_envelope, actual_tx); + assert_eq!( + expected_envelope.hash(&signature), + H256::from_str("0x206e4c71335333f8658e995cc0c4ee54395d239acb08587ab8e5409bfdd94a6f") + .unwrap() + ); + } + #[cfg(not(feature = "celo"))] #[test] fn test_eip155_decode() { diff --git a/ethers-core/src/types/transaction/eip2930.rs b/ethers-core/src/types/transaction/eip2930.rs index 39c3e2b6..31581826 100644 --- a/ethers-core/src/types/transaction/eip2930.rs +++ b/ethers-core/src/types/transaction/eip2930.rs @@ -1,6 +1,5 @@ -use super::{decode_to, normalize_v, request::TransactionRequest}; -use crate::types::{Address, Bytes, Signature, Transaction, H256, U256, U64}; - +use super::{extract_chain_id, normalize_v}; +use crate::types::{Address, Bytes, Signature, Transaction, TransactionRequest, H256, U256, U64}; use rlp::{Decodable, DecoderError, RlpStream}; use rlp_derive::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper}; use serde::{Deserialize, Serialize}; @@ -104,42 +103,37 @@ impl Eip2930TransactionRequest { rlp.out().freeze().into() } - /// Decodes fields based on the RLP offset passed - fn decode_base_rlp(&mut self, rlp: &rlp::Rlp, offset: &mut usize) -> Result<(), DecoderError> { - self.tx.chain_id = Some(rlp.val_at(*offset)?); - *offset += 1; - self.tx.nonce = Some(rlp.val_at(*offset)?); - *offset += 1; - self.tx.gas_price = Some(rlp.val_at(*offset)?); - *offset += 1; - self.tx.gas = Some(rlp.val_at(*offset)?); + /// Decodes fields based on the RLP offset passed. + fn decode_base_rlp(rlp: &rlp::Rlp, offset: &mut usize) -> Result { + let request = TransactionRequest::decode_unsigned_rlp_base(rlp, offset)?; + let access_list = rlp.val_at(*offset)?; *offset += 1; - self.tx.gas = Some(rlp.val_at(*offset)?); - *offset += 1; - self.tx.to = decode_to(rlp, offset)?; - self.tx.value = Some(rlp.val_at(*offset)?); - *offset += 1; - let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?; - self.tx.data = match data.len() { - 0 => None, - _ => Some(Bytes::from(data.to_vec())), - }; - *offset += 1; - self.access_list = rlp.val_at(*offset)?; - *offset += 1; + Ok(Self { tx: request, access_list }) + } - Ok(()) + /// Decodes the given RLP into a transaction, attempting to decode its signature as well. + pub fn decode_signed_rlp(rlp: &rlp::Rlp) -> Result<(Self, Signature), rlp::DecoderError> { + let mut offset = 0; + let mut txn = Self::decode_base_rlp(rlp, &mut offset)?; + + let v = rlp.at(offset)?.as_val()?; + // populate chainid from v + txn.tx.chain_id = extract_chain_id(v); + offset += 1; + let r = rlp.at(offset)?.as_val()?; + offset += 1; + let s = rlp.at(offset)?.as_val()?; + + let sig = Signature { r, s, v }; + Ok((txn, sig)) } } /// Get a Eip2930TransactionRequest from a rlp encoded byte stream impl Decodable for Eip2930TransactionRequest { fn decode(rlp: &rlp::Rlp) -> Result { - let mut new_tx = Self::new(TransactionRequest::new(), AccessList::default()); - let mut offset = 0; - new_tx.decode_base_rlp(rlp, &mut offset)?; - Ok(new_tx) + Self::decode_base_rlp(rlp, &mut 0) } } diff --git a/ethers-core/src/types/transaction/request.rs b/ethers-core/src/types/transaction/request.rs index 2068645f..fd0a1359 100644 --- a/ethers-core/src/types/transaction/request.rs +++ b/ethers-core/src/types/transaction/request.rs @@ -1,12 +1,26 @@ //! Transaction types use super::{decode_to, extract_chain_id, rlp_opt, NUM_TX_FIELDS}; use crate::{ - types::{Address, Bytes, NameOrAddress, Signature, Transaction, H256, U256, U64}, + types::{ + Address, Bytes, NameOrAddress, Signature, SignatureError, Transaction, H256, U256, U64, + }, utils::keccak256, }; use rlp::{Decodable, RlpStream}; use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// An error involving a transaction request. +#[derive(Debug, Error)] +pub enum RequestError { + /// When decoding a transaction request from RLP + #[error(transparent)] + DecodingError(#[from] rlp::DecoderError), + /// When recovering the address from a signature + #[error(transparent)] + RecoveryError(#[from] SignatureError), +} /// Parameters for sending a transaction #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Debug)] @@ -195,7 +209,7 @@ impl TransactionRequest { /// Decodes the unsigned rlp, returning the transaction request and incrementing the counter /// passed as we are traversing the rlp list. - fn decode_unsigned_rlp_base( + pub(crate) fn decode_unsigned_rlp_base( rlp: &rlp::Rlp, offset: &mut usize, ) -> Result { @@ -249,12 +263,12 @@ impl TransactionRequest { } /// Decodes the given RLP into a transaction, attempting to decode its signature as well. - pub fn decode_signed_rlp(rlp: &rlp::Rlp) -> Result<(Self, Signature), rlp::DecoderError> { + pub fn decode_signed_rlp(rlp: &rlp::Rlp) -> Result<(Self, Signature), RequestError> { let mut offset = 0; let mut txn = Self::decode_unsigned_rlp_base(rlp, &mut offset)?; let v = rlp.at(offset)?.as_val()?; - // populate chainid from v + // populate chainid from v in case the signature follows EIP155 txn.chain_id = extract_chain_id(v); offset += 1; let r = rlp.at(offset)?.as_val()?; @@ -262,6 +276,8 @@ impl TransactionRequest { let s = rlp.at(offset)?.as_val()?; let sig = Signature { r, s, v }; + txn.from = Some(sig.recover(txn.sighash())?); + Ok((txn, sig)) } } @@ -269,7 +285,7 @@ impl TransactionRequest { impl Decodable for TransactionRequest { /// Decodes the given RLP into a transaction request, ignoring the signature if populated fn decode(rlp: &rlp::Rlp) -> Result { - TransactionRequest::decode_unsigned_rlp(rlp) + Self::decode_unsigned_rlp(rlp) } } @@ -335,10 +351,11 @@ impl TransactionRequest { #[cfg(test)] #[cfg(not(feature = "celo"))] mod tests { - use crate::types::Signature; + use crate::types::{NameOrAddress, Signature}; use rlp::{Decodable, Rlp}; use super::{Address, TransactionRequest, U256, U64}; + use std::str::FromStr; #[test] fn encode_decode_rlp() { @@ -469,4 +486,54 @@ mod tests { assert_eq!(expected_sig, decoded_sig); assert_eq!(decoded_tx.chain_id, Some(U64::from(1))); } + + #[test] + fn test_eip155_signing_decode_vitalik() { + // Test vectors come from http://vitalik.ca/files/eip155_testvec.txt and + // https://github.com/ethereum/go-ethereum/blob/master/core/types/transaction_signing_test.go + // Tests that the rlp decoding properly extracts the from address + let rlp_transactions = + vec!["f864808504a817c800825208943535353535353535353535353535353535353535808025a0044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116da0044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d", + "f864018504a817c80182a410943535353535353535353535353535353535353535018025a0489efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bcaa0489efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6", + "f864028504a817c80282f618943535353535353535353535353535353535353535088025a02d7c5bef027816a800da1736444fb58a807ef4c9603b7848673f7e3a68eb14a5a02d7c5bef027816a800da1736444fb58a807ef4c9603b7848673f7e3a68eb14a5", + "f865038504a817c803830148209435353535353535353535353535353535353535351b8025a02a80e1ef1d7842f27f2e6be0972bb708b9a135c38860dbe73c27c3486c34f4e0a02a80e1ef1d7842f27f2e6be0972bb708b9a135c38860dbe73c27c3486c34f4de", + "f865048504a817c80483019a28943535353535353535353535353535353535353535408025a013600b294191fc92924bb3ce4b969c1e7e2bab8f4c93c3fc6d0a51733df3c063a013600b294191fc92924bb3ce4b969c1e7e2bab8f4c93c3fc6d0a51733df3c060", + "f865058504a817c8058301ec309435353535353535353535353535353535353535357d8025a04eebf77a833b30520287ddd9478ff51abbdffa30aa90a8d655dba0e8a79ce0c1a04eebf77a833b30520287ddd9478ff51abbdffa30aa90a8d655dba0e8a79ce0c1", + "f866068504a817c80683023e3894353535353535353535353535353535353535353581d88025a06455bf8ea6e7463a1046a0b52804526e119b4bf5136279614e0b1e8e296a4e2fa06455bf8ea6e7463a1046a0b52804526e119b4bf5136279614e0b1e8e296a4e2d", + "f867078504a817c807830290409435353535353535353535353535353535353535358201578025a052f1a9b320cab38e5da8a8f97989383aab0a49165fc91c737310e4f7e9821021a052f1a9b320cab38e5da8a8f97989383aab0a49165fc91c737310e4f7e9821021", + "f867088504a817c8088302e2489435353535353535353535353535353535353535358202008025a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c12a064b1702d9298fee62dfeccc57d322a463ad55ca201256d01f62b45b2e1c21c10", + "f867098504a817c809830334509435353535353535353535353535353535353535358202d98025a052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afba052f8f61201b2b11a78d6e866abc9c3db2ae8631fa656bfe5cb53668255367afb"]; + let rlp_transactions_bytes = rlp_transactions + .iter() + .map(|rlp_str| hex::decode(rlp_str).unwrap()) + .collect::>>(); + + let raw_addresses = vec![ + "0xf0f6f18bca1b28cd68e4357452947e021241e9ce", + "0x23ef145a395ea3fa3deb533b8a9e1b4c6c25d112", + "0x2e485e0c23b4c3c542628a5f672eeab0ad4888be", + "0x82a88539669a3fd524d669e858935de5e5410cf0", + "0xf9358f2538fd5ccfeb848b64a96b743fcc930554", + "0xa8f7aba377317440bc5b26198a363ad22af1f3a4", + "0xf1f571dc362a0e5b2696b8e775f8491d3e50de35", + "0xd37922162ab7cea97c97a87551ed02c9a38b7332", + "0x9bddad43f934d313c2b79ca28a432dd2b7281029", + "0x3c24d7329e92f84f08556ceb6df1cdb0104ca49f", + ]; + + let addresses = raw_addresses + .iter() + .map(|addr| NameOrAddress::Address(Address::from_str(*addr).unwrap())); + + // decoding will do sender recovery and we don't expect any of these to error, so we should + // check that the address matches for each decoded transaction + let decoded_transactions = rlp_transactions_bytes.iter().map(|raw_tx| { + TransactionRequest::decode_signed_rlp(&Rlp::new(raw_tx.as_slice())).unwrap().0 + }); + + for (tx, from_addr) in decoded_transactions.zip(addresses) { + let from_tx: NameOrAddress = tx.from.unwrap().into(); + assert_eq!(from_tx, from_addr); + } + } } diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs index 84b9dfde..addc59d6 100644 --- a/ethers-core/src/types/transaction/response.rs +++ b/ethers-core/src/types/transaction/response.rs @@ -330,13 +330,16 @@ impl Transaction { } } -/// Get a TransactionReceipt directly from an rlp encoded byte stream +/// Get a Transaction directly from a rlp encoded byte stream impl Decodable for Transaction { fn decode(rlp: &rlp::Rlp) -> Result { let mut txn = Self::default(); // we can get the type from the first value let mut offset = 0; - txn.transaction_type = Some(rlp.data().unwrap().into()); + txn.transaction_type = match rlp.is_data() { + true => Ok(Some(rlp.data()?.into())), + false => Ok(None), + }?; let rest = rlp::Rlp::new( rlp.as_raw().get(1..).ok_or(DecoderError::Custom("no transaction payload"))?, );