Skip to content

Commit

Permalink
fix: add support for blob transaction types
Browse files Browse the repository at this point in the history
  • Loading branch information
njgheorghita authored and KolbyML committed Mar 15, 2024
1 parent f31d43f commit e0dc410
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 10 deletions.
2 changes: 2 additions & 0 deletions ethportal-api/src/types/execution/block_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ mod tests {
#[case(TX17, 1544975)]
// EIP1559 w/ populated access list
#[case(TX6, 41942)]
// todo blob
fn encode_and_decode_txs(#[case] tx: &str, #[case] expected_nonce: u32) {
let tx_rlp = hex_decode(tx).unwrap();
let tx = rlp::decode(&tx_rlp).expect("error decoding tx");
Expand All @@ -478,6 +479,7 @@ mod tests {
Transaction::Legacy(tx) => assert_eq!(tx.nonce, expected_nonce),
Transaction::AccessList(tx) => assert_eq!(tx.nonce, expected_nonce),
Transaction::EIP1559(tx) => assert_eq!(tx.nonce, expected_nonce),
Transaction::Blob(tx) => assert_eq!(tx.nonce, expected_nonce),
}
let encoded_tx = rlp::encode(&tx);
assert_eq!(hex_encode(tx_rlp), hex_encode(encoded_tx));
Expand Down
100 changes: 92 additions & 8 deletions ethportal-api/src/types/execution/header.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ethereum_types::{Bloom, H160, H256, H64, U256};
use ethereum_types::{Bloom, H160, H256, H64, U256, U64};
use reth_rpc_types::Header as RpcHeader;
use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream};
use ruint::Uint;
Expand All @@ -13,6 +13,7 @@ use crate::{

const LONDON_BLOCK_NUMBER: u64 = 12965000;
const SHANGHAI_BLOCK_NUMBER: u64 = 17034871;
const DENCUN_BLOCK_NUMBER: u64 = 19426589; // double-check

/// A block header.
#[derive(Debug, Clone, Eq, Deserialize, Serialize)]
Expand Down Expand Up @@ -58,6 +59,10 @@ pub struct Header {
pub base_fee_per_gas: Option<U256>,
/// Withdrawals root from execution payload. Introduced by EIP-4895.
pub withdrawals_root: Option<H256>,
/// Blob gas used. Introduced by EIP-4844
pub blob_gas_used: Option<U64>,
/// Excess blob gas. Introduced by EIP-4844
pub excess_blob_gas: Option<U64>,
}

fn se_hex<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
Expand Down Expand Up @@ -93,7 +98,10 @@ impl Header {

/// Append header to RLP stream `s`, optionally `with_seal`.
fn stream_rlp(&self, s: &mut RlpStream, with_seal: bool) {
let stream_length_without_seal = if self.withdrawals_root.is_some() {
let stream_length_without_seal = if self.excess_blob_gas.is_some() {
// add two for excess_blob_gas and blob_gas_used
17
} else if self.withdrawals_root.is_some() {
15
} else if self.base_fee_per_gas.is_some() {
14
Expand Down Expand Up @@ -121,18 +129,26 @@ impl Header {
.append(&self.timestamp)
.append(&self.extra_data);

// naked unwraps ok since we validate that properties exist before unwrapping
#[allow(clippy::unwrap_used)]
if with_seal && self.mix_hash.is_some() && self.nonce.is_some() {
s.append(&self.mix_hash.unwrap())
.append(self.nonce.as_ref().unwrap());
s.append(&self.mix_hash.expect("mix_hash to be Some"))
.append(self.nonce.as_ref().expect("nonce to be Some"));
}

if let Some(val) = self.base_fee_per_gas {
s.append(&val);
}

if let Some(val) = self.withdrawals_root {
s.append(&val);
}

if let Some(val) = self.blob_gas_used {
s.append(&val);
}

if let Some(val) = self.excess_blob_gas {
s.append(&val);
}
}
}

Expand All @@ -157,6 +173,8 @@ impl Decodable for Header {
nonce: Some(rlp.val_at(14)?),
base_fee_per_gas: None,
withdrawals_root: None,
blob_gas_used: None,
excess_blob_gas: None,
};

if header.number >= LONDON_BLOCK_NUMBER {
Expand All @@ -167,6 +185,11 @@ impl Decodable for Header {
header.withdrawals_root = Some(rlp.val_at(16)?);
}

if header.number >= DENCUN_BLOCK_NUMBER {
header.blob_gas_used = Some(rlp.val_at(17)?);
header.excess_blob_gas = Some(rlp.val_at(18)?);
}

Ok(header)
}
}
Expand Down Expand Up @@ -196,6 +219,8 @@ impl PartialEq for Header {
&& self.nonce == other.nonce
&& self.base_fee_per_gas == other.base_fee_per_gas
&& self.withdrawals_root == other.withdrawals_root
&& self.blob_gas_used == other.blob_gas_used
&& self.excess_blob_gas == other.excess_blob_gas
}
}

Expand Down Expand Up @@ -224,6 +249,8 @@ impl From<Header> for RpcHeader {
nonce,
base_fee_per_gas,
withdrawals_root,
blob_gas_used,
excess_blob_gas,
} = header;

Self {
Expand Down Expand Up @@ -252,8 +279,8 @@ impl From<Header> for RpcHeader {
nonce: nonce.map(|h64| h64.as_fixed_bytes().into()),
base_fee_per_gas: base_fee_per_gas.map(u256_to_uint256),
withdrawals_root: withdrawals_root.map(|root| root.to_fixed_bytes().into()),
blob_gas_used: None,
excess_blob_gas: None,
blob_gas_used,
excess_blob_gas,
hash,
parent_beacon_block_root: None,
}
Expand Down Expand Up @@ -557,6 +584,63 @@ mod tests {
&hex_decode("0x17cf53189035bbae5bce5c844355badd701aa9d2dd4b4f5ab1f9f0e8dd9fea5b")
.unwrap(),
);
assert_eq!(header.number, 17034871);
assert_eq!(header.hash(), expected_hash);
}

#[test]
fn dencun_rlp() {
let body =
std::fs::read_to_string("../test_assets/mainnet/block_19433903_value.json").unwrap();
let response: Value = serde_json::from_str(&body).unwrap();
let header: Header = serde_json::from_value(response["result"].clone()).unwrap();
let encoded = rlp::encode(&header);
let decoded: Header = rlp::decode(&encoded).unwrap();
assert_eq!(header, decoded);
let re_encoded = rlp::encode(&decoded);
assert_eq!(encoded, re_encoded);
let body =
std::fs::read_to_string("../test_assets/mainnet/block_19433902_value.json").unwrap();
let response: Value = serde_json::from_str(&body).unwrap();
let header: Header = serde_json::from_value(response["result"].clone()).unwrap();
let encoded = rlp::encode(&header);
let decoded: Header = rlp::decode(&encoded).unwrap();
assert_eq!(header, decoded);
let re_encoded = rlp::encode(&decoded);
assert_eq!(encoded, re_encoded);
}

#[test]
fn post_dencun_header_without_blob_txs() {
let body =
std::fs::read_to_string("../test_assets/mainnet/block_19433902_value.json").unwrap();
let response: Value = serde_json::from_str(&body).unwrap();
let header: Header = serde_json::from_value(response["result"].clone()).unwrap();
let etherscan_hash = H256::from_slice(
&hex_decode("0x8ec7bd37afa247fde16aa96317c77055b7a633aa8dc9ae27d0d5c776b58fef04")
.unwrap(),
);
let expected_hash =
H256::from_slice(&hex_decode(response["result"]["hash"].as_str().unwrap()).unwrap());
assert_eq!(etherscan_hash, expected_hash);
assert_eq!(header.number, 19433902);
assert_eq!(header.hash(), expected_hash);
}

#[test]
fn post_dencun_header_with_blob_txs() {
let body =
std::fs::read_to_string("../test_assets/mainnet/block_19433903_value.json").unwrap();
let response: Value = serde_json::from_str(&body).unwrap();
let header: Header = serde_json::from_value(response["result"].clone()).unwrap();
let etherscan_hash = H256::from_slice(
&hex_decode("0xb39d99cc81a35570c4d5765d9273cb282e424324cd5aae059cc00de7d59afb69")
.unwrap(),
);
let expected_hash =
H256::from_slice(&hex_decode(response["result"]["hash"].as_str().unwrap()).unwrap());
assert_eq!(etherscan_hash, expected_hash);
assert_eq!(header.number, 19433903);
assert_eq!(header.hash(), expected_hash);
}
}
19 changes: 17 additions & 2 deletions ethportal-api/src/types/execution/receipts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ impl Encodable for Receipt {
Receipt::Legacy(receipt) => receipt.rlp_append(s),
Receipt::AccessList(receipt) => receipt.rlp_append(s),
Receipt::EIP1559(receipt) => receipt.rlp_append(s),
Receipt::Blob(receipt) => receipt.rlp_append(s),
}
}
}
Expand All @@ -342,6 +343,7 @@ impl Encodable for Receipts {
#[repr(u8)]
/// The typed transaction ID
pub enum TransactionId {
Blob = 0x03,
EIP1559 = 0x02,
AccessList = 0x01,
Legacy = 0x00,
Expand All @@ -354,6 +356,7 @@ impl TryFrom<u8> for TransactionId {
match val {
id if id == TransactionId::EIP1559 as u8 => Ok(Self::EIP1559),
id if id == TransactionId::AccessList as u8 => Ok(Self::AccessList),
id if id == TransactionId::Blob as u8 => Ok(Self::Blob),
id if (id & 0x80) != 0x00 => Ok(Self::Legacy),
id if id == TransactionId::Legacy as u8 => Ok(Self::Legacy),
_ => Err(DecoderError::Custom(
Expand Down Expand Up @@ -383,6 +386,7 @@ pub enum Receipt {
Legacy(LegacyReceipt),
AccessList(LegacyReceipt),
EIP1559(LegacyReceipt),
Blob(LegacyReceipt),
}

impl Receipt {
Expand All @@ -393,6 +397,7 @@ impl Receipt {
TransactionId::EIP1559 => Self::EIP1559(legacy_receipt),
TransactionId::AccessList => Self::AccessList(legacy_receipt),
TransactionId::Legacy => Self::Legacy(legacy_receipt),
TransactionId::Blob => Self::Blob(legacy_receipt),
}
}

Expand All @@ -401,6 +406,7 @@ impl Receipt {
Self::Legacy(receipt) => receipt,
Self::AccessList(receipt) => receipt,
Self::EIP1559(receipt) => receipt,
Self::Blob(receipt) => receipt,
}
}

Expand All @@ -409,6 +415,7 @@ impl Receipt {
Self::Legacy(receipt) => receipt,
Self::AccessList(receipt) => receipt,
Self::EIP1559(receipt) => receipt,
Self::Blob(receipt) => receipt,
}
}

Expand All @@ -427,6 +434,10 @@ impl Receipt {
receipt.rlp_append(&mut stream);
[&[TransactionId::EIP1559 as u8], stream.as_raw()].concat()
}
Self::Blob(receipt) => {
receipt.rlp_append(&mut stream);
[&[TransactionId::Blob as u8], stream.as_raw()].concat()
}
}
}

Expand All @@ -439,9 +450,10 @@ impl Receipt {
.map_err(|_| DecoderError::Custom("Unknown transaction id"))?;
//other transaction types
match id {
TransactionId::EIP1559 => Ok(Self::EIP1559(rlp::decode(&receipt[1..])?)),
TransactionId::AccessList => Ok(Self::AccessList(rlp::decode(&receipt[1..])?)),
TransactionId::Legacy => Ok(Self::Legacy(rlp::decode(receipt)?)),
TransactionId::AccessList => Ok(Self::AccessList(rlp::decode(&receipt[1..])?)),
TransactionId::EIP1559 => Ok(Self::EIP1559(rlp::decode(&receipt[1..])?)),
TransactionId::Blob => Ok(Self::Blob(rlp::decode(&receipt[1..])?)),
}
}
}
Expand All @@ -468,6 +480,9 @@ impl<'de> Deserialize<'de> for Receipt {
TransactionId::EIP1559 => Ok(Receipt::EIP1559(
LegacyReceipt::deserialize(obj).map_err(serde::de::Error::custom)?,
)),
TransactionId::Blob => Ok(Receipt::Blob(
LegacyReceipt::deserialize(obj).map_err(serde::de::Error::custom)?,
)),
}
}
}
Expand Down
77 changes: 77 additions & 0 deletions ethportal-api/src/types/execution/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub enum Transaction {
Legacy(LegacyTransaction),
AccessList(AccessListTransaction),
EIP1559(EIP1559Transaction),
Blob(BlobTransaction),
}

