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
This commit is contained in:
Dan Cline 2022-04-08 22:05:16 -04:00 committed by GitHub
parent 247f08f1a9
commit 6e004e7780
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 246 additions and 61 deletions

View File

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

View File

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

View File

@ -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<Self, DecoderError> {
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<Self, rlp::DecoderError> {
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)
}
}

View File

@ -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()
}
/// Get a TypedTransaction directly from an rlp encoded byte stream
impl rlp::Decodable for TypedTransaction {
/// 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<U64> = 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 a rlp encoded byte stream
impl Decodable for TypedTransaction {
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
let tx_type: Option<U64> = 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() {

View File

@ -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<Self, DecoderError> {
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<Self, rlp::DecoderError> {
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)
}
}

View File

@ -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<Self, rlp::DecoderError> {
@ -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<Self, rlp::DecoderError> {
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::<Vec<Vec<u8>>>();
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);
}
}
}

View File

@ -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<Self, DecoderError> {
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"))?,
);