diff --git a/Cargo.lock b/Cargo.lock index e9fa279d..168a03c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2207,6 +2207,7 @@ dependencies = [ name = "firehose-protos" version = "0.1.0" dependencies = [ + "alloy-consensus", "alloy-eip2930", "alloy-primitives 0.8.9", "ethportal-api", diff --git a/crates/firehose-protos/Cargo.toml b/crates/firehose-protos/Cargo.toml index 2ae21cae..8feb7dc3 100644 --- a/crates/firehose-protos/Cargo.toml +++ b/crates/firehose-protos/Cargo.toml @@ -8,6 +8,7 @@ doctest = false path = "src/lib.rs" [dependencies] +alloy-consensus.workspace = true alloy-eip2930.workspace = true alloy-primitives.workspace = true ethportal-api.workspace = true diff --git a/crates/firehose-protos/src/error.rs b/crates/firehose-protos/src/error.rs index d7820d1a..30e9b2b0 100644 --- a/crates/firehose-protos/src/error.rs +++ b/crates/firehose-protos/src/error.rs @@ -1,5 +1,7 @@ use thiserror::Error; +use crate::ethereum_v2::transaction::EcdsaComponent; + #[derive(Error, Debug)] pub enum ProtosError { #[error("Block conversion error")] @@ -17,12 +19,18 @@ pub enum ProtosError { #[error("Invalid access tuple storage key: {0}")] InvalidAccessTupleStorageKey(String), + #[error("Invalid BigInt: {0}")] + InvalidBigInt(String), + #[error("Invalid log address: {0}")] InvalidLogAddress(String), #[error("Invalid log topic: {0}")] InvalidLogTopic(String), + #[error("Invalid trace signature {0:?} component: {1}")] + InvalidTraceSignature(EcdsaComponent, String), + #[error("KzgCommitmentInvalid")] KzgCommitmentInvalid, @@ -58,4 +66,10 @@ pub enum ProtosError { #[error("SSZ Types error: {0}")] SszTypesError(String), + + #[error("Transaction missing call")] + TransactionMissingCall, + + #[error("TxTypeConversionError")] + TxTypeConversion, } diff --git a/crates/firehose-protos/src/ethereum_v2/transaction.rs b/crates/firehose-protos/src/ethereum_v2/transaction.rs index 1454d6d3..37705d21 100644 --- a/crates/firehose-protos/src/ethereum_v2/transaction.rs +++ b/crates/firehose-protos/src/ethereum_v2/transaction.rs @@ -1,6 +1,13 @@ -use reth_primitives::TxType; +use alloy_consensus::{TxEip1559, TxEip2930, TxLegacy}; +use alloy_eip2930::{AccessList, AccessListItem}; +use alloy_primitives::{ + hex, Address, Bytes, ChainId, FixedBytes, Parity, TxKind, Uint, U128, U256, +}; +use reth_primitives::{Signature, Transaction, TransactionSigned, TxType}; -use super::transaction_trace::Type; +use crate::error::ProtosError; + +use super::{transaction_trace::Type, BigInt, CallType, TransactionTrace}; impl From for TxType { fn from(tx_type: Type) -> Self { @@ -23,3 +30,236 @@ impl From for TxType { } } } + +impl TransactionTrace { + pub fn is_success(&self) -> bool { + self.status == 1 + } + + pub fn parity(&self) -> Result { + // Extract the first byte of the V value (Ethereum's V value). + let v: u8 = if self.v.is_empty() { 0 } else { self.v[0] }; + + let parity = match v { + // V values 0 and 1 directly indicate Y parity. + 0 | 1 => v == 1, + + // V values 27 and 28 are commonly used in Ethereum and indicate Y parity. + 27 | 28 => v - 27 == 1, + + // V values 37 and 38 are less common but still valid and represent Y parity. + 37 | 38 => v - 37 == 1, + + // If V is outside the expected range, return an error. + _ => { + return Err(ProtosError::InvalidTraceSignature( + EcdsaComponent::V, + v.to_string(), + )) + } + }; + + Ok(parity.into()) + } +} + +#[derive(Clone, Debug)] +pub enum EcdsaComponent { + R, + S, + V, +} + +impl TryFrom<&TransactionTrace> for TxKind { + type Error = ProtosError; + + fn try_from(trace: &TransactionTrace) -> Result { + let first_call = trace + .calls + .first() + .ok_or(ProtosError::TransactionMissingCall)?; + + match first_call.call_type() { + CallType::Create => Ok(TxKind::Create), + _ => { + // If not, interpret the transaction as a Call + let address = Address::from_slice(trace.to.as_slice()); + Ok(TxKind::Call(address)) + } + } + } +} + +impl TryFrom<&TransactionTrace> for Signature { + type Error = ProtosError; + + fn try_from(trace: &TransactionTrace) -> Result { + use EcdsaComponent::*; + + // Extract the R value from the trace and ensure it's a valid 32-byte array. + let r_bytes: [u8; 32] = trace + .r + .as_slice() + .try_into() + .map_err(|_| Self::Error::InvalidTraceSignature(R, hex::encode(&trace.r)))?; + let r = U256::from_be_bytes(r_bytes); + + // Extract the S value from the trace and ensure it's a valid 32-byte array. + let s_bytes: [u8; 32] = trace + .s + .as_slice() + .try_into() + .map_err(|_| Self::Error::InvalidTraceSignature(S, hex::encode(&trace.s)))?; + let s = U256::from_be_bytes(s_bytes); + + // Extract the Y parity from the V value. + let odd_y_parity = trace.parity()?; + + Ok(Signature::new(r, s, odd_y_parity)) + } +} + +impl TryFrom<&TransactionTrace> for reth_primitives::TxType { + type Error = ProtosError; + + fn try_from(trace: &TransactionTrace) -> Result { + match Type::try_from(trace.r#type) { + Ok(tx_type) => Ok(TxType::from(tx_type)), + Err(_) => Err(ProtosError::TxTypeConversion), + } + } +} + +pub const CHAIN_ID: ChainId = 1; + +impl TryFrom<&TransactionTrace> for Transaction { + type Error = ProtosError; + + fn try_from(trace: &TransactionTrace) -> Result { + let tx_type = reth_primitives::TxType::try_from(trace)?; + let nonce = trace.nonce; + let gas_price = match &trace.gas_price { + Some(gas_price) => gas_price.clone(), + None => BigInt { bytes: vec![0] }, + }; + let gas_price = u128::try_from(&gas_price)?; + let gas_limit = trace.gas_limit; + + let to = TxKind::try_from(trace)?; + + let trace_value = match &trace.value { + Some(value) => value.clone(), + None => BigInt { bytes: vec![0] }, + }; + let value = Uint::from(u128::try_from(&trace_value)?); + let input = Bytes::copy_from_slice(trace.input.as_slice()); + + let transaction: Transaction = match tx_type { + TxType::Legacy => { + let v: u8 = if trace.v.is_empty() { 0 } else { trace.v[0] }; + + let chain_id: Option = if v == 27 || v == 28 { + None + } else { + Some(CHAIN_ID) + }; + + Transaction::Legacy(TxLegacy { + chain_id, + nonce, + gas_price, + gas_limit, + to, + value, + input, + }) + } + TxType::Eip2930 => { + let access_list = AccessList::try_from(trace)?; + + Transaction::Eip2930(TxEip2930 { + chain_id: CHAIN_ID, + nonce, + gas_price, + gas_limit, + to, + value, + access_list, + input, + }) + } + TxType::Eip1559 => { + let access_list = AccessList::try_from(trace)?; + + let trace_max_fee_per_gas = match trace.max_fee_per_gas.clone() { + Some(max_fee_per_gas) => max_fee_per_gas, + None => BigInt { bytes: vec![0] }, + }; + let max_fee_per_gas = u128::try_from(&trace_max_fee_per_gas)?; + + let trace_max_priority_fee_per_gas = match trace.max_priority_fee_per_gas.clone() { + Some(max_priority_fee_per_gas) => max_priority_fee_per_gas, + None => BigInt { bytes: vec![0] }, + }; + let max_priority_fee_per_gas = u128::try_from(&trace_max_priority_fee_per_gas)?; + + Transaction::Eip1559(TxEip1559 { + chain_id: CHAIN_ID, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + input, + }) + } + TxType::Eip4844 => unimplemented!(), + TxType::Eip7702 => unimplemented!(), + }; + + Ok(transaction) + } +} + +impl TryFrom<&TransactionTrace> for TransactionSigned { + type Error = ProtosError; + + fn try_from(trace: &TransactionTrace) -> Result { + let transaction = Transaction::try_from(trace)?; + let signature = Signature::try_from(trace)?; + let hash = FixedBytes::from_slice(trace.hash.as_slice()); + + Ok(TransactionSigned { + transaction, + signature, + hash, + }) + } +} + +impl TryFrom<&TransactionTrace> for AccessList { + type Error = ProtosError; + + fn try_from(trace: &TransactionTrace) -> Result { + let access_list_items = trace + .access_list + .iter() + .map(AccessListItem::try_from) + .collect::, Self::Error>>()?; + + Ok(AccessList(access_list_items)) + } +} + +impl TryFrom<&BigInt> for u128 { + type Error = ProtosError; + + fn try_from(value: &BigInt) -> Result { + let slice = value.bytes.as_slice(); + let n = + U128::try_from_be_slice(slice).ok_or(ProtosError::InvalidBigInt(hex::encode(slice)))?; + Ok(u128::from_le_bytes(n.to_le_bytes())) + } +} diff --git a/crates/flat-files-decoder/src/receipts/error.rs b/crates/flat-files-decoder/src/receipts/error.rs index 27dab023..2be2bbb8 100644 --- a/crates/flat-files-decoder/src/receipts/error.rs +++ b/crates/flat-files-decoder/src/receipts/error.rs @@ -1,4 +1,3 @@ -use crate::transactions::tx_type::TransactionTypeError; use firehose_protos::error::ProtosError; use thiserror::Error; @@ -6,8 +5,6 @@ use thiserror::Error; pub enum ReceiptError { #[error("Invalid status")] InvalidStatus, - #[error("Invalid tx type")] - InvalidTxType(#[from] TransactionTypeError), #[error("Invalid address: {0}")] InvalidAddress(String), #[error("Invalid topic: {0}")] @@ -22,4 +19,6 @@ pub enum ReceiptError { MissingReceipt, #[error("Protos error: {0}")] ProtosError(#[from] ProtosError), + #[error("TryFromSliceError: {0}")] + TryFromSliceError(#[from] std::array::TryFromSliceError), } diff --git a/crates/flat-files-decoder/src/receipts/receipt.rs b/crates/flat-files-decoder/src/receipts/receipt.rs index 01ce08cf..bedd6840 100644 --- a/crates/flat-files-decoder/src/receipts/receipt.rs +++ b/crates/flat-files-decoder/src/receipts/receipt.rs @@ -1,5 +1,4 @@ use crate::receipts::error::ReceiptError; -use crate::transactions::tx_type::map_tx_type; use alloy_primitives::{Bloom, FixedBytes}; use firehose_protos::ethereum_v2::TransactionTrace; use reth_primitives::{Log, Receipt, ReceiptWithBloom}; @@ -14,8 +13,9 @@ impl TryFrom<&TransactionTrace> for FullReceipt { type Error = ReceiptError; fn try_from(trace: &TransactionTrace) -> Result { - let success = map_success(&trace.status)?; - let tx_type = map_tx_type(&trace.r#type)?; + let success = trace.is_success(); + let tx_type = trace.try_into()?; + let trace_receipt = match &trace.receipt { Some(receipt) => receipt, None => return Err(ReceiptError::MissingReceipt), @@ -48,15 +48,9 @@ impl TryFrom<&TransactionTrace> for FullReceipt { } } -fn map_success(status: &i32) -> Result { - Ok(*status == 1) -} - fn map_bloom(slice: &[u8]) -> Result { if slice.len() == 256 { - let array: [u8; 256] = slice - .try_into() - .expect("Slice length doesn't match array length"); + let array: [u8; 256] = slice.try_into()?; Ok(Bloom(FixedBytes(array))) } else { Err(ReceiptError::InvalidBloom(hex::encode(slice))) diff --git a/crates/flat-files-decoder/src/transactions/access_list.rs b/crates/flat-files-decoder/src/transactions/access_list.rs deleted file mode 100644 index 65ff674f..00000000 --- a/crates/flat-files-decoder/src/transactions/access_list.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::transactions::error::TransactionError; -use alloy_eip2930::{AccessList, AccessListItem}; -use firehose_protos::ethereum_v2::AccessTuple; - -pub(crate) fn compute_access_list( - access_list: &[AccessTuple], -) -> Result { - let access_list_items = access_list - .iter() - .map(AccessListItem::try_from) - .collect::, _>>()?; - - Ok(AccessList(access_list_items)) -} diff --git a/crates/flat-files-decoder/src/transactions/error.rs b/crates/flat-files-decoder/src/transactions/error.rs index 31be1ddd..10a0f19b 100644 --- a/crates/flat-files-decoder/src/transactions/error.rs +++ b/crates/flat-files-decoder/src/transactions/error.rs @@ -1,5 +1,3 @@ -use crate::transactions::signature::InvalidSignatureError; -use crate::transactions::tx_type::TransactionTypeError; use firehose_protos::error::ProtosError; use thiserror::Error; @@ -15,10 +13,6 @@ pub enum TransactionError { InvalidBigInt(String), #[error("EIP-4844 not supported")] EIP4844NotSupported, - #[error("Invalid Signature: {0}")] - InvalidSignature(#[from] InvalidSignatureError), - #[error("Invalid Transaction Type: {0}")] - InvalidType(#[from] TransactionTypeError), #[error("Missing Gas Price")] MissingGasPrice, #[error("Missing Value")] diff --git a/crates/flat-files-decoder/src/transactions/mod.rs b/crates/flat-files-decoder/src/transactions/mod.rs index 16e230c0..2d7d6eb4 100644 --- a/crates/flat-files-decoder/src/transactions/mod.rs +++ b/crates/flat-files-decoder/src/transactions/mod.rs @@ -1,24 +1,15 @@ pub mod error; -pub mod tx_type; - -mod access_list; -mod signature; -mod transaction; -mod transaction_signed; use crate::transactions::error::TransactionError; -use alloy_primitives::U128; -use firehose_protos::ethereum_v2::{BigInt, Block}; +use firehose_protos::ethereum_v2::Block; use reth_primitives::{proofs::calculate_transaction_root, TransactionSigned}; use revm_primitives::hex; -use self::transaction_signed::trace_to_signed; - pub fn check_transaction_root(block: &Block) -> Result<(), TransactionError> { let mut transactions: Vec = Vec::new(); for trace in &block.transaction_traces { - transactions.push(trace_to_signed(trace)?); + transactions.push(trace.try_into()?); } let tx_root = calculate_transaction_root(&transactions); @@ -38,20 +29,14 @@ pub fn check_transaction_root(block: &Block) -> Result<(), TransactionError> { Ok(()) } -pub(crate) fn bigint_to_u128(value: BigInt) -> Result { - let slice = value.bytes.as_slice(); - let n = U128::try_from_be_slice(slice) - .ok_or(TransactionError::InvalidBigInt(hex::encode(slice)))?; - Ok(u128::from_le_bytes(n.to_le_bytes())) -} - #[cfg(test)] mod tests { use crate::dbin::DbinFile; use alloy_primitives::{Address, Bytes, Parity, TxHash, TxKind, U256}; use firehose_protos::{ - bstream::v1::Block as BstreamBlock, ethereum_v2::transaction_trace::Type, + bstream::v1::Block as BstreamBlock, + ethereum_v2::{transaction_trace::Type, BigInt}, }; use prost::Message; use reth_primitives::TxType; @@ -68,7 +53,7 @@ mod tests { bytes: n_bytes.to_vec(), }; - let new_u128: u128 = bigint_to_u128(bigint)?; + let new_u128 = u128::try_from(&bigint)?; assert_eq!(new_u128, n_u128); Ok(()) } @@ -87,7 +72,7 @@ mod tests { let trace = block.transaction_traces.first().unwrap(); - let transaction = trace_to_signed(trace).unwrap(); + let transaction = TransactionSigned::try_from(trace).unwrap(); let tx_details = transaction.transaction; @@ -156,7 +141,7 @@ mod tests { .find(|t| Type::try_from(t.r#type).unwrap() == Type::TrxTypeLegacy) .unwrap(); - let transaction = trace_to_signed(trace).unwrap(); + let transaction = TransactionSigned::try_from(trace).unwrap(); let signature = transaction.signature; @@ -201,7 +186,7 @@ mod tests { .find(|t| t.index == 141) .unwrap(); - let transaction = trace_to_signed(trace).unwrap(); + let transaction = TransactionSigned::try_from(trace).unwrap(); let tx_details = transaction.transaction; diff --git a/crates/flat-files-decoder/src/transactions/signature.rs b/crates/flat-files-decoder/src/transactions/signature.rs deleted file mode 100644 index e18344c9..00000000 --- a/crates/flat-files-decoder/src/transactions/signature.rs +++ /dev/null @@ -1,51 +0,0 @@ -use alloy_primitives::U256; -use firehose_protos::ethereum_v2::TransactionTrace; -use reth_primitives::Signature; -use revm_primitives::hex; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum InvalidSignatureError { - #[error("Invalid R: {0}")] - R(String), - #[error("Invalid S: {0}")] - S(String), - #[error("Invalid V: {0}")] - V(u8), -} - -pub(crate) fn signature_from_trace( - trace: &TransactionTrace, -) -> Result { - let r_bytes: [u8; 32] = trace - .r - .as_slice() - .try_into() - .map_err(|_| InvalidSignatureError::R(hex::encode(&trace.r)))?; - let r = U256::from_be_bytes(r_bytes); - - let s_bytes: [u8; 32] = trace - .s - .as_slice() - .try_into() - .map_err(|_| InvalidSignatureError::S(hex::encode(&trace.s)))?; - let s = U256::from_be_bytes(s_bytes); - - let odd_y_parity = get_y_parity(trace)?; - - Ok(Signature::new(r, s, odd_y_parity.into())) -} - -fn get_y_parity(trace: &TransactionTrace) -> Result { - let v: u8 = if trace.v.is_empty() { 0 } else { trace.v[0] }; - - if v == 0 || v == 1 { - Ok(v == 1) - } else if v == 27 || v == 28 { - Ok(v - 27 == 1) - } else if v == 37 || v == 38 { - Ok(v - 37 == 1) - } else { - Err(InvalidSignatureError::V(v)) - } -} diff --git a/crates/flat-files-decoder/src/transactions/transaction.rs b/crates/flat-files-decoder/src/transactions/transaction.rs deleted file mode 100644 index dc456314..00000000 --- a/crates/flat-files-decoder/src/transactions/transaction.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::transactions::access_list::compute_access_list; -use crate::transactions::error::TransactionError; -use crate::transactions::tx_type::map_tx_type; -use alloy_consensus::{TxEip1559, TxEip2930, TxLegacy}; -use alloy_primitives::{Address, ChainId}; -use alloy_primitives::{Bytes, TxKind, Uint}; -use firehose_protos::ethereum_v2::{BigInt, CallType, TransactionTrace}; -use reth_primitives::Transaction; -use reth_primitives::TxType; - -use super::bigint_to_u128; - -pub const CHAIN_ID: ChainId = 1; - -pub(crate) fn trace_to_transaction( - trace: &TransactionTrace, -) -> Result { - let tx_type = map_tx_type(&trace.r#type)?; - - let nonce = trace.nonce; - let trace_gas_price = match trace.gas_price.clone() { - Some(gas_price) => gas_price, - None => BigInt { bytes: vec![0] }, - }; - let gas_price = bigint_to_u128(trace_gas_price)?; - let gas_limit = trace.gas_limit; - - let to = get_tx_kind(trace)?; - - let chain_id = CHAIN_ID; - - let trace_value = match trace.value.clone() { - Some(value) => value, - None => BigInt { bytes: vec![0] }, - }; - let value = Uint::from(bigint_to_u128(trace_value)?); - let input = Bytes::copy_from_slice(trace.input.as_slice()); - - let transaction: Transaction = match tx_type { - TxType::Legacy => { - let v: u8 = if trace.v.is_empty() { 0 } else { trace.v[0] }; - - let chain_id: Option = if v == 27 || v == 28 { - None - } else { - Some(CHAIN_ID) - }; - - Transaction::Legacy(TxLegacy { - chain_id, - nonce, - gas_price, - gas_limit, - to, - value, - input, - }) - } - TxType::Eip2930 => { - let access_list = compute_access_list(&trace.access_list)?; - - Transaction::Eip2930(TxEip2930 { - chain_id, - nonce, - gas_price, - gas_limit, - to, - value, - access_list, - input, - }) - } - TxType::Eip1559 => { - let access_list = compute_access_list(&trace.access_list)?; - - let trace_max_fee_per_gas = match trace.max_fee_per_gas.clone() { - Some(max_fee_per_gas) => max_fee_per_gas, - None => BigInt { bytes: vec![0] }, - }; - let max_fee_per_gas = bigint_to_u128(trace_max_fee_per_gas)?; - - let trace_max_priority_fee_per_gas = match trace.max_priority_fee_per_gas.clone() { - Some(max_priority_fee_per_gas) => max_priority_fee_per_gas, - None => BigInt { bytes: vec![0] }, - }; - let max_priority_fee_per_gas = bigint_to_u128(trace_max_priority_fee_per_gas)?; - - Transaction::Eip1559(TxEip1559 { - chain_id, - nonce, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - to, - value, - access_list, - input, - }) - } - TxType::Eip4844 => unimplemented!(), - TxType::Eip7702 => unimplemented!(), - }; - - Ok(transaction) -} - -fn get_tx_kind(trace: &TransactionTrace) -> Result { - let first_call = trace.calls.first().ok_or(TransactionError::MissingCall)?; - - let call_type = first_call.call_type(); - - if call_type == CallType::Create { - Ok(TxKind::Create) - } else { - let address = Address::from_slice(trace.to.as_slice()); - Ok(TxKind::Call(address)) - } -} diff --git a/crates/flat-files-decoder/src/transactions/transaction_signed.rs b/crates/flat-files-decoder/src/transactions/transaction_signed.rs deleted file mode 100644 index 5e21aa9e..00000000 --- a/crates/flat-files-decoder/src/transactions/transaction_signed.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::transactions::error::TransactionError; -use alloy_primitives::FixedBytes; -use firehose_protos::ethereum_v2::TransactionTrace; -use reth_primitives::TransactionSigned; -use revm_primitives::hex; -use std::str::FromStr; - -use super::{signature::signature_from_trace, transaction::trace_to_transaction}; - -pub(crate) fn trace_to_signed( - trace: &TransactionTrace, -) -> Result { - let transaction = trace_to_transaction(trace)?; - let signature = signature_from_trace(trace)?; - let hash = FixedBytes::from_str(&hex::encode(trace.hash.as_slice())) - .map_err(|_| TransactionError::MissingCall)?; - let tx_signed = TransactionSigned { - transaction, - signature, - hash, - }; - Ok(tx_signed) -} diff --git a/crates/flat-files-decoder/src/transactions/tx_type.rs b/crates/flat-files-decoder/src/transactions/tx_type.rs deleted file mode 100644 index 6ad5da09..00000000 --- a/crates/flat-files-decoder/src/transactions/tx_type.rs +++ /dev/null @@ -1,14 +0,0 @@ -use firehose_protos::ethereum_v2::transaction_trace::Type; -use reth_primitives::TxType; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum TransactionTypeError { - #[error("Transaction type is missing")] - Missing, -} - -pub(crate) fn map_tx_type(tx_type: &i32) -> Result { - let tx_type = Type::try_from(*tx_type).map_err(|_| TransactionTypeError::Missing)?; - Ok(TxType::from(tx_type)) -}