impl Transaction {
Expand All @@ -38,6 +39,12 @@ impl Encodable for Transaction {
let encoded = [&[TransactionId::EIP1559 as u8], stream.as_raw()].concat();
s.append_raw(&encoded, 1);
}
Self::Blob(tx) => {
let mut stream = RlpStream::new();
tx.rlp_append(&mut stream);
let encoded = [&[TransactionId::Blob as u8], stream.as_raw()].concat();
s.append_raw(&encoded, 1);
}
}
}
}
Expand All @@ -54,6 +61,7 @@ impl Decodable for Transaction {
TransactionId::EIP1559 => Ok(Self::EIP1559(rlp::decode(&rlp.as_raw()[1..])?)),
TransactionId::AccessList => Ok(Self::AccessList(rlp::decode(&rlp.as_raw()[1..])?)),
TransactionId::Legacy => Ok(Self::Legacy(rlp::decode(rlp.as_raw())?)),
TransactionId::Blob => Ok(Self::Blob(rlp::decode(rlp.as_raw())?)),
}
}
}
Expand Down Expand Up @@ -90,6 +98,11 @@ impl<'de> Deserialize<'de> for Transaction {
EIP1559TransactionHelper::deserialize(obj).map_err(serde::de::Error::custom)?;
Ok(Self::EIP1559(helper.into()))
}
TransactionId::Blob => {
let helper =
BlobTransactionHelper::deserialize(obj).map_err(serde::de::Error::custom)?;
Ok(Self::Blob(helper.into()))
}
}
}
}
Expand Down Expand Up @@ -252,6 +265,70 @@ impl Into<EIP1559Transaction> for EIP1559TransactionHelper {
}
}

