diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index d3fa574e811..0a3639f255c 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -24,6 +24,6 @@ mod receipt; pub use receipt::{Receipt, ReceiptEnvelope, ReceiptWithBloom}; mod transaction; -pub use transaction::{TxEip1559, TxEip2930, TxEnvelope, TxLegacy, TxType}; +pub use transaction::{TxEip1559, TxEip2930, TxEip4844, TxEnvelope, TxLegacy, TxType}; pub use alloy_network::TxKind; diff --git a/crates/consensus/src/receipt/envelope.rs b/crates/consensus/src/receipt/envelope.rs index f2114ca4986..134a2912ca3 100644 --- a/crates/consensus/src/receipt/envelope.rs +++ b/crates/consensus/src/receipt/envelope.rs @@ -26,6 +26,10 @@ pub enum ReceiptEnvelope { /// /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559 Eip1559(ReceiptWithBloom), + /// Receipt envelope with type flag 2, containing a [EIP-4844] receipt. + /// + /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 + Eip4844(ReceiptWithBloom), } impl ReceiptEnvelope { @@ -35,6 +39,7 @@ impl ReceiptEnvelope { Self::Legacy(_) | Self::TaggedLegacy(_) => TxType::Legacy, Self::Eip2930(_) => TxType::Eip2930, Self::Eip1559(_) => TxType::Eip1559, + Self::Eip4844(_) => TxType::Eip4844, } } @@ -42,9 +47,11 @@ impl ReceiptEnvelope { /// however, future receipt types may be added. pub const fn as_receipt_with_bloom(&self) -> Option<&ReceiptWithBloom> { match self { - Self::Legacy(t) | Self::TaggedLegacy(t) | Self::Eip2930(t) | Self::Eip1559(t) => { - Some(t) - } + Self::Legacy(t) + | Self::TaggedLegacy(t) + | Self::Eip2930(t) + | Self::Eip1559(t) + | Self::Eip4844(t) => Some(t), } } @@ -52,9 +59,11 @@ impl ReceiptEnvelope { /// receipt types may be added. pub const fn as_receipt(&self) -> Option<&Receipt> { match self { - Self::Legacy(t) | Self::TaggedLegacy(t) | Self::Eip2930(t) | Self::Eip1559(t) => { - Some(&t.receipt) - } + Self::Legacy(t) + | Self::TaggedLegacy(t) + | Self::Eip2930(t) + | Self::Eip1559(t) + | Self::Eip4844(t) => Some(&t.receipt), } } @@ -104,6 +113,7 @@ impl Encodable2718 for ReceiptEnvelope { Self::TaggedLegacy(_) => Some(TxType::Legacy as u8), Self::Eip2930(_) => Some(TxType::Eip2930 as u8), Self::Eip1559(_) => Some(TxType::Eip1559 as u8), + Self::Eip4844(_) => Some(TxType::Eip4844 as u8), } } @@ -127,6 +137,7 @@ impl Decodable2718 for ReceiptEnvelope { TxType::Legacy => Ok(Self::TaggedLegacy(receipt)), TxType::Eip2930 => Ok(Self::Eip2930(receipt)), TxType::Eip1559 => Ok(Self::Eip1559(receipt)), + TxType::Eip4844 => Ok(Self::Eip4844(receipt)), } } diff --git a/crates/consensus/src/transaction/eip4844.rs b/crates/consensus/src/transaction/eip4844.rs new file mode 100644 index 00000000000..22f73b8350a --- /dev/null +++ b/crates/consensus/src/transaction/eip4844.rs @@ -0,0 +1,343 @@ +use crate::{TxKind, TxType}; +use alloy_eips::{eip2930::AccessList, eip4844::DATA_GAS_PER_BLOB}; +use alloy_network::{Signed, Transaction}; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; +use std::mem; + +/// [EIP-4844 Blob Transaction](https://eips.ethereum.org/EIPS/eip-4844#blob-transaction) +/// +/// A transaction with blob hashes and max blob fee +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct TxEip4844 { + /// Added as EIP-pub 155: Simple replay attack protection + pub chain_id: ChainId, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasFeeCap` + pub max_fee_per_gas: u128, + /// Max Priority fee that transaction is paying + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasTipCap` + pub max_priority_fee_per_gas: u128, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: AccessList, + + /// It contains a vector of fixed size hash(32 bytes) + pub blob_versioned_hashes: Vec, + + /// Max fee per data gas + /// + /// aka BlobFeeCap or blobGasFeeCap + pub max_fee_per_blob_gas: u128, + + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxEip4844 { + /// Returns the effective gas price for the given `base_fee`. + pub const fn effective_gas_price(&self, base_fee: Option) -> u128 { + match base_fee { + None => self.max_fee_per_gas, + Some(base_fee) => { + // if the tip is greater than the max priority fee per gas, set it to the max + // priority fee per gas + base fee + let tip = self.max_fee_per_gas.saturating_sub(base_fee as u128); + if tip > self.max_priority_fee_per_gas { + self.max_priority_fee_per_gas + base_fee as u128 + } else { + // otherwise return the max fee per gas + self.max_fee_per_gas + } + } + } + } + + /// Returns the total gas for all blobs in this transaction. + #[inline] + pub fn blob_gas(&self) -> u64 { + // SAFETY: we don't expect u64::MAX / DATA_GAS_PER_BLOB hashes in a single transaction + self.blob_versioned_hashes.len() as u64 * DATA_GAS_PER_BLOB + } + + /// Decodes the inner [TxEip4844] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `max_priority_fee_per_gas` + /// - `max_fee_per_gas` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + /// - `max_fee_per_blob_gas` + /// - `blob_versioned_hashes` + pub fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + max_fee_per_blob_gas: Decodable::decode(buf)?, + blob_versioned_hashes: Decodable::decode(buf)?, + }) + } + + /// Outputs the length of the transaction's fields, without a RLP header. + pub(crate) fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.gas_limit.length(); + len += self.max_fee_per_gas.length(); + len += self.max_priority_fee_per_gas.length(); + len += self.to.length(); + len += self.value.length(); + len += self.access_list.length(); + len += self.blob_versioned_hashes.length(); + len += self.max_fee_per_blob_gas.length(); + len += self.input.0.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + self.max_fee_per_blob_gas.encode(out); + self.blob_versioned_hashes.encode(out); + } + + /// Calculates a heuristic for the in-memory size of the [TxEip4844] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_limit + mem::size_of::() + // max_fee_per_gas + mem::size_of::() + // max_priority_fee_per_gas + self.to.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() + // input + self.blob_versioned_hashes.capacity() * mem::size_of::() + // blob hashes size + mem::size_of::() // max_fee_per_data_gas + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + pub(crate) fn encode_with_signature( + &self, + signature: &Signature, + out: &mut dyn BufMut, + with_header: bool, + ) { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + if with_header { + Header { + list: false, + payload_length: 1 + length_of_length(payload_length) + payload_length, + } + .encode(out); + } + out.put_u8(self.tx_type() as u8); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.encode(out); + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP header. + pub fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.rlp_vrs_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Get transaction type + pub const fn tx_type(&self) -> TxType { + TxType::Eip4844 + } + + /// Encodes the legacy transaction in RLP for signing. + /// + /// This encodes the transaction as: + /// `tx_type || rlp(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, + /// value, input, access_list, max_fee_per_blob_gas, blob_versioned_hashes)` + /// + /// Note that there is no rlp header before the transaction type byte. + pub fn encode_for_signing(&self, out: &mut dyn BufMut) { + out.put_u8(self.tx_type() as u8); + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } +} + +impl Transaction for TxEip4844 { + type Signature = Signature; + + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + fn into_signed(self, signature: Signature) -> Signed { + let payload_length = 1 + self.fields_len() + signature.rlp_vrs_len(); + let mut buf = Vec::with_capacity(payload_length); + buf.put_u8(TxType::Eip1559 as u8); + self.encode_signed(&signature, &mut buf); + let hash = keccak256(&buf); + + // Drop any v chain id value to ensure the signature format is correct at the time of + // combination for an EIP-4844 transaction. V should indicate the y-parity of the + // signature. + Signed::new_unchecked(self, signature.with_parity_bool(), hash) + } + + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + let tx = Self::decode_inner(buf)?; + let signature = Signature::decode_rlp_vrs(buf)?; + + Ok(tx.into_signed(signature)) + } + + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { + self.encode_for_signing(out); + } + + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { + TxEip4844::encode_with_signature(self, signature, out, true); + } + + fn input(&self) -> &[u8] { + &self.input + } + + fn input_mut(&mut self) -> &mut Bytes { + &mut self.input + } + + fn set_input(&mut self, input: Bytes) { + self.input = input; + } + + fn to(&self) -> TxKind { + self.to + } + + fn set_to(&mut self, to: TxKind) { + self.to = to; + } + + fn value(&self) -> U256 { + self.value + } + + fn set_value(&mut self, value: U256) { + self.value = value; + } + + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.nonce = nonce; + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn set_gas_limit(&mut self, limit: u64) { + self.gas_limit = limit; + } + + fn gas_price(&self) -> Option { + None + } + + fn set_gas_price(&mut self, price: U256) { + let _ = price; + } +} diff --git a/crates/consensus/src/transaction/envelope.rs b/crates/consensus/src/transaction/envelope.rs index f89466d915c..de3f6b9d4e7 100644 --- a/crates/consensus/src/transaction/envelope.rs +++ b/crates/consensus/src/transaction/envelope.rs @@ -1,4 +1,4 @@ -use crate::{TxEip1559, TxEip2930, TxLegacy}; +use crate::{TxEip1559, TxEip2930, TxEip4844, TxLegacy}; use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Encodable2718}; use alloy_network::Signed; use alloy_rlp::{length_of_length, Decodable, Encodable}; @@ -9,6 +9,7 @@ use alloy_rlp::{length_of_length, Decodable, Encodable}; /// [2718]: https://eips.ethereum.org/EIPS/eip-2718 /// [1559]: https://eips.ethereum.org/EIPS/eip-1559 /// [2930]: https://eips.ethereum.org/EIPS/eip-2930 +/// [4844]: https://eips.ethereum.org/EIPS/eip-4844 #[repr(u8)] #[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] pub enum TxType { @@ -18,6 +19,8 @@ pub enum TxType { Eip2930 = 1, /// EIP-1559 transaction type. Eip1559 = 2, + /// EIP-4844 transaction type. + Eip4844 = 3, } #[cfg(any(test, feature = "arbitrary"))] @@ -27,6 +30,7 @@ impl<'a> arbitrary::Arbitrary<'a> for TxType { 0 => TxType::Legacy, 1 => TxType::Eip2930, 2 => TxType::Eip1559, + 3 => TxType::Eip4844, _ => unreachable!(), }) } @@ -38,7 +42,7 @@ impl TryFrom for TxType { fn try_from(value: u8) -> Result { match value { // SAFETY: repr(u8) with explicit discriminant - ..=2 => Ok(unsafe { std::mem::transmute(value) }), + ..=3 => Ok(unsafe { std::mem::transmute(value) }), _ => Err(Eip2718Error::UnexpectedType(value)), } } @@ -65,6 +69,8 @@ pub enum TxEnvelope { Eip2930(Signed), /// A [`TxEip1559`]. Eip1559(Signed), + /// A [`TxEip4844`]. + Eip4844(Signed), } impl From> for TxEnvelope { @@ -86,6 +92,7 @@ impl TxEnvelope { Self::Legacy(_) | Self::TaggedLegacy(_) => TxType::Legacy, Self::Eip2930(_) => TxType::Eip2930, Self::Eip1559(_) => TxType::Eip1559, + Self::Eip4844(_) => TxType::Eip4844, } } @@ -95,6 +102,7 @@ impl TxEnvelope { Self::Legacy(t) | Self::TaggedLegacy(t) => t.length(), Self::Eip2930(t) => t.length(), Self::Eip1559(t) => t.length(), + Self::Eip4844(t) => t.length(), } } @@ -140,6 +148,7 @@ impl Decodable2718 for TxEnvelope { TxType::Legacy => Ok(Self::TaggedLegacy(Decodable::decode(buf)?)), TxType::Eip2930 => Ok(Self::Eip2930(Decodable::decode(buf)?)), TxType::Eip1559 => Ok(Self::Eip1559(Decodable::decode(buf)?)), + TxType::Eip4844 => Ok(Self::Eip4844(Decodable::decode(buf)?)), } } @@ -155,6 +164,7 @@ impl Encodable2718 for TxEnvelope { Self::TaggedLegacy(_) => Some(TxType::Legacy as u8), Self::Eip2930(_) => Some(TxType::Eip2930 as u8), Self::Eip1559(_) => Some(TxType::Eip1559 as u8), + Self::Eip4844(_) => Some(TxType::Eip4844 as u8), } } @@ -177,6 +187,10 @@ impl Encodable2718 for TxEnvelope { out.put_u8(TxType::Eip1559 as u8); tx.encode(out); } + TxEnvelope::Eip4844(tx) => { + out.put_u8(TxType::Eip4844 as u8); + tx.encode(out); + } } } } @@ -229,6 +243,40 @@ mod tests { assert_eq!(from, address!("a12e1462d0ceD572f396F58B6E2D03894cD7C8a4")); } + #[test] + #[cfg(feature = "k256")] + // Test vector from https://sepolia.etherscan.io/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0 + // Blobscan: https://sepolia.blobscan.com/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0 + fn test_decode_live_4844_tx() { + use alloy_primitives::{address, b256}; + + // https://sepolia.etherscan.io/getRawTx?tx=0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0 + let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap(); + let res = TxEnvelope::decode(&mut raw_tx.as_slice()).unwrap(); + assert_eq!(res.tx_type(), TxType::Eip4844); + + let tx = match res { + TxEnvelope::Eip4844(tx) => tx, + _ => unreachable!(), + }; + + assert_eq!(tx.tx().to, TxKind::Call(address!("11E9CA82A3a762b4B5bd264d4173a242e7a77064"))); + + assert_eq!( + tx.tx().blob_versioned_hashes, + vec![ + b256!("012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a"), + b256!("0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4"), + b256!("013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7"), + b256!("01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1"), + b256!("011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549") + ] + ); + + let from = tx.recover_signer().unwrap(); + assert_eq!(from, address!("A83C816D4f9b2783761a22BA6FADB0eB0606D7B2")); + } + fn test_encode_decode_roundtrip(tx: T) where Signed: Into, diff --git a/crates/consensus/src/transaction/mod.rs b/crates/consensus/src/transaction/mod.rs index ec5b99d0c9d..5b782d8211d 100644 --- a/crates/consensus/src/transaction/mod.rs +++ b/crates/consensus/src/transaction/mod.rs @@ -7,5 +7,8 @@ pub use eip2930::TxEip2930; mod legacy; pub use legacy::TxLegacy; +mod eip4844; +pub use eip4844::TxEip4844; + mod envelope; pub use envelope::{TxEnvelope, TxType};