Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
feat(core): implemented signed transaction RLP decoding (#1096)
Browse files Browse the repository at this point in the history
* feat(core): implement signed transaction decoding

* add geth signed transaction test vectors

* add signed tx decoding CHANGELOG entry
  • Loading branch information
Rjected authored Apr 9, 2022
1 parent 247f08f commit 6e004e7
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 61 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion ethers-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
62 changes: 46 additions & 16 deletions ethers-core/src/types/transaction/eip1559.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down
102 changes: 96 additions & 6 deletions ethers-core/src/types/transaction/eip2718.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use super::{
eip1559::Eip1559TransactionRequest,
eip1559::{Eip1559RequestError, Eip1559TransactionRequest},
eip2930::{AccessList, Eip2930TransactionRequest},
request::RequestError,
};
use crate::{
types::{
Address, Bytes, NameOrAddress, Signature, Transaction, TransactionRequest, H256, U256, U64,
},
utils::keccak256,
};
use rlp::Decodable;
use serde::{Deserialize, Serialize};
use thiserror::Error;

/// The TypedTransaction enum represents all Ethereum transaction types.
///
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<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 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<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"))?,
);
Expand Down Expand Up @@ -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() {
Expand Down
54 changes: 24 additions & 30 deletions ethers-core/src/types/transaction/eip2930.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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)
}
}

Expand Down
Loading

0 comments on commit 6e004e7

Please sign in to comment.