diff --git a/Cargo.lock b/Cargo.lock index 318aaf1..2199890 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2879,15 +2879,20 @@ name = "ev-node" version = "0.1.0" dependencies = [ "alloy-consensus", + "alloy-consensus-any", "alloy-eips", "alloy-evm", "alloy-genesis", + "alloy-network", "alloy-primitives", "alloy-rpc-types", "alloy-rpc-types-engine", + "alloy-rpc-types-eth", "async-trait", + "c-kzg", "clap", "ev-common", + "ev-primitives", "ev-revm", "evolve-ev-reth", "eyre", @@ -2896,6 +2901,7 @@ dependencies = [ "reth-basic-payload-builder", "reth-chainspec", "reth-cli", + "reth-codecs", "reth-consensus", "reth-db", "reth-engine-local", @@ -2919,9 +2925,14 @@ dependencies = [ "reth-primitives-traits", "reth-provider", "reth-revm", + "reth-rpc", "reth-rpc-api", "reth-rpc-builder", + "reth-rpc-convert", "reth-rpc-engine-api", + "reth-rpc-eth-api", + "reth-rpc-eth-types", + "reth-storage-api", "reth-tasks", "reth-testing-utils", "reth-tracing", @@ -2952,6 +2963,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "ev-primitives" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "reth-codecs", + "reth-ethereum-primitives", + "reth-primitives-traits", + "serde", +] + [[package]] name = "ev-reth" version = "0.1.0" @@ -3003,6 +3028,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "ev-precompiles", + "ev-primitives", "reth-evm", "reth-evm-ethereum", "reth-primitives", @@ -3031,6 +3057,7 @@ dependencies = [ "ev-common", "ev-node", "ev-precompiles", + "ev-primitives", "ev-revm", "evolve-ev-reth", "eyre", @@ -3081,6 +3108,7 @@ dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-txpool", "async-trait", + "ev-primitives", "eyre", "jsonrpsee", "jsonrpsee-core", diff --git a/Cargo.toml b/Cargo.toml index d3babc7..52cf637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "bin/ev-reth", "crates/common", + "crates/ev-primitives", "crates/evolve", "crates/node", "crates/tests", @@ -53,7 +54,7 @@ reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth.git", reth-engine-local = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-engine-primitives = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-ethereum-payload-builder = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } -reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } +reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4", features = ["serde", "serde-bincode-compat", "reth-codec"] } reth-e2e-test-utils = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-evm = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } @@ -69,8 +70,12 @@ reth-revm = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-rpc-api = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-rpc-builder = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-rpc-engine-api = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } +reth-rpc = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } +reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } +reth-codecs = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } ev-revm = { path = "crates/ev-revm" } +ev-primitives = { path = "crates/ev-primitives" } # Consensus dependencies @@ -108,6 +113,8 @@ alloy-signer = { version = "1.0.37", default-features = false } alloy-signer-local = { version = "1.0.37", features = ["mnemonic"] } alloy-primitives = { version = "1.3.1", default-features = false } alloy-consensus = { version = "1.0.37", default-features = false } +alloy-consensus-any = { version = "1.0.37", default-features = false } +alloy-rlp = { version = "0.3.12", default-features = false } alloy-genesis = { version = "1.0.37", default-features = false } alloy-rpc-types-txpool = { version = "1.0.37", default-features = false } alloy-sol-types = { version = "1.3.1", default-features = false } diff --git a/crates/ev-primitives/Cargo.toml b/crates/ev-primitives/Cargo.toml new file mode 100644 index 0000000..4e18985 --- /dev/null +++ b/crates/ev-primitives/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ev-primitives" +version = "0.1.0" +edition = "2021" +rust-version = "1.82" +license = "MIT OR Apache-2.0" + +[dependencies] +alloy-consensus = { workspace = true } +alloy-eips = { workspace = true, features = ["serde"] } +alloy-primitives = { workspace = true, features = ["k256", "rlp", "serde"] } +alloy-rlp = { workspace = true, features = ["derive"] } +reth-codecs = { workspace = true } +reth-ethereum-primitives = { workspace = true } +reth-primitives-traits = { workspace = true, features = ["serde-bincode-compat"] } +serde = { workspace = true, features = ["derive"] } + +[features] +serde-bincode-compat = ["reth-primitives-traits/serde-bincode-compat"] diff --git a/crates/ev-primitives/src/lib.rs b/crates/ev-primitives/src/lib.rs new file mode 100644 index 0000000..985961d --- /dev/null +++ b/crates/ev-primitives/src/lib.rs @@ -0,0 +1,696 @@ +//! EV-specific primitive types, including the EvNode 0x76 transaction. + +use alloy_consensus::{ + error::ValueError, + transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx, SignerRecoverable, TxHashRef}, + SignableTransaction, Transaction, TransactionEnvelope, +}; +use alloy_eips::eip2930::AccessList; +use alloy_primitives::{keccak256, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_rlp::{bytes::Buf, BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable}; +use reth_codecs::{ + alloy::transaction::{CompactEnvelope, Envelope, FromTxCompact, ToTxCompact}, + txtype::COMPACT_EXTENDED_IDENTIFIER_FLAG, + Compact, +}; +use reth_primitives_traits::{InMemorySize, NodePrimitives, SignedTransaction}; +use std::vec::Vec; + +/// EIP-2718 transaction type for EvNode batch + sponsorship. +pub const EVNODE_TX_TYPE_ID: u8 = 0x76; +/// Signature domain for sponsor authorization. +pub const EVNODE_SPONSOR_DOMAIN: u8 = 0x78; + +/// Single call entry in an EvNode transaction. +#[derive(Clone, Debug, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable, serde::Serialize, serde::Deserialize)] +pub struct Call { + /// Destination (CALL or CREATE). + pub to: TxKind, + /// ETH value. + pub value: U256, + /// Calldata. + pub input: Bytes, +} + +/// EvNode batch + sponsorship transaction payload. +#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub struct EvNodeTransaction { + pub chain_id: u64, + pub nonce: u64, + pub max_priority_fee_per_gas: u128, + pub max_fee_per_gas: u128, + pub gas_limit: u64, + pub calls: Vec, + pub access_list: AccessList, + pub fee_payer: Option
, + pub fee_payer_signature: Option, +} + +/// Signed EvNode transaction (executor signature). +pub type EvNodeSignedTx = alloy_consensus::Signed; + +/// Envelope type that includes standard Ethereum transactions and EvNode transactions. +#[derive(Clone, Debug, TransactionEnvelope)] +#[envelope(tx_type_name = EvTxType)] +pub enum EvTxEnvelope { + /// Standard Ethereum typed transaction envelope. + #[envelope(flatten)] + Ethereum(reth_ethereum_primitives::TransactionSigned), + /// EvNode typed transaction. + #[envelope(ty = 0x76)] + EvNode(EvNodeSignedTx), +} + +/// Signed transaction type alias for ev-reth. +pub type TransactionSigned = EvTxEnvelope; + +/// Pooled transaction envelope with optional blob sidecar support. +#[derive(Clone, Debug, TransactionEnvelope)] +#[envelope(tx_type_name = EvPooledTxType)] +pub enum EvPooledTxEnvelope { + /// Standard Ethereum pooled transaction envelope (may include blob sidecar). + #[envelope(flatten)] + Ethereum(reth_ethereum_primitives::PooledTransactionVariant), + /// EvNode typed transaction (no sidecar). + #[envelope(ty = 0x76)] + EvNode(EvNodeSignedTx), +} + +/// Block type alias for ev-reth. +pub type Block = alloy_consensus::Block; + +/// Block body type alias for ev-reth. +pub type BlockBody = alloy_consensus::BlockBody; + +/// Receipt type alias for ev-reth. +pub type Receipt = reth_ethereum_primitives::Receipt; + +/// Helper struct that specifies the ev-reth NodePrimitives types. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct EvPrimitives; + +impl NodePrimitives for EvPrimitives { + type Block = Block; + type BlockHeader = alloy_consensus::Header; + type BlockBody = BlockBody; + type SignedTx = TransactionSigned; + type Receipt = Receipt; +} + +impl EvNodeTransaction { + /// Returns the executor signing hash (domain 0x76, empty sponsor fields). + pub fn executor_signing_hash(&self) -> B256 { + let payload = self.encoded_payload(None, None); + let mut preimage = Vec::with_capacity(1 + payload.len()); + preimage.push(EVNODE_TX_TYPE_ID); + preimage.extend_from_slice(&payload); + keccak256(preimage) + } + + /// Returns the sponsor signing hash (domain 0x78, sponsor address bound). + pub fn sponsor_signing_hash(&self, fee_payer: Address) -> B256 { + let payload = self.encoded_payload(Some(fee_payer), None); + let mut preimage = Vec::with_capacity(1 + payload.len()); + preimage.push(EVNODE_SPONSOR_DOMAIN); + preimage.extend_from_slice(&payload); + keccak256(preimage) + } + + /// Recovers the executor address from the provided signature. + pub fn recover_executor(&self, signature: &Signature) -> Result { + signature.recover_address_from_prehash(&self.executor_signing_hash()) + } + + /// Recovers the sponsor address from the provided signature and fee payer. + pub fn recover_sponsor( + &self, + fee_payer: Address, + signature: &Signature, + ) -> Result { + signature.recover_address_from_prehash(&self.sponsor_signing_hash(fee_payer)) + } + + fn first_call(&self) -> Option<&Call> { + self.calls.first() + } + + fn encoded_payload( + &self, + fee_payer: Option
, + fee_payer_signature: Option<&Signature>, + ) -> Vec { + let payload_len = self.payload_fields_length(fee_payer, fee_payer_signature); + let mut out = Vec::with_capacity(Header { list: true, payload_length: payload_len }.length_with_payload()); + Header { list: true, payload_length: payload_len }.encode(&mut out); + self.encode_payload_fields(&mut out, fee_payer, fee_payer_signature); + out + } + + fn payload_fields_length( + &self, + fee_payer: Option
, + fee_payer_signature: Option<&Signature>, + ) -> usize { + self.chain_id.length() + + self.nonce.length() + + self.max_priority_fee_per_gas.length() + + self.max_fee_per_gas.length() + + self.gas_limit.length() + + self.calls.length() + + self.access_list.length() + + optional_address_length(fee_payer.as_ref()) + + optional_signature_length(fee_payer_signature) + } + + fn encode_payload_fields( + &self, + out: &mut dyn BufMut, + fee_payer: Option
, + fee_payer_signature: Option<&Signature>, + ) { + 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.calls.encode(out); + self.access_list.encode(out); + encode_optional_address(out, fee_payer.as_ref()); + encode_optional_signature(out, fee_payer_signature); + } +} + +impl Transaction for EvNodeTransaction { + fn chain_id(&self) -> Option { + Some(self.chain_id.into()) + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn gas_price(&self) -> Option { + None + } + + fn max_fee_per_gas(&self) -> u128 { + self.max_fee_per_gas + } + + fn max_priority_fee_per_gas(&self) -> Option { + Some(self.max_priority_fee_per_gas) + } + + fn max_fee_per_blob_gas(&self) -> Option { + None + } + + fn priority_fee_or_price(&self) -> u128 { + self.max_priority_fee_per_gas + } + + fn effective_gas_price(&self, base_fee: Option) -> u128 { + base_fee.map_or(self.max_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 { + self.max_fee_per_gas + } + }) + } + + fn is_dynamic_fee(&self) -> bool { + true + } + + fn kind(&self) -> TxKind { + self.first_call().map(|call| call.to).unwrap_or(TxKind::Create) + } + + fn is_create(&self) -> bool { + matches!(self.first_call().map(|call| call.to), Some(TxKind::Create)) + } + + fn value(&self) -> U256 { + self.first_call().map(|call| call.value).unwrap_or_default() + } + + fn input(&self) -> &Bytes { + static EMPTY: Bytes = Bytes::new(); + self.first_call().map(|call| &call.input).unwrap_or(&EMPTY) + } + + fn access_list(&self) -> Option<&AccessList> { + Some(&self.access_list) + } + + fn blob_versioned_hashes(&self) -> Option<&[B256]> { + None + } + + fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> { + None + } +} + +impl alloy_eips::Typed2718 for EvNodeTransaction { + fn ty(&self) -> u8 { + EVNODE_TX_TYPE_ID + } +} + +impl SignableTransaction for EvNodeTransaction { + fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) { + self.chain_id = chain_id.into(); + } + + fn encode_for_signing(&self, out: &mut dyn BufMut) { + out.put_u8(EVNODE_TX_TYPE_ID); + let payload_len = self.payload_fields_length(None, None); + Header { list: true, payload_length: payload_len }.encode(out); + self.encode_payload_fields(out, None, None); + } + + fn payload_len_for_signature(&self) -> usize { + 1 + Header { list: true, payload_length: self.payload_fields_length(None, None) }.length_with_payload() + } +} + +impl RlpEcdsaEncodableTx for EvNodeTransaction { + fn rlp_encoded_fields_length(&self) -> usize { + self.payload_fields_length(self.fee_payer, self.fee_payer_signature.as_ref()) + } + + fn rlp_encode_fields(&self, out: &mut dyn BufMut) { + self.encode_payload_fields(out, self.fee_payer, self.fee_payer_signature.as_ref()); + } +} + +impl RlpEcdsaDecodableTx for EvNodeTransaction { + const DEFAULT_TX_TYPE: u8 = EVNODE_TX_TYPE_ID; + + fn rlp_decode_fields(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)?, + calls: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + fee_payer: decode_optional_address(buf)?, + fee_payer_signature: decode_optional_signature(buf)?, + }) + } +} + +impl Encodable for EvNodeTransaction { + fn length(&self) -> usize { + Header { list: true, payload_length: self.rlp_encoded_fields_length() }.length_with_payload() + } + + fn encode(&self, out: &mut dyn BufMut) { + self.rlp_encode(out); + } +} + +impl Decodable for EvNodeTransaction { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Self::rlp_decode(buf) + } +} + +impl Compact for EvNodeTransaction { + fn to_compact(&self, buf: &mut B) -> usize + where + B: alloy_rlp::bytes::BufMut + AsMut<[u8]>, + { + let mut out = Vec::new(); + self.encode(&mut out); + out.to_compact(buf) + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + let (bytes, buf) = Vec::::from_compact(buf, len); + let mut slice = bytes.as_slice(); + let decoded = Self::decode(&mut slice).expect("valid evnode tx rlp"); + (decoded, buf) + } +} + +impl InMemorySize for Call { + fn size(&self) -> usize { + core::mem::size_of::() + self.input.len() + } +} + +impl InMemorySize for EvNodeTransaction { + fn size(&self) -> usize { + let calls_size = self.calls.iter().map(InMemorySize::size).sum::(); + let access_list_size = self.access_list.size(); + let fee_payer_size = self.fee_payer.map(|_| core::mem::size_of::
()).unwrap_or(0); + let sponsor_sig_size = self + .fee_payer_signature + .map(|_| core::mem::size_of::()) + .unwrap_or(0); + core::mem::size_of::() + calls_size + access_list_size + fee_payer_size + sponsor_sig_size + } +} + +impl InMemorySize for EvTxType { + fn size(&self) -> usize { + core::mem::size_of::() + } +} + +impl InMemorySize for EvTxEnvelope { + fn size(&self) -> usize { + match self { + EvTxEnvelope::Ethereum(tx) => tx.size(), + EvTxEnvelope::EvNode(tx) => tx.size(), + } + } +} + +impl InMemorySize for EvPooledTxEnvelope { + fn size(&self) -> usize { + match self { + EvPooledTxEnvelope::Ethereum(tx) => tx.size(), + EvPooledTxEnvelope::EvNode(tx) => tx.size(), + } + } +} + +impl SignerRecoverable for EvTxEnvelope { + fn recover_signer(&self) -> Result { + match self { + EvTxEnvelope::Ethereum(tx) => tx.recover_signer(), + EvTxEnvelope::EvNode(tx) => tx + .signature() + .recover_address_from_prehash(&tx.tx().executor_signing_hash()) + .map_err(|_| alloy_consensus::crypto::RecoveryError::new()), + } + } + + fn recover_signer_unchecked(&self) -> Result { + self.recover_signer() + } +} + +impl TxHashRef for EvTxEnvelope { + fn tx_hash(&self) -> &B256 { + match self { + EvTxEnvelope::Ethereum(tx) => tx.tx_hash(), + EvTxEnvelope::EvNode(tx) => tx.hash(), + } + } +} + +impl SignerRecoverable for EvPooledTxEnvelope { + fn recover_signer(&self) -> Result { + match self { + EvPooledTxEnvelope::Ethereum(tx) => tx.recover_signer(), + EvPooledTxEnvelope::EvNode(tx) => tx + .signature() + .recover_address_from_prehash(&tx.tx().executor_signing_hash()) + .map_err(|_| alloy_consensus::crypto::RecoveryError::new()), + } + } + + fn recover_signer_unchecked(&self) -> Result { + self.recover_signer() + } +} + +impl TxHashRef for EvPooledTxEnvelope { + fn tx_hash(&self) -> &B256 { + match self { + EvPooledTxEnvelope::Ethereum(tx) => tx.tx_hash(), + EvPooledTxEnvelope::EvNode(tx) => tx.hash(), + } + } +} + +impl TryFrom for EvPooledTxEnvelope { + type Error = ValueError; + + fn try_from(value: EvTxEnvelope) -> Result { + match value { + EvTxEnvelope::Ethereum(tx) => Ok(Self::Ethereum(tx.try_into()?)), + EvTxEnvelope::EvNode(tx) => Ok(Self::EvNode(tx)), + } + } +} + +impl From for EvTxEnvelope { + fn from(value: EvPooledTxEnvelope) -> Self { + match value { + EvPooledTxEnvelope::Ethereum(tx) => EvTxEnvelope::Ethereum(tx.into()), + EvPooledTxEnvelope::EvNode(tx) => EvTxEnvelope::EvNode(tx), + } + } +} + +impl Compact for EvTxType { + fn to_compact(&self, buf: &mut B) -> usize + where + B: alloy_rlp::bytes::BufMut + AsMut<[u8]>, + { + match self { + EvTxType::Ethereum(inner) => inner.to_compact(buf), + EvTxType::EvNode => { + buf.put_u8(EVNODE_TX_TYPE_ID); + COMPACT_EXTENDED_IDENTIFIER_FLAG + } + } + } + + fn from_compact(mut buf: &[u8], identifier: usize) -> (Self, &[u8]) { + match identifier { + COMPACT_EXTENDED_IDENTIFIER_FLAG => { + let extended_identifier = buf.get_u8(); + match extended_identifier { + EVNODE_TX_TYPE_ID => (Self::EvNode, buf), + _ => panic!("Unsupported EvTxType identifier: {extended_identifier}"), + } + } + v => { + let (inner, buf) = alloy_consensus::TxType::from_compact(buf, v); + (Self::Ethereum(inner), buf) + } + } + } +} + +impl Envelope for EvTxEnvelope { + fn signature(&self) -> &Signature { + match self { + EvTxEnvelope::Ethereum(tx) => tx.signature(), + EvTxEnvelope::EvNode(tx) => tx.signature(), + } + } + + fn tx_type(&self) -> Self::TxType { + match self { + EvTxEnvelope::Ethereum(tx) => EvTxType::Ethereum(tx.tx_type()), + EvTxEnvelope::EvNode(_) => EvTxType::EvNode, + } + } +} + +impl FromTxCompact for EvTxEnvelope { + type TxType = EvTxType; + + fn from_tx_compact(buf: &[u8], tx_type: Self::TxType, signature: Signature) -> (Self, &[u8]) + where + Self: Sized, + { + match tx_type { + EvTxType::Ethereum(inner) => { + let (tx, buf) = + reth_ethereum_primitives::TransactionSigned::from_tx_compact(buf, inner, signature); + (Self::Ethereum(tx), buf) + } + EvTxType::EvNode => { + let (tx, buf) = EvNodeTransaction::from_compact(buf, buf.len()); + let tx = alloy_consensus::Signed::new_unhashed(tx, signature); + (Self::EvNode(tx), buf) + } + } + } +} + +impl ToTxCompact for EvTxEnvelope { + fn to_tx_compact(&self, buf: &mut (impl alloy_rlp::bytes::BufMut + AsMut<[u8]>)) { + match self { + EvTxEnvelope::Ethereum(tx) => tx.to_tx_compact(buf), + EvTxEnvelope::EvNode(tx) => { + tx.tx().to_compact(buf); + } + } + } +} + +impl Compact for EvTxEnvelope { + fn to_compact(&self, buf: &mut B) -> usize + where + B: alloy_rlp::bytes::BufMut + AsMut<[u8]>, + { + ::to_compact(self, buf) + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + ::from_compact(buf, len) + } +} + +impl SignedTransaction for EvTxEnvelope {} +impl SignedTransaction for EvPooledTxEnvelope {} + +impl reth_primitives_traits::serde_bincode_compat::RlpBincode for EvTxEnvelope {} + +fn optional_address_length(value: Option<&Address>) -> usize { + match value { + Some(addr) => addr.length(), + None => 1, + } +} + +fn optional_signature_length(value: Option<&Signature>) -> usize { + match value { + Some(sig) => sig.as_bytes().as_slice().length(), + None => 1, + } +} + +fn encode_optional_address(out: &mut dyn BufMut, value: Option<&Address>) { + match value { + Some(addr) => addr.encode(out), + None => out.put_u8(alloy_rlp::EMPTY_STRING_CODE), + } +} + +fn encode_optional_signature(out: &mut dyn BufMut, value: Option<&Signature>) { + match value { + Some(sig) => sig.as_bytes().as_slice().encode(out), + None => out.put_u8(alloy_rlp::EMPTY_STRING_CODE), + } +} + +fn decode_optional_address(buf: &mut &[u8]) -> alloy_rlp::Result> { + let bytes = Header::decode_bytes(buf, false)?; + if bytes.is_empty() { + return Ok(None); + } + if bytes.len() != 20 { + return Err(alloy_rlp::Error::UnexpectedLength); + } + Ok(Some(Address::from_slice(bytes))) +} + +fn decode_optional_signature(buf: &mut &[u8]) -> alloy_rlp::Result> { + let bytes = Header::decode_bytes(buf, false)?; + if bytes.is_empty() { + return Ok(None); + } + let raw: [u8; 65] = bytes.try_into().map_err(|_| alloy_rlp::Error::UnexpectedLength)?; + Signature::from_raw_array(&raw) + .map(Some) + .map_err(|_| alloy_rlp::Error::Custom("invalid signature bytes")) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_eips::eip2930::AccessList; + + fn sample_signature() -> Signature { + let mut bytes = [0u8; 65]; + bytes[64] = 27; + Signature::from_raw_array(&bytes).expect("valid test signature") + } + + fn sample_tx() -> EvNodeTransaction { + EvNodeTransaction { + chain_id: 1, + nonce: 1, + max_priority_fee_per_gas: 1, + max_fee_per_gas: 2, + gas_limit: 30_000, + calls: vec![Call { to: TxKind::Create, value: U256::from(1), input: Bytes::new() }], + access_list: AccessList::default(), + fee_payer: None, + fee_payer_signature: None, + } + } + + #[test] + fn executor_signing_hash_ignores_sponsor_fields() { + let mut tx = sample_tx(); + let base_hash = tx.executor_signing_hash(); + + tx.fee_payer = Some(Address::ZERO); + tx.fee_payer_signature = Some(sample_signature()); + + assert_eq!(base_hash, tx.executor_signing_hash()); + } + + #[test] + fn sponsor_signing_hash_binds_fee_payer() { + let tx = sample_tx(); + let a = Address::from_slice(&[1u8; 20]); + let b = Address::from_slice(&[2u8; 20]); + assert_ne!(tx.sponsor_signing_hash(a), tx.sponsor_signing_hash(b)); + } + + #[test] + fn rlp_roundtrip_with_optional_signature() { + let mut tx = sample_tx(); + tx.fee_payer = Some(Address::from_slice(&[3u8; 20])); + tx.fee_payer_signature = Some(sample_signature()); + + let mut out = Vec::new(); + tx.encode(&mut out); + let mut slice = out.as_slice(); + let decoded = EvNodeTransaction::decode(&mut slice).expect("decode tx"); + assert_eq!(decoded.fee_payer, tx.fee_payer); + assert_eq!(decoded.fee_payer_signature, tx.fee_payer_signature); + } + + #[test] + fn decode_optional_address_none() { + let mut buf: &[u8] = &[alloy_rlp::EMPTY_STRING_CODE]; + let decoded = decode_optional_address(&mut buf).expect("decode none address"); + assert_eq!(decoded, None); + assert!(buf.is_empty()); + } + + #[test] + fn decode_optional_signature_none() { + let mut buf: &[u8] = &[alloy_rlp::EMPTY_STRING_CODE]; + let decoded = decode_optional_signature(&mut buf).expect("decode none signature"); + assert_eq!(decoded, None); + assert!(buf.is_empty()); + } + + #[test] + fn decode_optional_address_rejects_invalid_length() { + let mut data = vec![0u8; 19]; + data.insert(0, 0x93); + let mut buf: &[u8] = &data; + let err = decode_optional_address(&mut buf).expect_err("invalid length"); + assert_eq!(err, alloy_rlp::Error::UnexpectedLength); + } + + #[test] + fn decode_optional_signature_rejects_invalid_length() { + let mut buf: &[u8] = &[0x82, 0x01, 0x02]; + let err = decode_optional_signature(&mut buf).expect_err("invalid length"); + assert_eq!(err, alloy_rlp::Error::UnexpectedLength); + } +} diff --git a/crates/ev-revm/Cargo.toml b/crates/ev-revm/Cargo.toml index 9ee6715..fe01658 100644 --- a/crates/ev-revm/Cargo.toml +++ b/crates/ev-revm/Cargo.toml @@ -19,6 +19,7 @@ revm-inspector.workspace = true revm-context-interface.workspace = true thiserror.workspace = true ev-precompiles = { path = "../ev-precompiles" } +ev-primitives = { path = "../ev-primitives" } [dev-dependencies] alloy-sol-types.workspace = true diff --git a/crates/ev-revm/src/evm.rs b/crates/ev-revm/src/evm.rs index 043b593..0fdaa87 100644 --- a/crates/ev-revm/src/evm.rs +++ b/crates/ev-revm/src/evm.rs @@ -1,6 +1,6 @@ //! EV-specific EVM wrapper that installs the base-fee redirect handler. -use crate::base_fee::BaseFeeRedirect; +use crate::{base_fee::BaseFeeRedirect, tx_env::EvTxEnv}; use alloy_evm::{Evm as AlloyEvm, EvmEnv}; use alloy_primitives::{Address, Bytes}; use reth_revm::{ @@ -361,3 +361,93 @@ where ) } } + +impl AlloyEvm + for EvEvm, DB>, INSP, PRECOMP> +where + DB: alloy_evm::Database, + INSP: Inspector, DB>, EthInterpreter>, + PRECOMP: PrecompileProvider< + Context, DB>, + Output = InterpreterResult, + >, +{ + type DB = DB; + type Tx = EvTxEnv; + type Error = EVMError; + type HaltReason = HaltReason; + type Spec = SpecId; + type Precompiles = PRECOMP; + type Inspector = INSP; + + fn block(&self) -> &BlockEnv { + &self.inner.ctx.block + } + + fn chain_id(&self) -> u64 { + self.inner.ctx.cfg.chain_id + } + + fn transact_raw( + &mut self, + tx: Self::Tx, + ) -> Result, Self::Error> { + if self.inspect { + InspectEvm::inspect_tx(self, tx) + } else { + ExecuteEvm::transact(self, tx) + } + .map(|res| ResultAndState::new(res.result, res.state)) + } + + fn transact_system_call( + &mut self, + caller: Address, + contract: Address, + data: Bytes, + ) -> Result, Self::Error> { + if self.inspect { + InspectSystemCallEvm::inspect_system_call_with_caller(self, caller, contract, data) + } else { + SystemCallEvm::system_call_with_caller(self, caller, contract, data) + } + .map(|res| ResultAndState::new(res.result, res.state)) + } + + fn finish(self) -> (Self::DB, EvmEnv) { + let Self { inner, .. } = self; + let Context { + block, + cfg, + journaled_state, + .. + } = inner.ctx; + ( + journaled_state.database, + EvmEnv { + block_env: block, + cfg_env: cfg, + }, + ) + } + + fn set_inspector_enabled(&mut self, enabled: bool) { + self.inspect = enabled; + } + + fn components(&self) -> (&Self::DB, &Self::Inspector, &Self::Precompiles) { + ( + &self.inner.ctx.journaled_state.database, + &self.inner.inspector, + &self.inner.precompiles, + ) + } + + fn components_mut(&mut self) -> (&mut Self::DB, &mut Self::Inspector, &mut Self::Precompiles) { + ( + &mut self.inner.ctx.journaled_state.database, + &mut self.inner.inspector, + &mut self.inner.precompiles, + ) + } +} diff --git a/crates/ev-revm/src/factory.rs b/crates/ev-revm/src/factory.rs index ae4d72e..ce95816 100644 --- a/crates/ev-revm/src/factory.rs +++ b/crates/ev-revm/src/factory.rs @@ -1,6 +1,6 @@ //! Helpers for wrapping Reth EVM factories with the EV handler. -use crate::{base_fee::BaseFeeRedirect, evm::EvEvm}; +use crate::{base_fee::BaseFeeRedirect, evm::EvEvm, tx_env::EvTxEnv}; use alloy_evm::{ eth::{EthBlockExecutorFactory, EthEvmContext, EthEvmFactory}, precompiles::{DynPrecompile, Precompile, PrecompilesMap}, @@ -14,13 +14,17 @@ use reth_revm::{ revm::{ context::{ result::{EVMError, HaltReason}, - TxEnv, + Evm as RevmEvm, FrameStack, TxEnv, }, context_interface::result::InvalidTransaction, + handler::instructions::EthInstructions, + interpreter::interpreter::EthInterpreter, + precompile::{PrecompileSpecId, Precompiles}, + Context, Inspector, primitives::hardfork::SpecId, - Inspector, }, }; +use reth_revm::revm::context_interface::journaled_state::JournalTr; use std::sync::Arc; /// Settings for enabling the base-fee redirect at a specific block height. @@ -215,6 +219,154 @@ impl EvmFactory for EvEvmFactory { } } +/// EV EVM factory that builds a mainnet EVM with `EvTxEnv` and EV hooks. +#[derive(Debug, Default, Clone)] +pub struct EvTxEvmFactory { + redirect: Option, + mint_precompile: Option, + contract_size_limit: Option, +} + +type EvEvmContext = Context, DB>; +type EvRevmEvm = RevmEvm< + EvEvmContext, + I, + EthInstructions>, + PrecompilesMap, + reth_revm::revm::handler::EthFrame, +>; + +impl EvTxEvmFactory { + pub const fn new( + redirect: Option, + mint_precompile: Option, + contract_size_limit: Option, + ) -> Self { + Self { redirect, mint_precompile, contract_size_limit } + } + + fn contract_size_limit_for_block(&self, block_number: U256) -> Option { + self.contract_size_limit.and_then(|settings| { + if block_number >= U256::from(settings.activation_height()) { + Some(settings.limit()) + } else { + None + } + }) + } + + fn install_mint_precompile(&self, precompiles: &mut PrecompilesMap, block_number: U256) { + let Some(settings) = self.mint_precompile else { + return; + }; + if block_number < U256::from(settings.activation_height()) { + return; + } + + let mint = Arc::new(MintPrecompile::new(settings.admin())); + let id = MintPrecompile::id().clone(); + + precompiles.apply_precompile(&MINT_PRECOMPILE_ADDR, move |_| { + let mint_for_call = Arc::clone(&mint); + let id_for_call = id; + Some(DynPrecompile::new_stateful(id_for_call, move |input| { + mint_for_call.call(input) + })) + }); + } + + fn redirect_for_block(&self, block_number: U256) -> Option { + self.redirect.and_then(|settings| { + if block_number >= U256::from(settings.activation_height()) { + Some(settings.redirect()) + } else { + None + } + }) + } + + fn build_evm>>( + &self, + db: DB, + env: EvmEnv, + inspector: I, + ) -> EvRevmEvm { + let precompiles = PrecompilesMap::from_static(Precompiles::new(PrecompileSpecId::from_spec_id( + env.cfg_env.spec, + ))); + + let mut journaled_state = reth_revm::revm::Journal::new(db); + journaled_state.set_spec_id(env.cfg_env.spec); + + let ctx = Context { + block: env.block_env, + tx: EvTxEnv::default(), + cfg: env.cfg_env, + journaled_state, + chain: (), + local: Default::default(), + error: Ok(()), + }; + + RevmEvm { + ctx, + inspector, + instruction: EthInstructions::new_mainnet(), + precompiles, + frame_stack: FrameStack::new(), + } + } +} + +impl EvmFactory for EvTxEvmFactory { + type Evm>> = + EvEvm, I, PrecompilesMap>; + type Context = EvEvmContext; + type Tx = EvTxEnv; + type Error = + EVMError; + type HaltReason = HaltReason; + type Spec = SpecId; + type Precompiles = PrecompilesMap; + + fn create_evm( + &self, + db: DB, + mut env: EvmEnv, + ) -> Self::Evm { + let block_number = env.block_env.number; + if let Some(limit) = self.contract_size_limit_for_block(block_number) { + env.cfg_env.limit_contract_code_size = Some(limit); + } + let inner = self.build_evm(db, env, NoOpInspector {}); + let mut evm = EvEvm::from_inner(inner, self.redirect_for_block(block_number), false); + { + let inner = evm.inner_mut(); + self.install_mint_precompile(&mut inner.precompiles, block_number); + } + evm + } + + fn create_evm_with_inspector>>( + &self, + db: DB, + mut env: EvmEnv, + inspector: I, + ) -> Self::Evm { + let block_number = env.block_env.number; + if let Some(limit) = self.contract_size_limit_for_block(block_number) { + env.cfg_env.limit_contract_code_size = Some(limit); + } + let inner = self.build_evm(db, env, inspector); + let mut evm = EvEvm::from_inner(inner, self.redirect_for_block(block_number), true); + { + let inner = evm.inner_mut(); + self.install_mint_precompile(&mut inner.precompiles, block_number); + } + evm + } +} + /// Wraps an [`EthEvmConfig`] so that it produces [`EvEvm`] instances. pub fn with_ev_handler( config: EthEvmConfig, diff --git a/crates/ev-revm/src/lib.rs b/crates/ev-revm/src/lib.rs index da8401f..43830d1 100644 --- a/crates/ev-revm/src/lib.rs +++ b/crates/ev-revm/src/lib.rs @@ -6,13 +6,15 @@ pub mod config; pub mod evm; pub mod factory; pub mod handler; +pub mod tx_env; pub use api::EvBuilder; pub use base_fee::{BaseFeeRedirect, BaseFeeRedirectError}; pub use config::{BaseFeeConfig, ConfigError}; pub use evm::{DefaultEvEvm, EvEvm}; pub use factory::{ - with_ev_handler, BaseFeeRedirectSettings, ContractSizeLimitSettings, EvEvmFactory, + with_ev_handler, BaseFeeRedirectSettings, ContractSizeLimitSettings, EvEvmFactory, EvTxEvmFactory, MintPrecompileSettings, }; pub use handler::EvHandler; +pub use tx_env::EvTxEnv; diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs new file mode 100644 index 0000000..9e52031 --- /dev/null +++ b/crates/ev-revm/src/tx_env.rs @@ -0,0 +1,175 @@ +use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; +use alloy_primitives::{Address, Bytes}; +use ev_primitives::EvTxEnvelope; +use reth_revm::revm::context::TxEnv; +use reth_revm::revm::context_interface::transaction::{ + AccessList, AccessListItem, RecoveredAuthorization, SignedAuthorization, + Transaction as RevmTransaction, +}; +use reth_revm::revm::handler::SystemCallTx; +use reth_revm::revm::primitives::{Address as RevmAddress, Bytes as RevmBytes, TxKind, B256, U256}; +use reth_revm::revm::context_interface::either::Either; +use reth_evm::TransactionEnv; + +/// Transaction environment wrapper that supports EvTxEnvelope conversions. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct EvTxEnv { + inner: TxEnv, +} + +impl EvTxEnv { + pub const fn new(inner: TxEnv) -> Self { + Self { inner } + } + + pub const fn inner(&self) -> &TxEnv { + &self.inner + } + + pub fn inner_mut(&mut self) -> &mut TxEnv { + &mut self.inner + } +} + +impl From for EvTxEnv { + fn from(inner: TxEnv) -> Self { + Self { inner } + } +} + +impl From for TxEnv { + fn from(env: EvTxEnv) -> Self { + env.inner + } +} + +impl RevmTransaction for EvTxEnv { + type AccessListItem<'a> = &'a AccessListItem where Self: 'a; + type Authorization<'a> = &'a Either where Self: 'a; + + fn tx_type(&self) -> u8 { + self.inner.tx_type + } + + fn caller(&self) -> RevmAddress { + self.inner.caller + } + + fn gas_limit(&self) -> u64 { + self.inner.gas_limit + } + + fn value(&self) -> U256 { + self.inner.value + } + + fn input(&self) -> &RevmBytes { + &self.inner.data + } + + fn nonce(&self) -> u64 { + self.inner.nonce + } + + fn kind(&self) -> TxKind { + self.inner.kind + } + + fn chain_id(&self) -> Option { + self.inner.chain_id + } + + fn gas_price(&self) -> u128 { + self.inner.gas_price + } + + fn access_list(&self) -> Option>> { + Some(self.inner.access_list.0.iter()) + } + + fn blob_versioned_hashes(&self) -> &[B256] { + &self.inner.blob_hashes + } + + fn max_fee_per_blob_gas(&self) -> u128 { + self.inner.max_fee_per_blob_gas + } + + fn authorization_list_len(&self) -> usize { + self.inner.authorization_list.len() + } + + fn authorization_list(&self) -> impl Iterator> { + self.inner.authorization_list.iter() + } + + fn max_priority_fee_per_gas(&self) -> Option { + self.inner.gas_priority_fee + } +} + +impl TransactionEnv for EvTxEnv { + fn set_gas_limit(&mut self, gas_limit: u64) { + self.inner.gas_limit = gas_limit; + } + + fn nonce(&self) -> u64 { + self.inner.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.inner.nonce = nonce; + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.inner.access_list = access_list; + } +} + +impl alloy_evm::ToTxEnv for EvTxEnv { + fn to_tx_env(&self) -> EvTxEnv { + self.clone() + } +} + +impl FromRecoveredTx for EvTxEnv { + fn from_recovered_tx(tx: &EvTxEnvelope, sender: Address) -> Self { + match tx { + EvTxEnvelope::Ethereum(inner) => EvTxEnv::new(TxEnv::from_recovered_tx(inner, sender)), + EvTxEnvelope::EvNode(ev) => { + let mut env = TxEnv::default(); + env.caller = sender; + env.gas_limit = ev.tx().gas_limit; + env.gas_price = ev.tx().max_fee_per_gas; + env.kind = ev.tx().calls.first().map(|call| call.to).unwrap_or(TxKind::Create); + env.value = ev.tx().calls.first().map(|call| call.value).unwrap_or_default(); + env.data = ev.tx().calls.first().map(|call| call.input.clone()).unwrap_or_default(); + EvTxEnv::new(env) + } + } + } +} + +impl FromTxWithEncoded for EvTxEnv { + fn from_encoded_tx(tx: &EvTxEnvelope, caller: Address, _encoded: Bytes) -> Self { + Self::from_recovered_tx(tx, caller) + } +} + +impl SystemCallTx for EvTxEnv { + fn new_system_tx_with_caller( + caller: Address, + system_contract_address: Address, + data: Bytes, + ) -> Self { + EvTxEnv::new( + TxEnv::builder() + .caller(caller) + .data(data) + .kind(TxKind::Call(system_contract_address)) + .gas_limit(30_000_000) + .build() + .unwrap(), + ) + } +} diff --git a/crates/evolve/Cargo.toml b/crates/evolve/Cargo.toml index b9d2dcb..98871ac 100644 --- a/crates/evolve/Cargo.toml +++ b/crates/evolve/Cargo.toml @@ -23,6 +23,7 @@ reth-node-api.workspace = true reth-ethereum = { workspace = true, features = ["node-api", "node"] } reth-ethereum-primitives.workspace = true reth-execution-types.workspace = true +ev-primitives = { path = "../ev-primitives" } # Alloy dependencies alloy-rpc-types-engine.workspace = true diff --git a/crates/evolve/src/consensus.rs b/crates/evolve/src/consensus.rs index e74ed4e..4fe433f 100644 --- a/crates/evolve/src/consensus.rs +++ b/crates/evolve/src/consensus.rs @@ -8,10 +8,10 @@ use reth_consensus_common::validation::{ }; use reth_ethereum::node::builder::{components::ConsensusBuilder, BuilderContext}; use reth_ethereum_consensus::EthBeaconConsensus; -use reth_ethereum_primitives::{Block, BlockBody, EthPrimitives, Receipt}; +use ev_primitives::{Block, BlockBody, EvPrimitives, Receipt}; use reth_execution_types::BlockExecutionResult; use reth_node_api::{FullNodeTypes, NodeTypes}; -use reth_primitives::{RecoveredBlock, SealedBlock, SealedHeader}; +use reth_primitives_traits::{RecoveredBlock, SealedBlock, SealedHeader}; use std::sync::Arc; /// Builder for `EvolveConsensus` @@ -34,9 +34,9 @@ impl EvolveConsensusBuilder { impl ConsensusBuilder for EvolveConsensusBuilder where Node: FullNodeTypes, - Node::Types: NodeTypes, + Node::Types: NodeTypes, { - type Consensus = Arc>; + type Consensus = Arc>; async fn build_consensus(self, ctx: &BuilderContext) -> eyre::Result { Ok(Arc::new(EvolveConsensus::new(ctx.chain_spec())) as Self::Consensus) @@ -107,18 +107,18 @@ impl Consensus for EvolveConsensus { validate_body_against_header(body, header.header()) } - fn validate_block_pre_execution(&self, block: &SealedBlock) -> Result<(), Self::Error> { + fn validate_block_pre_execution(&self, block: &SealedBlock) -> Result<(), Self::Error> { // Use inner consensus for pre-execution validation self.inner.validate_block_pre_execution(block) } } -impl FullConsensus for EvolveConsensus { +impl FullConsensus for EvolveConsensus { fn validate_block_post_execution( &self, block: &RecoveredBlock, result: &BlockExecutionResult, ) -> Result<(), ConsensusError> { - as FullConsensus>::validate_block_post_execution(&self.inner, block, result) + as FullConsensus>::validate_block_post_execution(&self.inner, block, result) } } diff --git a/crates/evolve/src/types.rs b/crates/evolve/src/types.rs index b8f23a2..9084360 100644 --- a/crates/evolve/src/types.rs +++ b/crates/evolve/src/types.rs @@ -1,5 +1,5 @@ use alloy_primitives::{Address, B256}; -use reth_primitives::TransactionSigned; +use ev_primitives::TransactionSigned; use serde::{Deserialize, Serialize}; /// Payload attributes for the Evolve Reth node diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 635ec38..03d8bcf 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -13,6 +13,7 @@ description = "Evolve node implementation" ev-common = { path = "../common" } evolve-ev-reth = { path = "../evolve" } ev-revm = { path = "../ev-revm" } +ev-primitives = { path = "../ev-primitives" } # Reth dependencies reth-node-builder.workspace = true @@ -31,6 +32,9 @@ reth-basic-payload-builder.workspace = true reth-engine-local.workspace = true reth-revm.workspace = true reth-trie-db.workspace = true +reth-storage-api.workspace = true +reth-transaction-pool.workspace = true +reth-codecs.workspace = true # Additional reth dependencies for payload builder reth-node-types.workspace = true @@ -43,17 +47,25 @@ reth-node-core.workspace = true reth-rpc-builder.workspace = true reth-rpc-api.workspace = true reth-rpc-engine-api.workspace = true +reth-rpc-convert.workspace = true +reth-rpc.workspace = true +reth-rpc-eth-api.workspace = true +reth-rpc-eth-types.workspace = true reth-engine-primitives.workspace = true reth-ethereum-primitives.workspace = true # Alloy dependencies alloy-rpc-types.workspace = true alloy-rpc-types-engine.workspace = true +alloy-rpc-types-eth.workspace = true alloy-primitives.workspace = true alloy-eips.workspace = true alloy-consensus.workspace = true +alloy-consensus-any.workspace = true alloy-evm.workspace = true alloy-genesis.workspace = true +alloy-network.workspace = true +c-kzg = "2.1.5" # Core dependencies eyre.workspace = true diff --git a/crates/node/src/attributes.rs b/crates/node/src/attributes.rs index ed7b3e6..b44b2a6 100644 --- a/crates/node/src/attributes.rs +++ b/crates/node/src/attributes.rs @@ -8,12 +8,12 @@ use reth_chainspec::EthereumHardforks; use reth_engine_local::payload::LocalPayloadAttributesBuilder; use reth_ethereum::{ node::api::payload::{PayloadAttributes, PayloadBuilderAttributes}, - TransactionSigned, }; use reth_payload_builder::EthPayloadBuilderAttributes; use reth_payload_primitives::PayloadAttributesBuilder; use serde::{Deserialize, Serialize}; +use ev_primitives::TransactionSigned; use crate::error::EvolveEngineError; /// Evolve payload attributes that support passing transactions via Engine API. diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index c29c3d6..03a6c0c 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -1,8 +1,7 @@ use crate::config::EvolvePayloadBuilderConfig; use alloy_consensus::transaction::Transaction; -use alloy_evm::eth::EthEvmFactory; use alloy_primitives::Address; -use ev_revm::EvEvmFactory; +use ev_revm::EvTxEvmFactory; use evolve_ev_reth::EvolvePayloadAttributes; use reth_chainspec::{ChainSpec, ChainSpecProvider}; use reth_errors::RethError; @@ -10,15 +9,17 @@ use reth_evm::{ execute::{BlockBuilder, BlockBuilderOutcome}, ConfigureEvm, NextBlockEnvAttributes, }; -use reth_evm_ethereum::EthEvmConfig; +use crate::executor::EvEvmConfig; use reth_payload_builder_primitives::PayloadBuilderError; -use reth_primitives::{transaction::SignedTransaction, Header, SealedBlock, SealedHeader}; +use reth_primitives::{transaction::SignedTransaction, Header, SealedHeader}; +use alloy_consensus::transaction::TxHashRef; +use reth_primitives_traits::SealedBlock; use reth_provider::{HeaderProvider, StateProviderFactory}; use reth_revm::{database::StateProviderDatabase, State}; use std::sync::Arc; use tracing::{debug, info}; -type EvolveEthEvmConfig = EthEvmConfig>; +type EvolveEthEvmConfig = EvEvmConfig; /// Payload builder for Evolve Reth node #[derive(Debug)] @@ -66,7 +67,7 @@ where pub async fn build_payload( &self, attributes: EvolvePayloadAttributes, - ) -> Result { + ) -> Result, PayloadBuilderError> { // Validate attributes attributes .validate() @@ -142,12 +143,12 @@ where ); for (i, tx) in attributes.transactions.iter().enumerate() { tracing::debug!( - index = i, - hash = ?tx.hash(), - nonce = tx.nonce(), - gas_price = ?tx.gas_price(), - gas_limit = tx.gas_limit(), - "Processing transaction" + index = i, + hash = ?tx.tx_hash(), + nonce = tx.nonce(), + gas_price = ?tx.gas_price(), + gas_limit = tx.gas_limit(), + "Processing transaction" ); // Convert to recovered transaction for execution @@ -157,6 +158,12 @@ where )) })?; + if matches!(recovered_tx.inner(), ev_primitives::EvTxEnvelope::EvNode(_)) { + return Err(PayloadBuilderError::Internal(RethError::Other( + "EvNode transaction execution not supported yet".into(), + ))); + } + // Execute the transaction match builder.execute_transaction(recovered_tx) { Ok(gas_used) => { diff --git a/crates/node/src/evm_executor.rs b/crates/node/src/evm_executor.rs new file mode 100644 index 0000000..4480ce1 --- /dev/null +++ b/crates/node/src/evm_executor.rs @@ -0,0 +1,283 @@ +use std::{borrow::Cow, boxed::Box, vec::Vec}; + +use alloy_consensus::{Transaction, TxReceipt}; +use alloy_eips::{eip7685::Requests, Encodable2718}; +use alloy_evm::{ + block::{ + state_changes::{balance_increment_state, post_block_balance_increments}, + BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory, + BlockExecutorFor, BlockValidationError, ExecutableTx, OnStateHook, StateChangePostBlockSource, + StateChangeSource, SystemCaller, + }, + eth::{ + dao_fork, eip6110, + receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx}, + spec::{EthExecutorSpec, EthSpec}, + EthBlockExecutionCtx, + }, + Database, EthEvmFactory, Evm, EvmFactory, FromRecoveredTx, FromTxWithEncoded, +}; +use alloy_primitives::Log; +use ev_primitives::{Receipt, TransactionSigned}; +use reth_codecs::alloy::transaction::Envelope; +use reth_ethereum_forks::EthereumHardfork; +use reth_revm::revm::{context_interface::result::ResultAndState, database::State, DatabaseCommit, Inspector}; + +/// Receipt builder that works with Ev transaction envelopes. +#[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] +pub struct EvReceiptBuilder; + +impl ReceiptBuilder for EvReceiptBuilder { + type Transaction = TransactionSigned; + type Receipt = Receipt; + + fn build_receipt( + &self, + ctx: ReceiptBuilderCtx<'_, Self::Transaction, E>, + ) -> Self::Receipt { + let ReceiptBuilderCtx { tx, result, cumulative_gas_used, .. } = ctx; + Receipt { + tx_type: tx.tx_type(), + success: result.is_success(), + cumulative_gas_used, + logs: result.into_logs(), + } + } +} + +/// Block executor for EV transactions. +#[derive(Debug)] +pub struct EvBlockExecutor<'a, Evm, Spec, R: ReceiptBuilder> { + spec: Spec, + pub ctx: EthBlockExecutionCtx<'a>, + evm: Evm, + system_caller: SystemCaller, + receipt_builder: R, + receipts: Vec, + gas_used: u64, +} + +impl<'a, Evm, Spec, R> EvBlockExecutor<'a, Evm, Spec, R> +where + Spec: Clone, + R: ReceiptBuilder, +{ + pub fn new(evm: Evm, ctx: EthBlockExecutionCtx<'a>, spec: Spec, receipt_builder: R) -> Self { + Self { + evm, + ctx, + receipts: Vec::new(), + gas_used: 0, + system_caller: SystemCaller::new(spec.clone()), + spec, + receipt_builder, + } + } +} + +impl<'db, DB, E, Spec, R> BlockExecutor for EvBlockExecutor<'_, E, Spec, R> +where + DB: Database + 'db, + E: Evm< + DB = &'db mut State, + Tx: FromRecoveredTx + FromTxWithEncoded, + >, + Spec: EthExecutorSpec, + R: ReceiptBuilder>, +{ + type Transaction = R::Transaction; + type Receipt = R::Receipt; + type Evm = E; + + fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> { + let state_clear_flag = + self.spec.is_spurious_dragon_active_at_block(self.evm.block().number.saturating_to()); + self.evm.db_mut().set_state_clear_flag(state_clear_flag); + + self.system_caller.apply_blockhashes_contract_call(self.ctx.parent_hash, &mut self.evm)?; + self.system_caller + .apply_beacon_root_contract_call(self.ctx.parent_beacon_block_root, &mut self.evm)?; + + Ok(()) + } + + fn execute_transaction_without_commit( + &mut self, + tx: impl ExecutableTx, + ) -> Result::HaltReason>, BlockExecutionError> { + let block_available_gas = self.evm.block().gas_limit - self.gas_used; + + if tx.tx().gas_limit() > block_available_gas { + return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { + transaction_gas_limit: tx.tx().gas_limit(), + block_available_gas, + } + .into()); + } + + self.evm.transact(&tx).map_err(|err| { + let hash = tx.tx().trie_hash(); + BlockExecutionError::evm(err, hash) + }) + } + + fn commit_transaction( + &mut self, + output: ResultAndState<::HaltReason>, + tx: impl ExecutableTx, + ) -> Result { + let ResultAndState { result, state } = output; + + self.system_caller.on_state(StateChangeSource::Transaction(self.receipts.len()), &state); + + let gas_used = result.gas_used(); + self.gas_used += gas_used; + + self.receipts.push(self.receipt_builder.build_receipt(ReceiptBuilderCtx { + tx: tx.tx(), + evm: &self.evm, + result, + state: &state, + cumulative_gas_used: self.gas_used, + })); + + self.evm.db_mut().commit(state); + + Ok(gas_used) + } + + fn finish( + mut self, + ) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { + let requests = if self + .spec + .is_prague_active_at_timestamp(self.evm.block().timestamp.saturating_to()) + { + let deposit_requests = + eip6110::parse_deposits_from_receipts(&self.spec, &self.receipts)?; + + let mut requests = Requests::default(); + + if !deposit_requests.is_empty() { + requests.push_request_with_type(eip6110::DEPOSIT_REQUEST_TYPE, deposit_requests); + } + + requests.extend(self.system_caller.apply_post_execution_changes(&mut self.evm)?); + requests + } else { + Requests::default() + }; + + let mut balance_increments = post_block_balance_increments( + &self.spec, + self.evm.block(), + self.ctx.ommers, + self.ctx.withdrawals.as_deref(), + ); + + if self + .spec + .ethereum_fork_activation(EthereumHardfork::Dao) + .transitions_at_block(self.evm.block().number.saturating_to()) + { + let drained_balance: u128 = self + .evm + .db_mut() + .drain_balances(dao_fork::DAO_HARDFORK_ACCOUNTS) + .map_err(|_| BlockValidationError::IncrementBalanceFailed)? + .into_iter() + .sum(); + + *balance_increments.entry(dao_fork::DAO_HARDFORK_BENEFICIARY).or_default() += + drained_balance; + } + + self.evm + .db_mut() + .increment_balances(balance_increments.clone()) + .map_err(|_| BlockValidationError::IncrementBalanceFailed)?; + + self.system_caller.try_on_state_with(|| { + balance_increment_state(&balance_increments, self.evm.db_mut()).map(|state| { + ( + StateChangeSource::PostBlock(StateChangePostBlockSource::BalanceIncrements), + Cow::Owned(state), + ) + }) + })?; + + Ok(( + self.evm, + BlockExecutionResult { receipts: self.receipts, requests, gas_used: self.gas_used }, + )) + } + + fn set_state_hook(&mut self, hook: Option>) { + self.system_caller.with_state_hook(hook); + } + + fn evm_mut(&mut self) -> &mut Self::Evm { + &mut self.evm + } + + fn evm(&self) -> &Self::Evm { + &self.evm + } +} + +/// Block executor factory for EV transactions. +#[derive(Debug, Clone, Default, Copy)] +pub struct EvBlockExecutorFactory { + receipt_builder: R, + spec: Spec, + evm_factory: EvmFactory, +} + +impl EvBlockExecutorFactory { + pub const fn new(receipt_builder: R, spec: Spec, evm_factory: EvmFactory) -> Self { + Self { receipt_builder, spec, evm_factory } + } + + pub const fn receipt_builder(&self) -> &R { + &self.receipt_builder + } + + pub const fn spec(&self) -> &Spec { + &self.spec + } + + pub const fn evm_factory(&self) -> &EvmFactory { + &self.evm_factory + } +} + +impl BlockExecutorFactory for EvBlockExecutorFactory +where + R: ReceiptBuilder> + + Clone, + Spec: EthExecutorSpec + Clone, + EvmF: EvmFactory + FromTxWithEncoded>, + Self: 'static, +{ + type EvmFactory = EvmF; + type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>; + type Transaction = R::Transaction; + type Receipt = R::Receipt; + + fn evm_factory(&self) -> &Self::EvmFactory { + &self.evm_factory + } + + fn create_executor<'a, DB, I>( + &'a self, + evm: EvmF::Evm<&'a mut State, I>, + ctx: Self::ExecutionCtx<'a>, + ) -> impl BlockExecutorFor<'a, Self, DB, I> + where + DB: Database + 'a, + I: Inspector>> + 'a, + { + EvBlockExecutor::new(evm, ctx, self.spec.clone(), self.receipt_builder.clone()) + } +} diff --git a/crates/node/src/executor.rs b/crates/node/src/executor.rs index 5bcbc0c..8046257 100644 --- a/crates/node/src/executor.rs +++ b/crates/node/src/executor.rs @@ -1,27 +1,318 @@ //! Helpers to build the ev-reth executor with EV-specific hooks applied. -use alloy_evm::eth::{spec::EthExecutorSpec, EthEvmFactory}; +use alloy_consensus::{BlockHeader, Header}; +use alloy_eips::Decodable2718; +use alloy_evm::eth::spec::EthExecutorSpec; +use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; +use alloy_primitives::U256; +use alloy_rpc_types_engine::ExecutionData; use ev_revm::{ - with_ev_handler, BaseFeeRedirect, BaseFeeRedirectSettings, ContractSizeLimitSettings, - EvEvmFactory, MintPrecompileSettings, + BaseFeeRedirect, BaseFeeRedirectSettings, ContractSizeLimitSettings, EvTxEvmFactory, + MintPrecompileSettings, }; -use reth_chainspec::ChainSpec; +use reth_chainspec::{ChainSpec, EthChainSpec}; use reth_ethereum::{ chainspec::EthereumHardforks, - evm::EthEvmConfig, node::{ api::FullNodeTypes, builder::{components::ExecutorBuilder as RethExecutorBuilder, BuilderContext}, }, }; use reth_ethereum_forks::Hardforks; +use reth_evm::{ + ConfigureEngineEvm, ConfigureEvm, EvmEnv, EvmEnvFor, ExecutableTxIterator, ExecutionCtxFor, + NextBlockEnvAttributes, TransactionEnv, +}; use reth_node_builder::PayloadBuilderConfig; +use reth_primitives_traits::{ + constants::MAX_TX_GAS_LIMIT_OSAKA, SealedBlock, SealedHeader, SignedTransaction, TxTy, +}; +use reth_revm::revm::{ + context::{BlockEnv, CfgEnv}, + context_interface::block::BlobExcessGasAndPrice, + primitives::hardfork::SpecId, +}; +use reth_errors::RethError; use tracing::info; +use crate::evm_executor::{EvBlockExecutorFactory, EvReceiptBuilder}; use crate::{config::EvolvePayloadBuilderConfig, EvolveNode}; +use ev_primitives::{EvPrimitives, EvTxEnvelope}; +use reth_evm_ethereum::{revm_spec, revm_spec_by_timestamp_and_block_number, EthBlockAssembler}; /// Type alias for the EV-aware EVM config we install into the node. -pub type EvolveEvmConfig = EthEvmConfig>; +pub type EvolveEvmConfig = EvEvmConfig; + +/// EVM config wired for EvPrimitives. +#[derive(Debug, Clone)] +pub struct EvEvmConfig { + pub executor_factory: EvBlockExecutorFactory, EvmFactory>, + pub block_assembler: EthBlockAssembler, +} + +impl EvEvmConfig { + pub fn new(chain_spec: std::sync::Arc) -> Self { + Self::new_with_evm_factory(chain_spec, EvTxEvmFactory::default()) + } +} + +impl EvEvmConfig { + pub fn new_with_evm_factory( + chain_spec: std::sync::Arc, + evm_factory: EvmFactory, + ) -> Self { + Self { + block_assembler: EthBlockAssembler::new(chain_spec.clone()), + executor_factory: EvBlockExecutorFactory::new(EvReceiptBuilder, chain_spec, evm_factory), + } + } + + pub const fn chain_spec(&self) -> &std::sync::Arc { + self.executor_factory.spec() + } + + pub fn with_extra_data(mut self, extra_data: alloy_primitives::Bytes) -> Self { + self.block_assembler.extra_data = extra_data; + self + } +} + +impl ConfigureEvm for EvEvmConfig +where + ChainSpec: EthExecutorSpec + EthChainSpec
+ Hardforks + 'static, + EvmF: reth_evm::EvmFactory< + Tx: TransactionEnv, + Spec = SpecId, + Precompiles = reth_evm::precompiles::PrecompilesMap, + > + Clone + + std::fmt::Debug + + Send + + Sync + + Unpin + + 'static, + EvmF::Tx: FromRecoveredTx + FromTxWithEncoded + Clone, +{ + type Primitives = EvPrimitives; + type Error = std::convert::Infallible; + type NextBlockEnvCtx = NextBlockEnvAttributes; + type BlockExecutorFactory = + EvBlockExecutorFactory, EvmF>; + type BlockAssembler = EthBlockAssembler; + + fn block_executor_factory(&self) -> &Self::BlockExecutorFactory { + &self.executor_factory + } + + fn block_assembler(&self) -> &Self::BlockAssembler { + &self.block_assembler + } + + fn evm_env(&self, header: &Header) -> Result { + let blob_params = self.chain_spec().blob_params_at_timestamp(header.timestamp); + let spec = revm_spec(self.chain_spec(), header); + + let mut cfg_env = + CfgEnv::new().with_chain_id(self.chain_spec().chain().id()).with_spec(spec); + + if let Some(blob_params) = &blob_params { + cfg_env.set_max_blobs_per_tx(blob_params.max_blobs_per_tx); + } + + if self.chain_spec().is_osaka_active_at_timestamp(header.timestamp) { + cfg_env.tx_gas_limit_cap = Some(MAX_TX_GAS_LIMIT_OSAKA); + } + + let blob_excess_gas_and_price = + header.excess_blob_gas.zip(blob_params).map(|(excess_blob_gas, params)| { + let blob_gasprice = params.calc_blob_fee(excess_blob_gas); + BlobExcessGasAndPrice { excess_blob_gas, blob_gasprice } + }); + + let block_env = BlockEnv { + number: U256::from(header.number), + beneficiary: header.beneficiary, + timestamp: U256::from(header.timestamp), + difficulty: if spec >= SpecId::MERGE { + U256::ZERO + } else { + header.difficulty + }, + prevrandao: if spec >= SpecId::MERGE { + Some(header.mix_hash) + } else { + None + }, + gas_limit: header.gas_limit, + basefee: header.base_fee_per_gas.unwrap_or_default(), + blob_excess_gas_and_price, + }; + + Ok(EvmEnv { cfg_env, block_env }) + } + + fn next_evm_env( + &self, + parent: &Header, + attributes: &NextBlockEnvAttributes, + ) -> Result { + let chain_spec = self.chain_spec(); + let blob_params = chain_spec.blob_params_at_timestamp(attributes.timestamp); + let spec_id = revm_spec_by_timestamp_and_block_number( + chain_spec, + attributes.timestamp, + parent.number() + 1, + ); + + let mut cfg = + CfgEnv::new().with_chain_id(self.chain_spec().chain().id()).with_spec(spec_id); + + if let Some(blob_params) = &blob_params { + cfg.set_max_blobs_per_tx(blob_params.max_blobs_per_tx); + } + + if self.chain_spec().is_osaka_active_at_timestamp(attributes.timestamp) { + cfg.tx_gas_limit_cap = Some(MAX_TX_GAS_LIMIT_OSAKA); + } + + let blob_excess_gas_and_price = + parent.excess_blob_gas.zip(blob_params).map(|(excess_blob_gas, params)| { + let blob_gasprice = params.calc_blob_fee(excess_blob_gas); + BlobExcessGasAndPrice { excess_blob_gas, blob_gasprice } + }); + + let mut gas_limit = parent.gas_limit; + let mut basefee = None; + + if self + .chain_spec() + .fork(reth_ethereum_forks::EthereumHardfork::London) + .transitions_at_block(parent.number + 1) + { + let elasticity_multiplier = self + .chain_spec() + .base_fee_params_at_timestamp(attributes.timestamp) + .elasticity_multiplier; + gas_limit *= elasticity_multiplier as u64; + basefee = Some(alloy_eips::eip1559::INITIAL_BASE_FEE); + } + + let block_env = BlockEnv { + number: U256::from(parent.number + 1), + beneficiary: attributes.suggested_fee_recipient, + timestamp: U256::from(attributes.timestamp), + difficulty: U256::ZERO, + prevrandao: Some(attributes.prev_randao), + gas_limit, + basefee: basefee.unwrap_or_default(), + blob_excess_gas_and_price, + }; + + Ok(EvmEnv { cfg_env: cfg, block_env }) + } + + fn context_for_block<'a>( + &self, + block: &'a SealedBlock, + ) -> Result, Self::Error> { + Ok(alloy_evm::eth::EthBlockExecutionCtx { + parent_hash: block.header().parent_hash, + parent_beacon_block_root: block.header().parent_beacon_block_root, + ommers: &block.body().ommers, + withdrawals: block.body().withdrawals.as_ref().map(std::borrow::Cow::Borrowed), + }) + } + + fn context_for_next_block( + &self, + parent: &SealedHeader
, + attributes: Self::NextBlockEnvCtx, + ) -> Result, Self::Error> { + Ok(alloy_evm::eth::EthBlockExecutionCtx { + parent_hash: parent.hash(), + parent_beacon_block_root: attributes.parent_beacon_block_root, + ommers: &[], + withdrawals: attributes.withdrawals.map(std::borrow::Cow::Owned), + }) + } +} + +impl ConfigureEngineEvm for EvEvmConfig +where + ChainSpec: EthExecutorSpec + EthChainSpec
+ Hardforks + 'static, + EvmF: reth_evm::EvmFactory< + Tx: TransactionEnv + + FromRecoveredTx + + FromTxWithEncoded, + Spec = SpecId, + Precompiles = reth_evm::precompiles::PrecompilesMap, + > + Clone + + std::fmt::Debug + + Send + + Sync + + Unpin + + 'static, +{ + fn evm_env_for_payload(&self, payload: &ExecutionData) -> EvmEnvFor { + let timestamp = payload.payload.timestamp(); + let block_number = payload.payload.block_number(); + + let blob_params = self.chain_spec().blob_params_at_timestamp(timestamp); + let spec = + revm_spec_by_timestamp_and_block_number(self.chain_spec(), timestamp, block_number); + + let mut cfg_env = + CfgEnv::new().with_chain_id(self.chain_spec().chain().id()).with_spec(spec); + + if let Some(blob_params) = &blob_params { + cfg_env.set_max_blobs_per_tx(blob_params.max_blobs_per_tx); + } + + if self.chain_spec().is_osaka_active_at_timestamp(timestamp) { + cfg_env.tx_gas_limit_cap = Some(MAX_TX_GAS_LIMIT_OSAKA); + } + + let blob_excess_gas_and_price = + payload.payload.excess_blob_gas().zip(blob_params).map(|(excess_blob_gas, params)| { + let blob_gasprice = params.calc_blob_fee(excess_blob_gas); + BlobExcessGasAndPrice { excess_blob_gas, blob_gasprice } + }); + + let block_env = BlockEnv { + number: U256::from(block_number), + beneficiary: payload.payload.fee_recipient(), + timestamp: U256::from(timestamp), + difficulty: if spec >= SpecId::MERGE { + U256::ZERO + } else { + payload.payload.as_v1().prev_randao.into() + }, + prevrandao: (spec >= SpecId::MERGE).then(|| payload.payload.as_v1().prev_randao), + gas_limit: payload.payload.gas_limit(), + basefee: payload.payload.saturated_base_fee_per_gas(), + blob_excess_gas_and_price, + }; + + EvmEnv { cfg_env, block_env } + } + + fn context_for_payload<'a>(&self, payload: &'a ExecutionData) -> ExecutionCtxFor<'a, Self> { + alloy_evm::eth::EthBlockExecutionCtx { + parent_hash: payload.parent_hash(), + parent_beacon_block_root: payload.sidecar.parent_beacon_block_root(), + ommers: &[], + withdrawals: payload.payload.withdrawals().map(|w| std::borrow::Cow::Owned(w.clone().into())), + } + } + + fn tx_iterator_for_payload(&self, payload: &ExecutionData) -> impl ExecutableTxIterator { + payload.payload.transactions().clone().into_iter().map(|tx| { + let tx = + TxTy::::decode_2718_exact(tx.as_ref()).map_err(RethError::other)?; + let signer = tx.try_recover().map_err(RethError::other)?; + Ok::<_, RethError>(tx.with_signer(signer)) + }) + } +} /// Builds the EV-aware EVM configuration by wrapping the default config with the EV handler. pub fn build_evm_config(ctx: &BuilderContext) -> eyre::Result @@ -30,8 +321,6 @@ where ChainSpec: Hardforks + EthExecutorSpec + EthereumHardforks, { let chain_spec = ctx.chain_spec(); - let base_config = EthEvmConfig::new(chain_spec.clone()) - .with_extra_data(ctx.payload_builder_config().extra_data_bytes()); let evolve_config = EvolvePayloadBuilderConfig::from_chain_spec(chain_spec.as_ref())?; evolve_config.validate()?; @@ -65,12 +354,14 @@ where ContractSizeLimitSettings::new(limit, activation) }); - Ok(with_ev_handler( - base_config, + let factory = EvTxEvmFactory::new( redirect, mint_precompile, contract_size_limit, - )) + ); + + Ok(EvEvmConfig::new_with_evm_factory(chain_spec.clone(), factory) + .with_extra_data(ctx.payload_builder_config().extra_data_bytes())) } /// Thin wrapper so we can plug the EV executor into the node components builder. diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index b25d5d0..383854b 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -19,10 +19,18 @@ pub mod config; pub mod error; /// Executor wiring for EV aware execution. pub mod executor; +/// EV-specific EVM executor building blocks. +pub mod evm_executor; /// Node composition and payload types. pub mod node; +/// Payload types for EvPrimitives. +pub mod payload_types; /// Payload service integration. pub mod payload_service; +/// RPC wiring for EvTxEnvelope support. +pub mod rpc; +/// Transaction pool wiring and validation. +pub mod txpool; /// Payload validator integration. pub mod validator; @@ -35,5 +43,6 @@ pub use config::{ConfigError, EvolvePayloadBuilderConfig}; pub use error::EvolveEngineError; pub use executor::{build_evm_config, EvolveEvmConfig, EvolveExecutorBuilder}; pub use node::{log_startup, EvolveEngineTypes, EvolveNode, EvolveNodeAddOns}; +pub use payload_types::EvBuiltPayload; pub use payload_service::{EvolveEnginePayloadBuilder, EvolvePayloadBuilderBuilder}; pub use validator::{EvolveEngineValidator, EvolveEngineValidatorBuilder}; diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index 6a3cbfb..3919c9b 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -13,12 +13,11 @@ use reth_ethereum::{ rpc::RpcAddOns, Node, NodeAdapter, }, - node::{EthereumNetworkBuilder, EthereumPoolBuilder}, - EthereumEthApiBuilder, + node::EthereumNetworkBuilder, }, - primitives::SealedBlock, }; -use reth_payload_builder::EthBuiltPayload; +use ev_primitives::EvPrimitives; +use reth_primitives_traits::SealedBlock; use serde::{Deserialize, Serialize}; use tracing::info; @@ -26,6 +25,9 @@ use crate::{ attributes::{EvolveEnginePayloadAttributes, EvolveEnginePayloadBuilderAttributes}, executor::EvolveExecutorBuilder, payload_service::EvolvePayloadBuilderBuilder, + payload_types::EvBuiltPayload, + rpc::EvEthApiBuilder, + txpool::EvolvePoolBuilder, validator::EvolveEngineValidatorBuilder, }; @@ -36,7 +38,7 @@ pub struct EvolveEngineTypes; impl PayloadTypes for EvolveEngineTypes { type ExecutionData = ExecutionData; - type BuiltPayload = EthBuiltPayload; + type BuiltPayload = EvBuiltPayload; type PayloadAttributes = EvolveEnginePayloadAttributes; type PayloadBuilderAttributes = EvolveEnginePayloadBuilderAttributes; @@ -75,14 +77,14 @@ impl EvolveNode { } impl NodeTypes for EvolveNode { - type Primitives = reth_ethereum::EthPrimitives; + type Primitives = EvPrimitives; type ChainSpec = ChainSpec; - type Storage = reth_ethereum::provider::EthStorage; + type Storage = reth_ethereum::provider::EthStorage; type Payload = EvolveEngineTypes; } /// Evolve node addons configuring RPC types with custom engine validator. -pub type EvolveNodeAddOns = RpcAddOns; +pub type EvolveNodeAddOns = RpcAddOns; impl Node for EvolveNode where @@ -90,7 +92,7 @@ where { type ComponentsBuilder = ComponentsBuilder< N, - EthereumPoolBuilder, + EvolvePoolBuilder, BasicPayloadServiceBuilder, EthereumNetworkBuilder, EvolveExecutorBuilder, @@ -101,7 +103,7 @@ where fn components_builder(&self) -> Self::ComponentsBuilder { ComponentsBuilder::default() .node_types::() - .pool(EthereumPoolBuilder::default()) + .pool(EvolvePoolBuilder::default()) .executor(EvolveExecutorBuilder::default()) .payload(BasicPayloadServiceBuilder::new( EvolvePayloadBuilderBuilder::new(), diff --git a/crates/node/src/payload_service.rs b/crates/node/src/payload_service.rs index d966776..12fffd4 100644 --- a/crates/node/src/payload_service.rs +++ b/crates/node/src/payload_service.rs @@ -15,9 +15,8 @@ use reth_ethereum::{ }, pool::{PoolTransaction, TransactionPool}, primitives::Header, - TransactionSigned, }; -use reth_payload_builder::{EthBuiltPayload, PayloadBuilderError}; +use reth_payload_builder::PayloadBuilderError; use reth_provider::HeaderProvider; use reth_revm::cached::CachedReads; use tokio::runtime::Handle; @@ -26,9 +25,11 @@ use tracing::info; use crate::{ attributes::EvolveEnginePayloadBuilderAttributes, builder::EvolvePayloadBuilder, config::EvolvePayloadBuilderConfig, executor::EvolveEvmConfig, node::EvolveEngineTypes, + payload_types::EvBuiltPayload, }; use evolve_ev_reth::config::set_current_block_gas_limit; +use ev_primitives::{EvPrimitives, TransactionSigned}; /// Evolve payload service builder that integrates with the evolve payload builder. #[derive(Debug, Clone)] @@ -68,7 +69,7 @@ where Types: NodeTypes< Payload = EvolveEngineTypes, ChainSpec = ChainSpec, - Primitives = reth_ethereum::EthPrimitives, + Primitives = EvPrimitives, >, >, Pool: TransactionPool> @@ -128,7 +129,7 @@ where + 'static, { type Attributes = EvolveEnginePayloadBuilderAttributes; - type BuiltPayload = EthBuiltPayload; + type BuiltPayload = EvBuiltPayload; fn try_build( &self, @@ -193,9 +194,9 @@ where sealed_block.gas_used ); - // Convert to EthBuiltPayload. + // Convert to EvBuiltPayload. let gas_used = sealed_block.gas_used; - let built_payload = EthBuiltPayload::new( + let built_payload = EvBuiltPayload::new( attributes.payload_id(), // Use the proper payload ID from attributes. Arc::new(sealed_block), U256::from(gas_used), // Block gas used. @@ -257,7 +258,7 @@ where .map_err(PayloadBuilderError::other)?; let gas_used = sealed_block.gas_used; - Ok(EthBuiltPayload::new( + Ok(EvBuiltPayload::new( attributes.payload_id(), Arc::new(sealed_block), U256::from(gas_used), diff --git a/crates/node/src/payload_types.rs b/crates/node/src/payload_types.rs new file mode 100644 index 0000000..715acd4 --- /dev/null +++ b/crates/node/src/payload_types.rs @@ -0,0 +1,178 @@ +use std::sync::Arc; + +use alloy_eips::eip7685::Requests; +use alloy_primitives::U256; +use alloy_rpc_types_engine::{ + BlobsBundleV1, BlobsBundleV2, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, + ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadFieldV2, + ExecutionPayloadV1, ExecutionPayloadV3, PayloadId, +}; +use ev_primitives::EvPrimitives; +use reth_payload_builder::BlobSidecars; +use reth_payload_primitives::BuiltPayload; +use reth_primitives_traits::SealedBlock; + +/// Built payload for EvPrimitives. +#[derive(Debug, Clone)] +pub struct EvBuiltPayload { + id: PayloadId, + block: Arc>, + fees: U256, + sidecars: BlobSidecars, + requests: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum EvBuiltPayloadConversionError { + #[error("unexpected EIP-7594 sidecars for this payload")] + UnexpectedEip7594Sidecars, + #[error("unexpected EIP-4844 sidecars for this payload")] + UnexpectedEip4844Sidecars, +} + +impl EvBuiltPayload { + pub const fn new( + id: PayloadId, + block: Arc>, + fees: U256, + requests: Option, + ) -> Self { + Self { id, block, fees, requests, sidecars: BlobSidecars::Empty } + } + + pub const fn id(&self) -> PayloadId { + self.id + } + + pub fn block(&self) -> &SealedBlock { + &self.block + } + + pub const fn fees(&self) -> U256 { + self.fees + } + + pub const fn sidecars(&self) -> &BlobSidecars { + &self.sidecars + } + + pub fn with_sidecars(mut self, sidecars: impl Into) -> Self { + self.sidecars = sidecars.into(); + self + } + + pub fn try_into_v3(self) -> Result { + let Self { block, fees, sidecars, .. } = self; + + let blobs_bundle = match sidecars { + BlobSidecars::Empty => BlobsBundleV1::empty(), + BlobSidecars::Eip4844(sidecars) => BlobsBundleV1::from(sidecars), + BlobSidecars::Eip7594(_) => { + return Err(EvBuiltPayloadConversionError::UnexpectedEip7594Sidecars) + } + }; + + Ok(ExecutionPayloadEnvelopeV3 { + execution_payload: ExecutionPayloadV3::from_block_unchecked( + block.hash(), + &Arc::unwrap_or_clone(block).into_block(), + ), + block_value: fees, + should_override_builder: false, + blobs_bundle, + }) + } + + pub fn try_into_v4(self) -> Result { + Ok(ExecutionPayloadEnvelopeV4 { + execution_requests: self.requests.clone().unwrap_or_default(), + envelope_inner: self.try_into()?, + }) + } + + pub fn try_into_v5(self) -> Result { + let Self { block, fees, sidecars, requests, .. } = self; + + let blobs_bundle = match sidecars { + BlobSidecars::Empty => BlobsBundleV2::empty(), + BlobSidecars::Eip7594(sidecars) => BlobsBundleV2::from(sidecars), + BlobSidecars::Eip4844(_) => { + return Err(EvBuiltPayloadConversionError::UnexpectedEip4844Sidecars) + } + }; + + Ok(ExecutionPayloadEnvelopeV5 { + execution_payload: ExecutionPayloadV3::from_block_unchecked( + block.hash(), + &Arc::unwrap_or_clone(block).into_block(), + ), + block_value: fees, + should_override_builder: false, + blobs_bundle, + execution_requests: requests.unwrap_or_default(), + }) + } +} + +impl BuiltPayload for EvBuiltPayload { + type Primitives = EvPrimitives; + + fn block(&self) -> &SealedBlock { + &self.block + } + + fn fees(&self) -> U256 { + self.fees + } + + fn requests(&self) -> Option { + self.requests.clone() + } +} + +impl From for ExecutionPayloadV1 { + fn from(value: EvBuiltPayload) -> Self { + Self::from_block_unchecked( + value.block().hash(), + &Arc::unwrap_or_clone(value.block).into_block(), + ) + } +} + +impl From for ExecutionPayloadEnvelopeV2 { + fn from(value: EvBuiltPayload) -> Self { + let EvBuiltPayload { block, fees, .. } = value; + + Self { + block_value: fees, + execution_payload: ExecutionPayloadFieldV2::from_block_unchecked( + block.hash(), + &Arc::unwrap_or_clone(block).into_block(), + ), + } + } +} + +impl TryFrom for ExecutionPayloadEnvelopeV3 { + type Error = EvBuiltPayloadConversionError; + + fn try_from(value: EvBuiltPayload) -> Result { + value.try_into_v3() + } +} + +impl TryFrom for ExecutionPayloadEnvelopeV4 { + type Error = EvBuiltPayloadConversionError; + + fn try_from(value: EvBuiltPayload) -> Result { + value.try_into_v4() + } +} + +impl TryFrom for ExecutionPayloadEnvelopeV5 { + type Error = EvBuiltPayloadConversionError; + + fn try_from(value: EvBuiltPayload) -> Result { + value.try_into_v5() + } +} diff --git a/crates/node/src/rpc.rs b/crates/node/src/rpc.rs new file mode 100644 index 0000000..102b8e5 --- /dev/null +++ b/crates/node/src/rpc.rs @@ -0,0 +1,282 @@ +//! RPC wiring for EvTxEnvelope support. + +use alloy_consensus::error::ValueError; +use alloy_consensus::transaction::Recovered; +use alloy_consensus::SignableTransaction; +use alloy_consensus_any::AnyReceiptEnvelope; +use alloy_network::{Ethereum, TxSigner}; +use alloy_primitives::{Address, Signature}; +use alloy_rpc_types_eth::{Log, Transaction, TransactionInfo, TransactionRequest, TransactionReceipt}; +use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks, Hardforks}; +use reth_evm::{ConfigureEvm, SpecFor, TxEnvFor}; +use reth_node_api::{FullNodeComponents, FullNodeTypes, NodeTypes}; +use reth_node_builder::rpc::{EthApiBuilder, EthApiCtx}; +use reth_rpc::EthApi; +use reth_rpc_convert::transaction::{ + ConvertReceiptInput, EthTxEnvError, ReceiptConverter, RpcTxConverter, SimTxConverter, + TryIntoSimTx, TryIntoTxEnv, TxEnvConverter, +}; +use reth_rpc_convert::{ + RpcConvert, RpcConverter, RpcTransaction, RpcTxReq, RpcTypes, SignTxRequestError, + SignableTxRequest, +}; +use reth_rpc_eth_api::{ + helpers::{pending_block::BuildPendingEnv, AddDevSigners}, + FullEthApiServer, FromEvmError, RpcNodeCore, +}; +use reth_rpc_eth_types::receipt::build_receipt; +use reth_rpc_eth_types::EthApiError; +use std::marker::PhantomData; + +use ev_primitives::{EvPrimitives, EvTxEnvelope}; +use ev_revm::EvTxEnv; +use crate::EvolveEvmConfig; + +/// Ev-specific RPC types using Ethereum responses with a custom request wrapper. +#[derive(Clone, Debug)] +pub struct EvRpcTypes; + +impl RpcTypes for EvRpcTypes { + type Header = ::Header; + type Receipt = TransactionReceipt>; + type TransactionResponse = ::TransactionResponse; + type TransactionRequest = EvTransactionRequest; +} + +/// Transaction request wrapper to satisfy local trait bounds. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +pub struct EvTransactionRequest(pub TransactionRequest); + +impl From for EvTransactionRequest { + fn from(value: TransactionRequest) -> Self { + Self(value) + } +} + +impl AsRef for EvTransactionRequest { + fn as_ref(&self) -> &TransactionRequest { + &self.0 + } +} + +impl AsMut for EvTransactionRequest { + fn as_mut(&mut self) -> &mut TransactionRequest { + &mut self.0 + } +} + +impl SignableTxRequest for EvTransactionRequest { + async fn try_build_and_sign( + self, + signer: impl TxSigner + Send, + ) -> Result { + let mut tx = + self.0.build_typed_tx().map_err(|_| SignTxRequestError::InvalidTransactionRequest)?; + let signature = signer.sign_transaction(&mut tx).await?; + let signed: reth_ethereum_primitives::TransactionSigned = + tx.into_signed(signature).into(); + Ok(EvTxEnvelope::Ethereum(signed)) + } +} + +impl TryIntoSimTx for EvTransactionRequest { + fn try_into_sim_tx(self) -> Result> { + self.0 + .try_into_sim_tx() + .map(EvTxEnvelope::Ethereum) + .map_err(|err| err.map(EvTransactionRequest)) + } +} + +impl TryIntoTxEnv for EvTransactionRequest { + type Err = EthTxEnvError; + + fn try_into_tx_env( + self, + cfg_env: &reth_revm::revm::context::CfgEnv, + block_env: &reth_revm::revm::context::BlockEnv, + ) -> Result { + self.0 + .try_into_tx_env(cfg_env, block_env) + .map(EvTxEnv::from) + } +} + +/// Receipt converter for EvPrimitives. +#[derive(Debug, Clone)] +pub struct EvReceiptConverter { + chain_spec: std::sync::Arc, +} + +impl EvReceiptConverter { + pub const fn new(chain_spec: std::sync::Arc) -> Self { + Self { chain_spec } + } +} + +impl ReceiptConverter for EvReceiptConverter +where + ChainSpec: EthChainSpec + 'static, +{ + type RpcReceipt = TransactionReceipt>; + type Error = EthApiError; + + fn convert_receipts( + &self, + inputs: Vec>, + ) -> Result, Self::Error> { + let mut receipts = Vec::with_capacity(inputs.len()); + + for input in inputs { + let blob_params = self.chain_spec.blob_params_at_timestamp(input.meta.timestamp); + receipts.push(build_receipt(input, blob_params, |receipt, next_log_index, meta| { + let rpc_receipt = receipt.into_rpc(next_log_index, meta); + let tx_type = u8::from(rpc_receipt.tx_type); + let inner = >::from(rpc_receipt).with_bloom(); + AnyReceiptEnvelope { inner, r#type: tx_type } + })); + } + + Ok(receipts) + } +} + +/// RPC converter type for EvTxEnvelope-based nodes. +pub type EvRpcConvert = RpcConverter< + EvRpcTypes, + EvolveEvmConfig, + EvReceiptConverter<<::Types as NodeTypes>::ChainSpec>, + (), + (), + EvSimTxConverter, + EvRpcTxConverter, + EvTxEnvConverter, +>; + +/// Eth API type for EvTxEnvelope-based nodes. +pub type EvEthApiFor = EthApi>; + +/// Builds [`EthApi`] for EvTxEnvelope nodes. +#[derive(Debug, Default)] +pub struct EvEthApiBuilder; + +impl EthApiBuilder for EvEthApiBuilder +where + N: FullNodeComponents< + Types: NodeTypes< + Primitives = EvPrimitives, + ChainSpec: + Hardforks + EthereumHardforks + EthChainSpec + std::fmt::Debug + Send + Sync + 'static, + >, + Evm = EvolveEvmConfig, + > + RpcNodeCore< + Primitives = EvPrimitives, + Provider = ::Provider, + Pool = ::Pool, + Evm = EvolveEvmConfig, + >, + ::Provider: ChainSpecProvider< + ChainSpec = <::Types as NodeTypes>::ChainSpec, + >, + ::Evm: + ConfigureEvm>, + TxEnvFor<::Evm>: From, + EvRpcConvert: RpcConvert< + Primitives = EvPrimitives, + TxEnv = TxEnvFor<::Evm>, + Error = EthApiError, + Network = EvRpcTypes, + Spec = SpecFor<::Evm>, + >, + EthApiError: FromEvmError<::Evm>, + EvEthApiFor: FullEthApiServer< + Provider = ::Provider, + Pool = ::Pool, + > + AddDevSigners, +{ + type EthApi = EvEthApiFor; + + async fn build_eth_api(self, ctx: EthApiCtx<'_, N>) -> eyre::Result { + let receipt_converter = + EvReceiptConverter::new(FullNodeComponents::provider(ctx.components).chain_spec()); + let rpc_converter = RpcConverter::new(receipt_converter) + .with_sim_tx_converter(EvSimTxConverter::default()) + .with_rpc_tx_converter(EvRpcTxConverter::default()); + let rpc_converter = + rpc_converter.with_tx_env_converter(EvTxEnvConverter::::default()); + + Ok(ctx.eth_api_builder().with_rpc_converter(rpc_converter).build()) + } +} + +/// Converts EvTxEnvelope into RPC transaction responses. +#[derive(Clone, Debug, Default)] +pub struct EvRpcTxConverter; + +impl RpcTxConverter, TransactionInfo> for EvRpcTxConverter { + type Err = EthApiError; + + fn convert_rpc_tx( + &self, + tx: EvTxEnvelope, + signer: Address, + tx_info: TransactionInfo, + ) -> Result, Self::Err> { + match tx { + EvTxEnvelope::Ethereum(inner) => Ok(Transaction::from_transaction( + Recovered::new_unchecked(inner.into(), signer), + tx_info, + )), + EvTxEnvelope::EvNode(_) => Err(EthApiError::TransactionConversionError), + } + } +} + +/// Converts transaction requests into simulated EvTxEnvelope transactions. +#[derive(Clone, Debug, Default)] +pub struct EvSimTxConverter; + +impl SimTxConverter, EvTxEnvelope> for EvSimTxConverter { + type Err = ValueError>; + + fn convert_sim_tx(&self, tx_req: RpcTxReq) -> Result { + tx_req + .0 + .try_into_sim_tx() + .map(EvTxEnvelope::Ethereum) + .map_err(|err| err.map(EvTransactionRequest)) + } +} + +/// Converts transaction requests into EvTxEnv. +#[derive(Clone, Debug)] +pub struct EvTxEnvConverter(PhantomData); + +impl Default for EvTxEnvConverter { + fn default() -> Self { + Self(PhantomData) + } +} + +impl TxEnvConverter, TxEnvFor, SpecFor> + for EvTxEnvConverter +where + Evm: ConfigureEvm + Send + Sync + 'static, + TxEnvFor: From, +{ + type Error = EthTxEnvError; + + fn convert_tx_env( + &self, + tx_req: RpcTxReq, + cfg_env: &reth_revm::revm::context::CfgEnv>, + block_env: &reth_revm::revm::context::BlockEnv, + ) -> Result, Self::Error> { + tx_req + .0 + .try_into_tx_env(cfg_env, block_env) + .map(EvTxEnv::from) + .map(Into::into) + } +} diff --git a/crates/node/src/txpool.rs b/crates/node/src/txpool.rs new file mode 100644 index 0000000..121260d --- /dev/null +++ b/crates/node/src/txpool.rs @@ -0,0 +1,491 @@ +use std::sync::Arc; + +use alloy_consensus::{ + transaction::{Recovered, TxHashRef}, + BlobTransactionValidationError, Signed, Typed2718, +}; +use alloy_eips::{ + eip2718::Encodable2718, + eip2718::WithEncoded, + eip7594::BlobTransactionSidecarVariant, + eip7840::BlobParams, + merge::EPOCH_SLOTS, +}; +use alloy_primitives::{Address, U256}; +use c_kzg::KzgSettings; +use ev_primitives::{EvNodeTransaction, EvPooledTxEnvelope, EvTxEnvelope, TransactionSigned}; +use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks}; +use reth_node_api::{FullNodeTypes, NodeTypes}; +use reth_node_builder::components::{create_blob_store_with_cache, PoolBuilder, TxPoolBuilder}; +use reth_node_builder::BuilderContext; +use reth_primitives_traits::NodePrimitives; +use reth_storage_api::{AccountInfoReader, StateProviderFactory}; +use reth_transaction_pool::{ + blobstore::DiskFileBlobStore, + error::{InvalidPoolTransactionError, PoolTransactionError}, + CoinbaseTipOrdering, EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, + EthTransactionValidator, PoolTransaction, TransactionOrigin, TransactionValidationOutcome, + TransactionValidationTaskExecutor, TransactionValidator, +}; +use tracing::{debug, info}; + +#[derive(Debug, Clone)] +pub struct EvPooledTransaction { + inner: EthPooledTransaction, +} + +impl EvPooledTransaction { + pub fn new(transaction: Recovered, encoded_length: usize) -> Self { + Self { inner: EthPooledTransaction::new(transaction, encoded_length) } + } + + pub const fn transaction(&self) -> &Recovered { + self.inner.transaction() + } +} + +impl PoolTransaction for EvPooledTransaction { + type TryFromConsensusError = + alloy_consensus::error::ValueError; + type Consensus = EvTxEnvelope; + type Pooled = EvPooledTxEnvelope; + + fn clone_into_consensus(&self) -> Recovered { + self.inner.transaction().clone() + } + + fn into_consensus(self) -> Recovered { + self.inner.transaction + } + + fn into_consensus_with2718(self) -> WithEncoded> { + self.inner.transaction.into_encoded() + } + + fn from_pooled(tx: Recovered) -> Self { + let encoded_length = tx.encode_2718_len(); + let (tx, signer) = tx.into_parts(); + match tx { + EvPooledTxEnvelope::Ethereum(tx) => match tx { + reth_ethereum_primitives::PooledTransactionVariant::Eip4844(tx) => { + let (tx, sig, hash) = tx.into_parts(); + let (tx, blob) = tx.into_parts(); + let tx = Signed::new_unchecked(tx, sig, hash); + let tx = reth_ethereum_primitives::TransactionSigned::from(tx); + let tx = EvTxEnvelope::Ethereum(tx); + let tx = Recovered::new_unchecked(tx, signer); + let mut pooled = Self::new(tx, encoded_length); + pooled.inner.blob_sidecar = EthBlobTransactionSidecar::Present(blob); + pooled + } + tx => { + let tx = EvTxEnvelope::Ethereum(tx.into()); + let tx = Recovered::new_unchecked(tx, signer); + Self::new(tx, encoded_length) + } + }, + EvPooledTxEnvelope::EvNode(tx) => { + let tx = EvTxEnvelope::EvNode(tx); + let tx = Recovered::new_unchecked(tx, signer); + Self::new(tx, encoded_length) + } + } + } + + fn hash(&self) -> &alloy_primitives::TxHash { + self.inner.transaction.tx_hash() + } + + fn sender(&self) -> Address { + self.inner.transaction.signer() + } + + fn sender_ref(&self) -> &Address { + self.inner.transaction.signer_ref() + } + + fn cost(&self) -> &U256 { + &self.inner.cost + } + + fn encoded_length(&self) -> usize { + self.inner.encoded_length + } +} + +impl Typed2718 for EvPooledTransaction { + fn ty(&self) -> u8 { + self.inner.ty() + } +} + +impl reth_primitives_traits::InMemorySize for EvPooledTransaction { + fn size(&self) -> usize { + self.inner.size() + } +} + +impl alloy_consensus::Transaction for EvPooledTransaction { + fn chain_id(&self) -> Option { + self.inner.chain_id() + } + + fn nonce(&self) -> u64 { + self.inner.nonce() + } + + fn gas_limit(&self) -> u64 { + self.inner.gas_limit() + } + + fn gas_price(&self) -> Option { + self.inner.gas_price() + } + + fn max_fee_per_gas(&self) -> u128 { + self.inner.max_fee_per_gas() + } + + fn max_priority_fee_per_gas(&self) -> Option { + self.inner.max_priority_fee_per_gas() + } + + fn max_fee_per_blob_gas(&self) -> Option { + self.inner.max_fee_per_blob_gas() + } + + fn priority_fee_or_price(&self) -> u128 { + self.inner.priority_fee_or_price() + } + + fn effective_gas_price(&self, base_fee: Option) -> u128 { + self.inner.effective_gas_price(base_fee) + } + + fn is_dynamic_fee(&self) -> bool { + self.inner.is_dynamic_fee() + } + + fn kind(&self) -> alloy_primitives::TxKind { + self.inner.kind() + } + + fn is_create(&self) -> bool { + self.inner.is_create() + } + + fn value(&self) -> U256 { + self.inner.value() + } + + fn input(&self) -> &alloy_primitives::Bytes { + self.inner.input() + } + + fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> { + self.inner.access_list() + } + + fn blob_versioned_hashes(&self) -> Option<&[alloy_primitives::B256]> { + self.inner.blob_versioned_hashes() + } + + fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> { + self.inner.authorization_list() + } +} + +impl EthPoolTransaction for EvPooledTransaction { + fn take_blob(&mut self) -> EthBlobTransactionSidecar { + if self.is_eip4844() { + std::mem::replace(&mut self.inner.blob_sidecar, EthBlobTransactionSidecar::Missing) + } else { + EthBlobTransactionSidecar::None + } + } + + fn try_into_pooled_eip4844( + self, + sidecar: std::sync::Arc, + ) -> Option> { + let (signed_transaction, signer) = self.into_consensus().into_parts(); + match signed_transaction { + EvTxEnvelope::Ethereum(tx) => { + let pooled_transaction = + tx.try_into_pooled_eip4844(std::sync::Arc::unwrap_or_clone(sidecar)).ok()?; + Some(Recovered::new_unchecked( + EvPooledTxEnvelope::Ethereum(pooled_transaction), + signer, + )) + } + EvTxEnvelope::EvNode(_) => None, + } + } + + fn try_from_eip4844( + tx: Recovered, + sidecar: BlobTransactionSidecarVariant, + ) -> Option { + let (tx, signer) = tx.into_parts(); + match tx { + EvTxEnvelope::Ethereum(tx) => tx + .try_into_pooled_eip4844(sidecar) + .ok() + .map(|tx| Recovered::new_unchecked(EvPooledTxEnvelope::Ethereum(tx), signer)) + .map(Self::from_pooled), + EvTxEnvelope::EvNode(_) => None, + } + } + + fn validate_blob( + &self, + sidecar: &BlobTransactionSidecarVariant, + settings: &KzgSettings, + ) -> Result<(), BlobTransactionValidationError> { + match self.inner.transaction.inner() { + EvTxEnvelope::Ethereum(tx) => match tx.as_eip4844() { + Some(tx) => tx.tx().validate_blob(sidecar, settings), + None => Err(BlobTransactionValidationError::NotBlobTransaction(self.ty())), + }, + EvTxEnvelope::EvNode(_) => Err(BlobTransactionValidationError::NotBlobTransaction(self.ty())), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum EvTxPoolError { + #[error("evnode transaction must include at least one call")] + EmptyCalls, + #[error("only the first call may be CREATE")] + InvalidCreatePosition, + #[error("fee_payer and fee_payer_signature must be both present or both absent")] + InvalidSponsorshipFields, + #[error("invalid sponsor signature")] + InvalidSponsorSignature, + #[error("state provider error: {0}")] + StateProvider(String), +} + +impl PoolTransactionError for EvTxPoolError { + fn is_bad_transaction(&self) -> bool { + matches!( + self, + Self::EmptyCalls | Self::InvalidCreatePosition | Self::InvalidSponsorSignature + ) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[derive(Debug, Clone)] +pub struct EvTransactionValidator { + inner: Arc>, +} + +impl EvTransactionValidator { + pub fn new(inner: EthTransactionValidator) -> Self { + Self { inner: Arc::new(inner) } + } + + fn validate_evnode_calls(&self, tx: &EvNodeTransaction) -> Result<(), InvalidPoolTransactionError> { + if tx.calls.is_empty() { + return Err(InvalidPoolTransactionError::other(EvTxPoolError::EmptyCalls)); + } + if tx.calls.iter().skip(1).any(|call| call.to.is_create()) { + return Err(InvalidPoolTransactionError::other(EvTxPoolError::InvalidCreatePosition)); + } + Ok(()) + } + + fn ensure_state( + &self, + state: &mut Option>, + ) -> Result<(), InvalidPoolTransactionError> + where + Client: StateProviderFactory, + { + if state.is_none() { + let new_state = self + .inner + .client() + .latest() + .map_err(|err| InvalidPoolTransactionError::other(EvTxPoolError::StateProvider(err.to_string())))?; + *state = Some(Box::new(new_state)); + } + Ok(()) + } + + fn validate_sponsor_balance( + &self, + state: &mut Option>, + fee_payer: Address, + gas_cost: U256, + ) -> Result<(), InvalidPoolTransactionError> + where + Client: StateProviderFactory, + { + self.ensure_state(state)?; + let state = state.as_ref().expect("state provider is set"); + let account = state + .basic_account(&fee_payer) + .map_err(|err| InvalidPoolTransactionError::other(EvTxPoolError::StateProvider(err.to_string())))? + .unwrap_or_default(); + if account.balance < gas_cost { + return Err(InvalidPoolTransactionError::Overdraft { cost: gas_cost, balance: account.balance }); + } + Ok(()) + } + + fn validate_evnode( + &self, + pooled: &EvPooledTransaction, + sender_balance: U256, + state: &mut Option>, + ) -> Result<(), InvalidPoolTransactionError> + where + Client: StateProviderFactory, + { + let consensus = pooled.transaction().inner(); + let EvTxEnvelope::EvNode(tx) = consensus else { + if sender_balance < *pooled.cost() { + return Err(InvalidPoolTransactionError::Overdraft { cost: *pooled.cost(), balance: sender_balance }); + } + return Ok(()); + }; + + let tx = tx.tx(); + self.validate_evnode_calls(tx)?; + + match (tx.fee_payer, tx.fee_payer_signature.as_ref()) { + (None, None) => Ok(()), + (Some(fee_payer), Some(signature)) => { + let recovered = tx + .recover_sponsor(fee_payer, signature) + .map_err(|_| InvalidPoolTransactionError::other(EvTxPoolError::InvalidSponsorSignature))?; + if recovered != fee_payer { + return Err(InvalidPoolTransactionError::other(EvTxPoolError::InvalidSponsorSignature)); + } + + let gas_cost = + U256::from(tx.max_fee_per_gas).saturating_mul(U256::from(tx.gas_limit)); + self.validate_sponsor_balance(state, fee_payer, gas_cost)?; + Ok(()) + } + _ => Err(InvalidPoolTransactionError::other(EvTxPoolError::InvalidSponsorshipFields)), + } + } +} + +impl TransactionValidator for EvTransactionValidator +where + Client: ChainSpecProvider + StateProviderFactory, +{ + type Transaction = EvPooledTransaction; + + async fn validate_transaction( + &self, + origin: TransactionOrigin, + transaction: ::Transaction, + ) -> TransactionValidationOutcome { + let mut state = None; + let outcome = self + .inner + .validate_one_with_state(origin, transaction, &mut state); + + match outcome { + TransactionValidationOutcome::Valid { + balance, + state_nonce, + bytecode_hash, + transaction, + propagate, + authorities, + } => match self.validate_evnode(transaction.transaction(), balance, &mut state) { + Ok(()) => TransactionValidationOutcome::Valid { + balance, + state_nonce, + bytecode_hash, + transaction, + propagate, + authorities, + }, + Err(err) => TransactionValidationOutcome::Invalid(transaction.into_transaction(), err), + }, + other => other, + } + } +} + +/// Pool builder that wires the custom EvNode transaction validator. +#[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] +pub struct EvolvePoolBuilder; + +impl PoolBuilder for EvolvePoolBuilder +where + Types: NodeTypes< + ChainSpec: EthereumHardforks, + Primitives: NodePrimitives, + >, + Node: FullNodeTypes, +{ + type Pool = reth_transaction_pool::Pool< + TransactionValidationTaskExecutor>, + CoinbaseTipOrdering, + DiskFileBlobStore, + >; + + async fn build_pool(self, ctx: &BuilderContext) -> eyre::Result { + let pool_config = ctx.pool_config(); + + let blobs_disabled = ctx.config().txpool.blobpool_max_count == 0; + + let blob_cache_size = if let Some(blob_cache_size) = pool_config.blob_cache_size { + Some(blob_cache_size) + } else { + let current_timestamp = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); + let blob_params = ctx + .chain_spec() + .blob_params_at_timestamp(current_timestamp) + .unwrap_or_else(BlobParams::cancun); + + Some((blob_params.target_blob_count * EPOCH_SLOTS * 2) as u32) + }; + + let blob_store = create_blob_store_with_cache(ctx, blob_cache_size)?; + + let validator = TransactionValidationTaskExecutor::eth_builder(ctx.provider().clone()) + .with_head_timestamp(ctx.head().timestamp) + .set_eip4844(!blobs_disabled) + .kzg_settings(ctx.kzg_settings()?) + .with_max_tx_input_bytes(ctx.config().txpool.max_tx_input_bytes) + .with_local_transactions_config(pool_config.local_transactions_config.clone()) + .set_tx_fee_cap(ctx.config().rpc.rpc_tx_fee_cap) + .with_max_tx_gas_limit(ctx.config().txpool.max_tx_gas_limit) + .with_minimum_priority_fee(ctx.config().txpool.minimum_priority_fee) + .disable_balance_check() + .with_additional_tasks(ctx.config().txpool.additional_validation_tasks) + .build_with_tasks::(ctx.task_executor().clone(), blob_store.clone()) + .map(EvTransactionValidator::new); + + if validator.validator().inner.eip4844() { + let kzg_settings = validator.validator().inner.kzg_settings().clone(); + ctx.task_executor().spawn_blocking(async move { + let _ = kzg_settings.get(); + debug!(target: "reth::cli", "Initialized KZG settings"); + }); + } + + let transaction_pool = TxPoolBuilder::new(ctx) + .with_validator(validator) + .build_and_spawn_maintenance_task(blob_store, pool_config)?; + + info!(target: "reth::cli", "Transaction pool initialized"); + debug!(target: "reth::cli", "Spawned txpool maintenance task"); + + Ok(transaction_pool) + } +} diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index 5b1e285..fd8e40c 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -15,8 +15,9 @@ use reth_ethereum::{ builder::rpc::PayloadValidatorBuilder, }, }; +use ev_primitives::EvTxEnvelope; use reth_ethereum_payload_builder::EthereumExecutionPayloadValidator; -use reth_primitives_traits::{Block as _, RecoveredBlock}; +use reth_primitives_traits::{Block as _, RecoveredBlock, SealedBlock}; use tracing::info; use crate::{attributes::EvolveEnginePayloadAttributes, node::EvolveEngineTypes}; @@ -43,7 +44,7 @@ impl EvolveEngineValidator { } impl PayloadValidator for EvolveEngineValidator { - type Block = reth_ethereum::Block; + type Block = ev_primitives::Block; fn ensure_well_formed_payload( &self, @@ -55,9 +56,8 @@ impl PayloadValidator for EvolveEngineValidator { match self.inner.ensure_well_formed_payload(payload.clone()) { Ok(sealed_block) => { info!("Evolve engine validator: payload validation succeeded"); - sealed_block - .try_recover() - .map_err(|e| NewPayloadError::Other(e.into())) + let ev_block = convert_sealed_block(sealed_block); + ev_block.try_recover().map_err(|e| NewPayloadError::Other(e.into())) } Err(err) => { // Log the error for debugging. @@ -69,9 +69,8 @@ impl PayloadValidator for EvolveEngineValidator { // For evolve, we trust the payload builder - just parse the block without hash validation. let ExecutionData { payload, sidecar } = payload; let sealed_block = payload.try_into_block_with_sidecar(&sidecar)?.seal_slow(); - sealed_block - .try_recover() - .map_err(|e| NewPayloadError::Other(e.into())) + let ev_block = convert_sealed_block(sealed_block); + ev_block.try_recover().map_err(|e| NewPayloadError::Other(e.into())) } else { // For other errors, re-throw them. Err(NewPayloadError::Eth(err)) @@ -90,6 +89,14 @@ impl PayloadValidator for EvolveEngineValidator { } } +fn convert_sealed_block( + sealed_block: SealedBlock, +) -> SealedBlock { + let (block, hash) = sealed_block.split(); + let ev_block = block.map_transactions(EvTxEnvelope::Ethereum); + SealedBlock::new_unchecked(ev_block, hash) +} + impl EngineApiValidator for EvolveEngineValidator { fn validate_version_specific_fields( &self, @@ -135,7 +142,7 @@ where Types: NodeTypes< Payload = EvolveEngineTypes, ChainSpec = ChainSpec, - Primitives = reth_ethereum::EthPrimitives, + Primitives = ev_primitives::EvPrimitives, >, >, { diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index ea4a864..a58d8e9 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -16,6 +16,7 @@ ev-node = { path = "../node" } ev-common = { path = "../common" } ev-revm.workspace = true ev-precompiles = { path = "../ev-precompiles" } +ev-primitives = { path = "../ev-primitives" } # Reth dependencies reth-testing-utils.workspace = true diff --git a/crates/tests/src/common.rs b/crates/tests/src/common.rs index e671ec9..39d6d56 100644 --- a/crates/tests/src/common.rs +++ b/crates/tests/src/common.rs @@ -8,13 +8,13 @@ use std::sync::Arc; use alloy_consensus::{transaction::SignerRecoverable, TxLegacy, TypedTransaction}; use alloy_genesis::Genesis; use alloy_primitives::{Address, Bytes, ChainId, Signature, TxKind, B256, U256}; +use ev_primitives::{EvTxEnvelope, TransactionSigned}; use ev_revm::{ with_ev_handler, BaseFeeRedirect, BaseFeeRedirectSettings, ContractSizeLimitSettings, MintPrecompileSettings, }; use eyre::Result; use reth_chainspec::{ChainSpec, ChainSpecBuilder}; -use reth_ethereum_primitives::TransactionSigned; use reth_evm_ethereum::EthEvmConfig; use reth_primitives::{Header, Transaction}; use reth_provider::test_utils::{ExtendedAccount, MockEthProvider}; @@ -41,6 +41,11 @@ pub const TEST_GAS_LIMIT: u64 = 30_000_000; /// Base fee used in mock headers to satisfy post-London/EIP-4844 requirements pub const TEST_BASE_FEE: u64 = 0; +fn to_ev_envelope(transaction: Transaction, signature: Signature) -> TransactionSigned { + let signed = alloy_consensus::Signed::new_unhashed(transaction, signature); + EvTxEnvelope::Ethereum(reth_ethereum_primitives::TransactionSigned::from(signed)) +} + /// Creates a reusable chain specification for tests. pub fn create_test_chain_spec() -> Arc { create_test_chain_spec_with_extras(None, None) @@ -172,7 +177,7 @@ impl EvolveTestFixture { ); // Find which address the test signature resolves to - let test_signed = TransactionSigned::new_unhashed( + let test_signed = to_ev_envelope( Transaction::Legacy(TxLegacy { chain_id: Some(ChainId::from(TEST_CHAIN_ID)), nonce: 0, @@ -248,7 +253,7 @@ pub fn create_test_transactions(count: usize, nonce_start: u64) -> Vec