#[derive(Eq, Debug, Clone, PartialEq, RlpDecodable, RlpEncodable)]
pub struct BlobTransaction {
pub chain_id: U256,
pub nonce: U256,
pub max_priority_fee_per_gas: U256,
pub max_fee_per_gas: U256,
pub gas_limit: U256,
pub to: ToAddress,
pub value: U256,
pub data: Bytes,
pub access_list: AccessList,
pub y_parity: U64,
pub r: U256,
pub s: U256,
pub max_fee_per_blob_gas: U256,
pub blob_versioned_hashes: Vec<H256>,
}

#[derive(Eq, Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BlobTransactionHelper {
pub chain_id: U256,
pub nonce: U256,
pub max_priority_fee_per_gas: U256,
pub max_fee_per_gas: U256,
#[serde(rename(deserialize = "gas"))]
pub gas_limit: U256,
pub to: ToAddress,
pub value: U256,
#[serde(rename(deserialize = "input"))]
pub data: JsonBytes,
pub access_list: Vec<AccessListItem>,
#[serde(rename(deserialize = "v"))]
pub y_parity: U64,
pub r: U256,
pub s: U256,
pub max_fee_per_blob_gas: U256,
pub blob_versioned_hashes: Vec<H256>,
}

#[allow(clippy::from_over_into)]
impl Into<BlobTransaction> for BlobTransactionHelper {
fn into(self) -> BlobTransaction {
BlobTransaction {
chain_id: self.chain_id,
nonce: self.nonce,
max_priority_fee_per_gas: self.max_priority_fee_per_gas,
max_fee_per_gas: self.max_fee_per_gas,
gas_limit: self.gas_limit,
to: self.to,
value: self.value,
data: self.data.0,
access_list: AccessList {
list: self.access_list,
},
y_parity: self.y_parity,
r: self.r,
s: self.s,
max_fee_per_blob_gas: self.max_fee_per_blob_gas,
blob_versioned_hashes: self.blob_versioned_hashes,
}
}
}

/// Enum to represent the "to" field in a tx. Which can be an address, or Null if a contract is
/// created.
#[derive(Default, Eq, Debug, Clone, PartialEq)]
Expand Down
Loading

0 comments on commit e0dc410

Please sign in to comment.