feat(primitives): transaction encoding and decoding (#102)

* feat(core): transaction encoding and decoding

* cargo fmt

* remove printlns

* fix tx conversion
This commit is contained in:
Dan Cline
2022-10-19 13:31:41 -04:00
committed by GitHub
parent f672781bfc
commit 7e26ba5090
5 changed files with 401 additions and 21 deletions

View File

@ -1,5 +1,5 @@
use reth_interfaces::executor::ExecutorDb;
use reth_primitives::{BlockLocked, Transaction, H160, H256, U256};
use reth_primitives::{BlockLocked, Transaction, TransactionKind, H160, H256, U256};
use revm::{
db::{CacheDB, Database, EmptyDB},
BlockEnv, TransactTo, TxEnv,
@ -58,8 +58,10 @@ pub fn fill_tx_env(tx_env: &mut TxEnv, transaction: &Transaction) {
tx_env.gas_limit = *gas_limit;
tx_env.gas_price = (*gas_price).into();
tx_env.gas_priority_fee = None;
tx_env.transact_to =
if let Some(to) = to { TransactTo::Call(*to) } else { TransactTo::create() };
tx_env.transact_to = match to {
TransactionKind::Call(to) => TransactTo::Call(*to),
TransactionKind::Create => TransactTo::create(),
};
tx_env.value = *value;
tx_env.data = input.0.clone();
tx_env.chain_id = *chain_id;
@ -78,13 +80,16 @@ pub fn fill_tx_env(tx_env: &mut TxEnv, transaction: &Transaction) {
tx_env.gas_limit = *gas_limit;
tx_env.gas_price = (*gas_price).into();
tx_env.gas_priority_fee = None;
tx_env.transact_to =
if let Some(to) = to { TransactTo::Call(*to) } else { TransactTo::create() };
tx_env.transact_to = match to {
TransactionKind::Call(to) => TransactTo::Call(*to),
TransactionKind::Create => TransactTo::create(),
};
tx_env.value = *value;
tx_env.data = input.0.clone();
tx_env.chain_id = Some(*chain_id);
tx_env.nonce = Some(*nonce);
tx_env.access_list = access_list
.0
.iter()
.map(|l| {
(
@ -108,13 +113,16 @@ pub fn fill_tx_env(tx_env: &mut TxEnv, transaction: &Transaction) {
tx_env.gas_limit = *gas_limit;
tx_env.gas_price = (*max_fee_per_gas).into();
tx_env.gas_priority_fee = Some((*max_priority_fee_per_gas).into());
tx_env.transact_to =
if let Some(to) = to { TransactTo::Call(*to) } else { TransactTo::create() };
tx_env.transact_to = match to {
TransactionKind::Call(to) => TransactTo::Call(*to),
TransactionKind::Create => TransactTo::create(),
};
tx_env.value = *value;
tx_env.data = input.0.clone();
tx_env.chain_id = Some(*chain_id);
tx_env.nonce = Some(*nonce);
tx_env.access_list = access_list
.0
.iter()
.map(|l| {
(

View File

@ -7,7 +7,7 @@ use std::ops::Deref;
/// Block header
#[main_codec]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
pub struct Header {
/// The Keccak 256-bit hash of the parent
/// blocks header, in its entirety; formally Hp.
@ -174,7 +174,7 @@ impl Decodable for Header {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
/// HeaderLocked that has precalculated hash, use unlock if you want to modify header.
pub struct HeaderLocked {
/// Locked Header fields.
@ -213,7 +213,7 @@ impl HeaderLocked {
mod tests {
use crate::Address;
use super::{Decodable, Encodable, Header, H160, H256};
use super::{Decodable, Encodable, Header, H256};
use ethers_core::{
types::Bytes,
utils::hex::{self, FromHex},

View File

@ -23,7 +23,9 @@ pub use header::{Header, HeaderLocked};
pub use jsonu256::JsonU256;
pub use log::Log;
pub use receipt::Receipt;
pub use transaction::{AccessList, AccessListItem, Transaction, TransactionSigned, TxType};
pub use transaction::{
AccessList, AccessListItem, Transaction, TransactionKind, TransactionSigned, TxType,
};
/// Block hash.
pub type BlockHash = H256;
@ -45,5 +47,5 @@ pub type StorageValue = H256;
// NOTE: There is a benefit of using wrapped Bytes as it gives us serde and debug
pub use ethers_core::{
types as rpc,
types::{Bloom, Bytes, H128, H160, H256, H512, H64, U256, U64},
types::{Bloom, Bytes, H128, H160, H256, H512, H64, U128, U256, U64},
};

View File

@ -1,8 +1,10 @@
use reth_rlp::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper};
use crate::{Address, H256};
/// A list of addresses and storage keys that the transaction plans to access.
/// Accesses outside the list are possible, but become more expensive.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, RlpDecodable, RlpEncodable)]
pub struct AccessListItem {
/// Account addresses that would be loaded at the start of execution
pub address: Address,
@ -11,4 +13,5 @@ pub struct AccessListItem {
}
/// AccessList as defined in EIP-2930
pub type AccessList = Vec<AccessListItem>;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, RlpDecodableWrapper, RlpEncodableWrapper)]
pub struct AccessList(pub Vec<AccessListItem>);

View File

@ -4,13 +4,16 @@ mod tx_type;
use crate::{Address, Bytes, TxHash, U256};
pub use access_list::{AccessList, AccessListItem};
use bytes::Buf;
use ethers_core::utils::keccak256;
use reth_rlp::{length_of_length, Decodable, DecodeError, Encodable, Header, EMPTY_STRING_CODE};
use signature::Signature;
use std::ops::Deref;
pub use tx_type::TxType;
/// Raw Transaction.
/// Transaction type is introduced in EIP-2718: https://eips.ethereum.org/EIPS/eip-2718
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Transaction {
/// Legacy transaciton.
Legacy {
@ -30,7 +33,7 @@ pub enum Transaction {
gas_limit: u64,
/// The 160-bit address of the message calls recipient or, for a contract creation
/// transaction, ∅, used here to denote the only member of B0 ; formally Tt.
to: Option<Address>,
to: TransactionKind,
/// A scalar value equal to the number of Wei to
/// be transferred to the message calls recipient or,
/// in the case of contract creation, as an endowment
@ -61,7 +64,7 @@ pub enum Transaction {
gas_limit: u64,
/// The 160-bit address of the message calls recipient or, for a contract creation
/// transaction, ∅, used here to denote the only member of B0 ; formally Tt.
to: Option<Address>,
to: TransactionKind,
/// A scalar value equal to the number of Wei to
/// be transferred to the message calls recipient or,
/// in the case of contract creation, as an endowment
@ -102,7 +105,7 @@ pub enum Transaction {
max_priority_fee_per_gas: u64,
/// The 160-bit address of the message calls recipient or, for a contract creation
/// transaction, ∅, used here to denote the only member of B0 ; formally Tt.
to: Option<Address>,
to: TransactionKind,
/// A scalar value equal to the number of Wei to
/// be transferred to the message calls recipient or,
/// in the case of contract creation, as an endowment
@ -127,13 +130,262 @@ impl Transaction {
/// Heavy operation that return hash over rlp encoded transaction.
/// It is only used for signature signing.
pub fn signature_hash(&self) -> TxHash {
todo!()
let mut encoded = vec![];
self.encode(&mut encoded);
keccak256(encoded).into()
}
/// Sets the transaction's chain id to the provided value.
pub fn set_chain_id(&mut self, chain_id: u64) {
match self {
Transaction::Legacy { chain_id: ref mut c, .. } => *c = Some(chain_id),
Transaction::Eip2930 { chain_id: ref mut c, .. } => *c = chain_id,
Transaction::Eip1559 { chain_id: ref mut c, .. } => *c = chain_id,
}
}
/// Gets the transaction's [`TransactionKind`], which is the address of the recipient or
/// [`TransactionKind::Create`] if the transaction is a contract creation.
pub fn kind(&self) -> &TransactionKind {
match self {
Transaction::Legacy { to, .. } => to,
Transaction::Eip2930 { to, .. } => to,
Transaction::Eip1559 { to, .. } => to,
}
}
/// Gets the transaction's value field.
pub fn value(&self) -> &U256 {
match self {
Transaction::Legacy { value, .. } => value,
Transaction::Eip2930 { value, .. } => value,
Transaction::Eip1559 { value, .. } => value,
}
}
/// Get the transaction's nonce.
pub fn nonce(&self) -> u64 {
match self {
Transaction::Legacy { nonce, .. } => *nonce,
Transaction::Eip2930 { nonce, .. } => *nonce,
Transaction::Eip1559 { nonce, .. } => *nonce,
}
}
/// Get the transaction's input field.
pub fn input(&self) -> &Bytes {
match self {
Transaction::Legacy { input, .. } => input,
Transaction::Eip2930 { input, .. } => input,
Transaction::Eip1559 { input, .. } => input,
}
}
/// Encodes individual transaction fields into the desired buffer, without a RLP header or
/// EIP-155 fields.
pub(crate) fn encode_inner(&self, out: &mut dyn bytes::BufMut) {
match self {
Transaction::Legacy { chain_id: _, nonce, gas_price, gas_limit, to, value, input } => {
nonce.encode(out);
gas_price.encode(out);
gas_limit.encode(out);
to.encode(out);
value.encode(out);
input.0.encode(out);
}
Transaction::Eip2930 {
chain_id,
nonce,
gas_price,
gas_limit,
to,
value,
input,
access_list,
} => {
out.put_u8(1);
chain_id.encode(out);
nonce.encode(out);
gas_price.encode(out);
gas_limit.encode(out);
to.encode(out);
value.encode(out);
input.0.encode(out);
access_list.encode(out);
}
Transaction::Eip1559 {
chain_id,
nonce,
gas_limit,
max_fee_per_gas,
max_priority_fee_per_gas,
to,
value,
input,
access_list,
} => {
out.put_u8(2);
chain_id.encode(out);
nonce.encode(out);
gas_limit.encode(out);
max_fee_per_gas.encode(out);
max_priority_fee_per_gas.encode(out);
to.encode(out);
value.encode(out);
input.0.encode(out);
access_list.encode(out);
}
}
}
/// Encodes EIP-155 arguments into the desired buffer. Only encodes values for legacy
/// transactions.
pub(crate) fn encode_eip155_fields(&self, out: &mut dyn bytes::BufMut) {
// if this is a legacy transaction without a chain ID, it must be pre-EIP-155
// and does not need to encode the chain ID for the signature hash encoding
if let Transaction::Legacy { chain_id: Some(id), .. } = self {
// EIP-155 encodes the chain ID and two zeroes
id.encode(out);
0x00u8.encode(out);
0x00u8.encode(out);
}
}
/// Outputs the length of EIP-155 fields. Only outputs a non-zero value for EIP-155 legacy
/// transactions.
pub(crate) fn eip155_fields_len(&self) -> usize {
if let Transaction::Legacy { chain_id: Some(id), .. } = self {
// EIP-155 encodes the chain ID and two zeroes, so we add 2 to the length of the chain
// ID to get the length of all 3 fields
// len(chain_id) + (0x00) + (0x00)
id.length() + 2
} else {
// this is either a pre-EIP-155 legacy transaction or a typed transaction
0
}
}
/// Outputs the length of the transaction payload without the length of the RLP header or
/// EIP-155 fields.
pub(crate) fn payload_len(&self) -> usize {
match self {
Transaction::Legacy { chain_id: _, nonce, gas_price, gas_limit, to, value, input } => {
let mut len = 0;
len += nonce.length();
len += gas_price.length();
len += gas_limit.length();
len += to.length();
len += value.length();
len += input.0.length();
len
}
Transaction::Eip2930 {
chain_id,
nonce,
gas_price,
gas_limit,
to,
value,
input,
access_list,
} => {
let mut len = 0;
len += chain_id.length();
len += nonce.length();
len += gas_price.length();
len += gas_limit.length();
len += to.length();
len += value.length();
len += input.0.length();
len += access_list.length();
// add 1 for the transaction type
len + 1
}
Transaction::Eip1559 {
chain_id,
nonce,
gas_limit,
max_fee_per_gas,
max_priority_fee_per_gas,
to,
value,
input,
access_list,
} => {
let mut len = 0;
len += chain_id.length();
len += nonce.length();
len += gas_limit.length();
len += max_fee_per_gas.length();
len += max_priority_fee_per_gas.length();
len += to.length();
len += value.length();
len += input.0.length();
len += access_list.length();
// add 1 for the transaction type
len + 1
}
}
}
}
/// This encodes the transaction _without_ the signature, and is only suitable for creating a hash
/// intended for signing.
impl Encodable for Transaction {
fn length(&self) -> usize {
let len = self.payload_len() + self.eip155_fields_len();
len + length_of_length(len)
}
fn encode(&self, out: &mut dyn bytes::BufMut) {
let header = Header { list: true, payload_length: self.length() };
header.encode(out);
self.encode_inner(out);
self.encode_eip155_fields(out);
}
}
/// Whether or not the transaction is a contract creation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TransactionKind {
/// A transaction that creates a contract.
Create,
/// A transaction that calls a contract or transfer.
Call(Address),
}
impl Encodable for TransactionKind {
fn length(&self) -> usize {
match self {
TransactionKind::Call(to) => to.length(),
TransactionKind::Create => EMPTY_STRING_CODE.length(),
}
}
fn encode(&self, out: &mut dyn reth_rlp::BufMut) {
match self {
TransactionKind::Call(to) => to.encode(out),
TransactionKind::Create => EMPTY_STRING_CODE.encode(out),
}
}
}
impl Decodable for TransactionKind {
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
if let Some(&first) = buf.first() {
if first == EMPTY_STRING_CODE {
buf.advance(1);
Ok(TransactionKind::Create)
} else {
let addr = <Address as Decodable>::decode(buf)?;
Ok(TransactionKind::Call(addr))
}
} else {
Err(DecodeError::InputTooShort)
}
}
}
/// Signed transaction.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TransactionSigned {
transaction: Transaction,
hash: TxHash,
@ -154,6 +406,111 @@ impl Deref for TransactionSigned {
}
}
impl Encodable for TransactionSigned {
fn length(&self) -> usize {
let mut len = self.transaction.payload_len();
if let Transaction::Legacy { chain_id: None, .. } = self.transaction {
// if the transaction has no chain id then it is a pre-EIP-155 transaction
len += self.signature.payload_len();
} else {
let id = match self.transaction {
Transaction::Legacy { chain_id: Some(id), .. } => id,
Transaction::Eip2930 { chain_id, .. } => chain_id,
Transaction::Eip1559 { chain_id, .. } => chain_id,
// we handled this case above
_ => unreachable!(
"legacy transaction without chain id should have been handled above"
),
};
len += self.signature.eip155_payload_len(id);
}
// add the length of the RLP header
len + length_of_length(len)
}
fn encode(&self, out: &mut dyn bytes::BufMut) {
let header = Header { list: true, payload_length: self.length() };
header.encode(out);
self.transaction.encode_inner(out);
if let Transaction::Legacy { chain_id: None, .. } = self.transaction {
// if the transaction has no chain id then it is a pre-EIP-155 transaction
self.signature.encode_inner(out);
} else {
let id = match self.transaction {
Transaction::Legacy { chain_id: Some(id), .. } => id,
Transaction::Eip2930 { chain_id, .. } => chain_id,
Transaction::Eip1559 { chain_id, .. } => chain_id,
// we handled this case above
_ => unreachable!(
"legacy transaction without chain id should have been handled above"
),
};
self.signature.encode_eip155_inner(out, id);
}
}
}
/// This `Decodable` implementation only supports decoding the transaction format sent over p2p.
impl Decodable for TransactionSigned {
fn decode(buf: &mut &[u8]) -> Result<Self, DecodeError> {
// keep this around so we can use it to calculate the hash
let original_encoding = *buf;
let header = Header::decode(buf)?;
// if the transaction is encoded as a string then it is a typed transaction
if !header.list {
let tx_type = *buf
.first()
.ok_or(DecodeError::Custom("typed tx cannot be decoded from an empty slice"))?;
buf.advance(1);
// decode common fields
let transaction = match tx_type {
1 => Transaction::Eip2930 {
chain_id: Decodable::decode(buf)?,
nonce: Decodable::decode(buf)?,
gas_price: Decodable::decode(buf)?,
gas_limit: Decodable::decode(buf)?,
to: Decodable::decode(buf)?,
value: Decodable::decode(buf)?,
input: Bytes(Decodable::decode(buf)?),
access_list: Decodable::decode(buf)?,
},
2 => Transaction::Eip1559 {
chain_id: Decodable::decode(buf)?,
nonce: Decodable::decode(buf)?,
gas_limit: Decodable::decode(buf)?,
to: Decodable::decode(buf)?,
value: Decodable::decode(buf)?,
input: Bytes(Decodable::decode(buf)?),
access_list: Decodable::decode(buf)?,
max_fee_per_gas: Decodable::decode(buf)?,
max_priority_fee_per_gas: Decodable::decode(buf)?,
},
_ => return Err(DecodeError::Custom("unsupported typed transaction type")),
};
let (signature, _) = Signature::decode_eip155_inner(buf)?;
let hash = keccak256(original_encoding).into();
Ok(TransactionSigned { transaction, hash, signature })
} else {
let mut transaction = Transaction::Legacy {
nonce: Decodable::decode(buf)?,
gas_price: Decodable::decode(buf)?,
gas_limit: Decodable::decode(buf)?,
to: Decodable::decode(buf)?,
value: Decodable::decode(buf)?,
input: Bytes(Decodable::decode(buf)?),
chain_id: None,
};
let (signature, extracted_id) = Signature::decode_eip155_inner(buf)?;
if let Some(id) = extracted_id {
transaction.set_chain_id(id);
}
let hash = keccak256(original_encoding).into();
Ok(TransactionSigned { transaction, hash, signature })
}
}
}
impl TransactionSigned {
/// Transaction signature.
pub fn signature(&self) -> &Signature {
@ -164,4 +521,14 @@ impl TransactionSigned {
pub fn hash(&self) -> TxHash {
self.hash
}
/// 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
}
}