From 93cb8fddd252770f752410d28d3f6336ddc5c8d0 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 13 Jan 2026 17:15:24 +0100 Subject: [PATCH 01/19] create primitives --- Cargo.lock | 10 + Cargo.toml | 3 + crates/ev-primitives/Cargo.toml | 12 + crates/ev-primitives/src/lib.rs | 423 ++++++++++++++++++++++++++++++++ 4 files changed, 448 insertions(+) create mode 100644 crates/ev-primitives/Cargo.toml create mode 100644 crates/ev-primitives/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 318aaf1..f751cdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2952,6 +2952,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "ev-primitives" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", +] + [[package]] name = "ev-reth" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d3babc7..ff33572 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", @@ -71,6 +72,7 @@ reth-rpc-builder = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1. reth-rpc-engine-api = { 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 +110,7 @@ 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-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..2fe4919 --- /dev/null +++ b/crates/ev-primitives/Cargo.toml @@ -0,0 +1,12 @@ +[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 } +alloy-primitives = { workspace = true, features = ["k256", "rlp"] } +alloy-rlp = { workspace = true, features = ["derive"] } diff --git a/crates/ev-primitives/src/lib.rs b/crates/ev-primitives/src/lib.rs new file mode 100644 index 0000000..e1316fb --- /dev/null +++ b/crates/ev-primitives/src/lib.rs @@ -0,0 +1,423 @@ +//! EV-specific primitive types, including the EvNode 0x76 transaction. + +use alloy_consensus::{ + transaction::RlpEcdsaDecodableTx, transaction::RlpEcdsaEncodableTx, SignableTransaction, + Transaction, TransactionEnvelope, +}; +use alloy_eips::eip2930::AccessList; +use alloy_primitives::{keccak256, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_rlp::{BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable}; + +/// 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)] +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)] +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(alloy_consensus::TxEnvelope), + /// EvNode typed transaction. + #[envelope(ty = 0x76)] + EvNode(EvNodeSignedTx), +} + +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) + } +} + +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); + } +} From d525ad22cbea7926d3f489b9838b382e7952062c Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Wed, 14 Jan 2026 16:27:07 +0100 Subject: [PATCH 02/19] work until rpc --- Cargo.lock | 18 ++ Cargo.toml | 6 +- crates/ev-primitives/Cargo.toml | 11 +- crates/ev-primitives/src/lib.rs | 285 ++++++++++++++++- crates/ev-revm/Cargo.toml | 1 + crates/ev-revm/src/evm.rs | 92 +++++- crates/ev-revm/src/factory.rs | 158 +++++++++- crates/ev-revm/src/lib.rs | 4 +- crates/ev-revm/src/tx_env.rs | 175 ++++++++++ crates/evolve/Cargo.toml | 1 + crates/evolve/src/consensus.rs | 14 +- crates/evolve/src/types.rs | 2 +- crates/node/Cargo.toml | 12 + crates/node/src/attributes.rs | 2 +- crates/node/src/builder.rs | 31 +- crates/node/src/evm_executor.rs | 283 +++++++++++++++++ crates/node/src/executor.rs | 313 +++++++++++++++++- crates/node/src/lib.rs | 9 + crates/node/src/node.rs | 22 +- crates/node/src/payload_service.rs | 15 +- crates/node/src/payload_types.rs | 178 +++++++++++ crates/node/src/rpc.rs | 282 +++++++++++++++++ crates/node/src/txpool.rs | 491 +++++++++++++++++++++++++++++ crates/node/src/validator.rs | 25 +- crates/tests/Cargo.toml | 1 + crates/tests/src/common.rs | 11 +- 26 files changed, 2367 insertions(+), 75 deletions(-) create mode 100644 crates/ev-revm/src/tx_env.rs create mode 100644 crates/node/src/evm_executor.rs create mode 100644 crates/node/src/payload_types.rs create mode 100644 crates/node/src/rpc.rs create mode 100644 crates/node/src/txpool.rs diff --git a/Cargo.lock b/Cargo.lock index f751cdc..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", @@ -2960,6 +2971,10 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", + "reth-codecs", + "reth-ethereum-primitives", + "reth-primitives-traits", + "serde", ] [[package]] @@ -3013,6 +3028,7 @@ dependencies = [ "alloy-primitives", "alloy-sol-types", "ev-precompiles", + "ev-primitives", "reth-evm", "reth-evm-ethereum", "reth-primitives", @@ -3041,6 +3057,7 @@ dependencies = [ "ev-common", "ev-node", "ev-precompiles", + "ev-primitives", "ev-revm", "evolve-ev-reth", "eyre", @@ -3091,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 ff33572..52cf637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,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" } @@ -70,6 +70,9 @@ 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" } @@ -110,6 +113,7 @@ 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 } diff --git a/crates/ev-primitives/Cargo.toml b/crates/ev-primitives/Cargo.toml index 2fe4919..4e18985 100644 --- a/crates/ev-primitives/Cargo.toml +++ b/crates/ev-primitives/Cargo.toml @@ -7,6 +7,13 @@ license = "MIT OR Apache-2.0" [dependencies] alloy-consensus = { workspace = true } -alloy-eips = { workspace = true } -alloy-primitives = { workspace = true, features = ["k256", "rlp"] } +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 index e1316fb..985961d 100644 --- a/crates/ev-primitives/src/lib.rs +++ b/crates/ev-primitives/src/lib.rs @@ -1,12 +1,20 @@ //! EV-specific primitive types, including the EvNode 0x76 transaction. use alloy_consensus::{ - transaction::RlpEcdsaDecodableTx, transaction::RlpEcdsaEncodableTx, SignableTransaction, - Transaction, TransactionEnvelope, + 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::{BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable}; +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; @@ -14,7 +22,7 @@ pub const EVNODE_TX_TYPE_ID: u8 = 0x76; pub const EVNODE_SPONSOR_DOMAIN: u8 = 0x78; /// Single call entry in an EvNode transaction. -#[derive(Clone, Debug, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, RlpEncodable, RlpDecodable, serde::Serialize, serde::Deserialize)] pub struct Call { /// Destination (CALL or CREATE). pub to: TxKind, @@ -25,7 +33,7 @@ pub struct Call { } /// EvNode batch + sponsorship transaction payload. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct EvNodeTransaction { pub chain_id: u64, pub nonce: u64, @@ -47,12 +55,48 @@ pub type EvNodeSignedTx = alloy_consensus::Signed; pub enum EvTxEnvelope { /// Standard Ethereum typed transaction envelope. #[envelope(flatten)] - Ethereum(alloy_consensus::TxEnvelope), + 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 { @@ -281,6 +325,235 @@ impl Decodable for EvNodeTransaction { } } +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(), 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 Date: Mon, 19 Jan 2026 08:52:10 +0100 Subject: [PATCH 03/19] reorder paths --- crates/ev-primitives/src/lib.rs | 679 +------------------------------ crates/ev-primitives/src/pool.rs | 79 ++++ crates/ev-primitives/src/tx.rs | 553 +++++++++++++++++++++++++ crates/node/src/txpool.rs | 30 +- 4 files changed, 651 insertions(+), 690 deletions(-) create mode 100644 crates/ev-primitives/src/pool.rs create mode 100644 crates/ev-primitives/src/tx.rs diff --git a/crates/ev-primitives/src/lib.rs b/crates/ev-primitives/src/lib.rs index 985961d..3ee5d26 100644 --- a/crates/ev-primitives/src/lib.rs +++ b/crates/ev-primitives/src/lib.rs @@ -1,80 +1,15 @@ //! 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; +mod pool; +mod tx; -/// 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; +pub use pool::{EvPooledTxEnvelope, EvPooledTxType}; +pub use tx::{ + Call, EvNodeSignedTx, EvNodeTransaction, EvTxEnvelope, EvTxType, TransactionSigned, + EVNODE_SPONSOR_DOMAIN, EVNODE_TX_TYPE_ID, +}; -/// 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), -} +use reth_primitives_traits::NodePrimitives; /// Block type alias for ev-reth. pub type Block = alloy_consensus::Block; @@ -96,601 +31,3 @@ impl NodePrimitives for EvPrimitives { 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-primitives/src/pool.rs b/crates/ev-primitives/src/pool.rs new file mode 100644 index 0000000..b612136 --- /dev/null +++ b/crates/ev-primitives/src/pool.rs @@ -0,0 +1,79 @@ +//! Pooled transaction envelope for ev-reth. + +use alloy_consensus::{ + error::ValueError, + transaction::{SignerRecoverable, TxHashRef}, + TransactionEnvelope, +}; +use alloy_primitives::{Address, B256}; +use reth_primitives_traits::{InMemorySize, SignedTransaction}; + +use crate::tx::{EvNodeSignedTx, 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), +} + +impl InMemorySize for EvPooledTxEnvelope { + fn size(&self) -> usize { + match self { + EvPooledTxEnvelope::Ethereum(tx) => tx.size(), + EvPooledTxEnvelope::EvNode(tx) => tx.size(), + } + } +} + +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 SignedTransaction for EvPooledTxEnvelope {} diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs new file mode 100644 index 0000000..abe191e --- /dev/null +++ b/crates/ev-primitives/src/tx.rs @@ -0,0 +1,553 @@ +//! Transaction types for ev-reth. + +use alloy_consensus::{ + 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, 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_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; + +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); + 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, executor address bound). + pub fn sponsor_signing_hash(&self, executor: Address) -> B256 { + let payload = self.encoded_payload_with_executor(executor); + 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 executor address. + pub fn recover_sponsor( + &self, + executor: Address, + signature: &Signature, + ) -> Result { + signature.recover_address_from_prehash(&self.sponsor_signing_hash(executor)) + } + + fn first_call(&self) -> Option<&Call> { + self.calls.first() + } + + fn encoded_payload(&self, fee_payer_signature: Option<&Signature>) -> Vec { + let payload_len = self.payload_fields_length(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_signature); + out + } + + fn encoded_payload_with_executor(&self, executor: Address) -> Vec { + let mut out = Vec::with_capacity(self.payload_fields_length(self.fee_payer_signature.as_ref()) + 32); + out.extend_from_slice(executor.as_slice()); + self.encode_payload_fields(&mut out, self.fee_payer_signature.as_ref()); + out + } + + fn payload_fields_length(&self, 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_signature_length(fee_payer_signature) + } + + fn encode_payload_fields(&self, out: &mut dyn BufMut, 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_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 { + let max_fee = self.max_fee_per_gas; + let Some(base_fee) = base_fee else { + return max_fee; + }; + let base_fee = base_fee as u128; + if max_fee < base_fee { + return max_fee; + } + let priority_fee = self.max_priority_fee_per_gas; + let max_priority_fee = max_fee.saturating_sub(base_fee); + base_fee.saturating_add(priority_fee.min(max_priority_fee)) + } + + 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); + Header { list: true, payload_length: payload_len }.encode(out); + self.encode_payload_fields(out, None); + } + + fn payload_len_for_signature(&self) -> usize { + 1 + Header { list: true, payload_length: self.payload_fields_length(None) }.length_with_payload() + } +} + +impl RlpEcdsaEncodableTx for EvNodeTransaction { + fn rlp_encoded_fields_length(&self) -> usize { + self.payload_fields_length(self.fee_payer_signature.as_ref()) + } + + fn rlp_encode_fields(&self, out: &mut dyn BufMut) { + self.encode_payload_fields(out, 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_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 sponsor_sig_size = self + .fee_payer_signature + .map(|_| core::mem::size_of::()) + .unwrap_or(0); + core::mem::size_of::() + calls_size + access_list_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 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 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 reth_primitives_traits::serde_bincode_compat::RlpBincode for EvTxEnvelope {} + +fn optional_signature_length(value: Option<&Signature>) -> usize { + match value { + Some(sig) => sig.as_bytes().as_slice().length(), + None => 1, + } +} + +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_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_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_signature = Some(sample_signature()); + + assert_eq!(base_hash, tx.executor_signing_hash()); + } + + #[test] + fn sponsor_signing_hash_binds_executor() { + 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_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_signature, tx.fee_payer_signature); + } + + #[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_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/node/src/txpool.rs b/crates/node/src/txpool.rs index 121260d..f7ac2ec 100644 --- a/crates/node/src/txpool.rs +++ b/crates/node/src/txpool.rs @@ -258,8 +258,6 @@ pub enum EvTxPoolError { 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}")] @@ -320,7 +318,7 @@ impl EvTransactionValidator { fn validate_sponsor_balance( &self, state: &mut Option>, - fee_payer: Address, + sponsor: Address, gas_cost: U256, ) -> Result<(), InvalidPoolTransactionError> where @@ -329,7 +327,7 @@ impl EvTransactionValidator { self.ensure_state(state)?; let state = state.as_ref().expect("state provider is set"); let account = state - .basic_account(&fee_payer) + .basic_account(&sponsor) .map_err(|err| InvalidPoolTransactionError::other(EvTxPoolError::StateProvider(err.to_string())))? .unwrap_or_default(); if account.balance < gas_cost { @@ -358,23 +356,17 @@ impl EvTransactionValidator { 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)); - } + if let Some(signature) = tx.fee_payer_signature.as_ref() { + let executor = pooled.transaction().signer(); + let sponsor = tx + .recover_sponsor(executor, signature) + .map_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)), + let gas_cost = U256::from(tx.max_fee_per_gas).saturating_mul(U256::from(tx.gas_limit)); + self.validate_sponsor_balance(state, sponsor, gas_cost)?; } + + Ok(()) } } From 9e61d28337d8778d55d1f0ce4b95230ae8aecfd9 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Mon, 19 Jan 2026 15:36:50 +0100 Subject: [PATCH 04/19] finish impl phase 1 --- crates/ev-primitives/src/tx.rs | 4 +- crates/ev-revm/src/api/exec.rs | 35 +- crates/ev-revm/src/handler.rs | 616 ++++++++++++++++++++++++++++++++- crates/ev-revm/src/lib.rs | 1 + crates/ev-revm/src/tx_env.rs | 168 ++++++++- crates/node/src/builder.rs | 12 +- crates/node/src/rpc.rs | 227 +++++++++++- 7 files changed, 1022 insertions(+), 41 deletions(-) diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index abe191e..4d102db 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -197,7 +197,9 @@ impl Transaction for EvNodeTransaction { } fn value(&self) -> U256 { - self.first_call().map(|call| call.value).unwrap_or_default() + self.calls + .iter() + .fold(U256::ZERO, |acc, call| acc.saturating_add(call.value)) } fn input(&self) -> &Bytes { diff --git a/crates/ev-revm/src/api/exec.rs b/crates/ev-revm/src/api/exec.rs index 2de215c..7d9acac 100644 --- a/crates/ev-revm/src/api/exec.rs +++ b/crates/ev-revm/src/api/exec.rs @@ -1,7 +1,7 @@ //! Execution traits for [`EvEvm`], mirroring the Reth mainnet implementations //! while inserting the EV-specific handler that redirects the base fee. -use crate::{evm::EvEvm, handler::EvHandler}; +use crate::{evm::EvEvm, handler::EvHandler, tx_env::{BatchCallsTx, SponsorPayerTx}}; use alloy_primitives::{Address, Bytes}; use reth_revm::revm::{ context::{result::ExecResultAndState, ContextSetters}, @@ -26,7 +26,9 @@ pub type EvExecutionResult = ExecutionResult; impl ExecuteEvm for EvEvm where - CTX: ContextTr> + ContextSetters, + CTX: ContextTr, Tx: SponsorPayerTx + BatchCallsTx> + + ContextSetters, + ::Tx: Clone, ::Db: Database, ::Journal: JournalTr + JournalTr::Db>, @@ -69,7 +71,13 @@ where impl ExecuteCommitEvm for EvEvm where - CTX: ContextTr> + ContextSetters, + CTX: ContextTr< + Db: DatabaseCommit, + Journal: JournalTr, + Tx: SponsorPayerTx + BatchCallsTx, + > + + ContextSetters, + ::Tx: Clone, PRECOMP: PrecompileProvider, { fn commit(&mut self, state: Self::State) { @@ -79,7 +87,9 @@ where impl InspectEvm for EvEvm where - CTX: ContextTr + JournalExt> + ContextSetters, + CTX: ContextTr + JournalExt, Tx: SponsorPayerTx + BatchCallsTx> + + ContextSetters, + ::Tx: Clone, INSP: Inspector, PRECOMP: PrecompileProvider, { @@ -100,8 +110,13 @@ where impl InspectCommitEvm for EvEvm where - CTX: ContextTr + JournalExt, Db: DatabaseCommit> + CTX: ContextTr< + Journal: JournalTr + JournalExt, + Db: DatabaseCommit, + Tx: SponsorPayerTx + BatchCallsTx, + > + ContextSetters, + ::Tx: Clone, INSP: Inspector, PRECOMP: PrecompileProvider, { @@ -109,7 +124,9 @@ where impl SystemCallEvm for EvEvm where - CTX: ContextTr, Tx: SystemCallTx> + ContextSetters, + CTX: ContextTr, Tx: SystemCallTx + SponsorPayerTx + BatchCallsTx> + + ContextSetters, + ::Tx: Clone, PRECOMP: PrecompileProvider, { fn system_call_one_with_caller( @@ -134,8 +151,12 @@ where impl InspectSystemCallEvm for EvEvm where - CTX: ContextTr + JournalExt, Tx: SystemCallTx> + CTX: ContextTr< + Journal: JournalTr + JournalExt, + Tx: SystemCallTx + SponsorPayerTx + BatchCallsTx, + > + ContextSetters, + ::Tx: Clone, INSP: Inspector, PRECOMP: PrecompileProvider, { diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index af2a350..537643f 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -1,21 +1,34 @@ //! Execution handler extensions for EV-specific fee policies. -use crate::base_fee::{BaseFeeRedirect, BaseFeeRedirectError}; +use crate::{ + base_fee::{BaseFeeRedirect, BaseFeeRedirectError}, + tx_env::{BatchCallsTx, SponsorPayerTx}, +}; use reth_revm::{ inspector::{Inspector, InspectorEvmTr, InspectorHandler}, revm::{ context::result::ExecutionResult, - context_interface::{result::HaltReason, ContextTr, JournalTr}, + context::ContextSetters, + context_interface::{ + result::HaltReason, + transaction::{AccessListItemTr, TransactionType}, + Block, Cfg, ContextTr, JournalTr, Transaction, + }, handler::{ post_execution, EthFrame, EvmTr, EvmTrError, FrameResult, FrameTr, Handler, MainnetHandler, }, interpreter::{ - interpreter::EthInterpreter, interpreter_action::FrameInit, InitialAndFloorGas, + gas::{calculate_initial_tx_gas, ACCESS_LIST_ADDRESS, ACCESS_LIST_STORAGE_KEY}, + interpreter::EthInterpreter, + interpreter_action::FrameInit, + Gas, InitialAndFloorGas, }, - state::EvmState, + primitives::{eip7702, hardfork::SpecId}, + state::{AccountInfo, Bytecode, EvmState}, }, }; +use std::cmp::Ordering; /// Handler wrapper that mirrors the mainnet handler but applies optional EV-specific policies. #[derive(Debug, Clone)] @@ -41,7 +54,12 @@ impl EvHandler { impl Handler for EvHandler where - EVM: EvmTr>, Frame = FRAME>, + EVM: EvmTr< + Context: ContextTr, Tx: SponsorPayerTx + BatchCallsTx> + + ContextSetters, + Frame = FRAME, + >, + <::Context as ContextTr>::Tx: Clone, ERROR: EvmTrError, FRAME: FrameTr, { @@ -54,6 +72,26 @@ where } fn validate_initial_tx_gas(&self, evm: &Self::Evm) -> Result { + let ctx = evm.ctx_ref(); + let tx = ctx.tx(); + if let Some(calls) = tx.batch_calls() { + if calls.is_empty() { + return Err(Self::Error::from_string("evnode transaction must include at least one call".into())); + } + if calls.iter().skip(1).any(|call| call.to.is_create()) { + return Err(Self::Error::from_string("only the first call may be CREATE".into())); + } + if calls.len() > 1 { + return validate_batch_initial_tx_gas( + tx, + calls, + ctx.cfg().spec().into(), + false, + ) + .map_err(From::from); + } + } + self.inner.validate_initial_tx_gas(evm) } @@ -69,7 +107,91 @@ where &self, evm: &mut Self::Evm, ) -> Result<(), Self::Error> { - self.inner.validate_against_state_and_deduct_caller(evm) + let ctx = evm.ctx_mut(); + let tx = ctx.tx(); + let sponsor_invalid = tx.sponsor_signature_invalid(); + let sponsor = tx.sponsor(); + let caller_address = tx.caller(); + let total_value = tx.batch_total_value(); + let is_call = tx.kind().is_call(); + let basefee = ctx.block().basefee() as u128; + let blob_price = ctx.block().blob_gasprice().unwrap_or_default(); + let is_balance_check_disabled = ctx.cfg().is_balance_check_disabled(); + let is_eip3607_disabled = ctx.cfg().is_eip3607_disabled(); + let is_nonce_check_disabled = ctx.cfg().is_nonce_check_disabled(); + + if sponsor_invalid { + return Err(Self::Error::from_string("invalid sponsor signature".into())); + } + + let (tx, journal) = ctx.tx_journal_mut(); + if let Some(sponsor) = sponsor { + { + let caller = journal.load_account_code(caller_address)?.data; + validate_account_nonce_and_code( + &caller.info, + tx, + is_eip3607_disabled, + is_nonce_check_disabled, + )?; + + let mut new_caller_balance = caller.info.balance; + if !is_balance_check_disabled && new_caller_balance < total_value { + return Err(reth_revm::revm::context_interface::result::InvalidTransaction::LackOfFundForMaxFee { + fee: Box::new(total_value), + balance: Box::new(new_caller_balance), + } + .into()); + } + + new_caller_balance = new_caller_balance.saturating_sub(total_value); + if is_balance_check_disabled { + new_caller_balance = new_caller_balance.max(total_value); + } + + caller.info.set_balance(new_caller_balance); + if is_call { + caller.info.set_nonce(caller.info.nonce.saturating_add(1)); + } + } + + let effective_balance_spending = tx + .effective_balance_spending(basefee, blob_price) + .expect("effective balance is always smaller than max balance so it can't overflow"); + let gas_cost = effective_balance_spending - total_value; + + let sponsor_account = journal.load_account_code(sponsor)?.data; + let sponsor_balance = sponsor_account.info.balance; + if !is_balance_check_disabled && sponsor_balance < gas_cost { + return Err(reth_revm::revm::context_interface::result::InvalidTransaction::LackOfFundForMaxFee { + fee: Box::new(gas_cost), + balance: Box::new(sponsor_balance), + } + .into()); + } + + let mut new_sponsor_balance = sponsor_balance.saturating_sub(gas_cost); + if is_balance_check_disabled { + new_sponsor_balance = new_sponsor_balance.max(gas_cost); + } + sponsor_account.info.set_balance(new_sponsor_balance); + } else { + let caller = journal.load_account_code(caller_address)?.data; + validate_account_nonce_and_code( + &caller.info, + tx, + is_eip3607_disabled, + is_nonce_check_disabled, + )?; + let new_caller_balance = + calculate_caller_fee(caller.info.balance, tx, basefee, blob_price, is_balance_check_disabled)?; + caller.info.set_balance(new_caller_balance); + if is_call { + caller.info.set_nonce(caller.info.nonce.saturating_add(1)); + } + } + + Ok(()) } fn first_frame_input( @@ -80,6 +202,65 @@ where self.inner.first_frame_input(evm, gas_limit) } + fn execution( + &mut self, + evm: &mut Self::Evm, + init_and_floor_gas: &InitialAndFloorGas, + ) -> Result { + let calls = match evm.ctx().tx().batch_calls() { + Some(calls) if calls.is_empty() => { + return Err(Self::Error::from_string( + "evnode transaction must include at least one call".into(), + )); + } + Some(calls) if calls.len() > 1 => calls.to_vec(), + _ => return self.inner.execution(evm, init_and_floor_gas), + }; + + let base_tx = evm.ctx().tx().clone(); + let gas_limit = base_tx.gas_limit(); + let checkpoint = evm.ctx_mut().journal_mut().checkpoint(); + let mut remaining_gas = gas_limit.saturating_sub(init_and_floor_gas.initial_gas); + let mut total_refunded: i64 = 0; + let mut last_result: Option = None; + + for call in calls.iter() { + let mut call_tx = base_tx.clone(); + call_tx.set_batch_call(call); + evm.ctx_mut().set_tx(call_tx); + let first_frame_input = self.inner.first_frame_input(evm, remaining_gas)?; + let mut frame_result = self.inner.run_exec_loop(evm, first_frame_input)?; + let instruction_result = frame_result.interpreter_result().result; + total_refunded = total_refunded.saturating_add(frame_result.gas().refunded()); + remaining_gas = frame_result.gas().remaining(); + + if !instruction_result.is_ok() { + evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); + finalize_batch_gas( + &mut frame_result, + gas_limit, + remaining_gas, + 0, + ); + return Ok(frame_result); + } + + last_result = Some(frame_result); + } + + evm.ctx_mut().journal_mut().checkpoint_commit(); + + let mut frame_result = last_result.expect("batch execution requires at least one call"); + finalize_batch_gas( + &mut frame_result, + gas_limit, + remaining_gas, + total_refunded, + ); + + Ok(frame_result) + } + fn last_frame_result( &mut self, evm: &mut Self::Evm, @@ -152,29 +333,201 @@ where impl InspectorHandler for EvHandler> where EVM: InspectorEvmTr< - Context: ContextTr>, + Context: ContextTr, Tx: SponsorPayerTx + BatchCallsTx>, Frame = EthFrame, Inspector: Inspector<::Context, EthInterpreter>, >, + ::Context: ContextSetters, + <::Context as ContextTr>::Tx: Clone, ERROR: EvmTrError, { type IT = EthInterpreter; } +fn validate_account_nonce_and_code( + caller_info: &AccountInfo, + tx: &Tx, + is_eip3607_disabled: bool, + is_nonce_check_disabled: bool, +) -> Result<(), reth_revm::revm::context_interface::result::InvalidTransaction> +where + Tx: Transaction, +{ + if !is_eip3607_disabled { + let bytecode = match caller_info.code.as_ref() { + Some(code) => code, + None => &Bytecode::default(), + }; + if !bytecode.is_empty() && !bytecode.is_eip7702() { + return Err( + reth_revm::revm::context_interface::result::InvalidTransaction::RejectCallerWithCode, + ); + } + } + + if !is_nonce_check_disabled { + let tx_nonce = tx.nonce(); + let state_nonce = caller_info.nonce; + match tx_nonce.cmp(&state_nonce) { + Ordering::Greater => { + return Err( + reth_revm::revm::context_interface::result::InvalidTransaction::NonceTooHigh { + tx: tx_nonce, + state: state_nonce, + }, + ); + } + Ordering::Less => { + return Err( + reth_revm::revm::context_interface::result::InvalidTransaction::NonceTooLow { + tx: tx_nonce, + state: state_nonce, + }, + ); + } + Ordering::Equal => {} + } + } + + Ok(()) +} + +fn calculate_caller_fee( + balance: reth_revm::revm::primitives::U256, + tx: &Tx, + basefee: u128, + blob_price: u128, + is_balance_check_disabled: bool, +) -> Result +where + Tx: Transaction, +{ + let effective_balance_spending = tx + .effective_balance_spending(basefee, blob_price) + .expect("effective balance is always smaller than max balance so it can't overflow"); + if !is_balance_check_disabled && balance < effective_balance_spending { + return Err( + reth_revm::revm::context_interface::result::InvalidTransaction::LackOfFundForMaxFee { + fee: Box::new(effective_balance_spending), + balance: Box::new(balance), + }, + ); + } + + let gas_balance_spending = effective_balance_spending - tx.value(); + + let mut new_balance = balance.saturating_sub(gas_balance_spending); + + if is_balance_check_disabled { + new_balance = new_balance.max(tx.value()); + } + + Ok(new_balance) +} + +fn validate_batch_initial_tx_gas( + tx: &Tx, + calls: &[ev_primitives::Call], + spec: SpecId, + is_eip7623_disabled: bool, +) -> Result { + let mut initial_gas = 0u64; + let mut floor_gas = 0u64; + + for call in calls { + let call_gas = calculate_initial_tx_gas( + spec, + call.input.as_ref(), + call.to.is_create(), + 0, + 0, + 0, + ); + initial_gas = initial_gas.saturating_add(call_gas.initial_gas); + floor_gas = floor_gas.saturating_add(call_gas.floor_gas); + } + + let mut accounts = 0u64; + let mut storages = 0u64; + if tx.tx_type() != TransactionType::Legacy { + if let Some(access_list) = tx.access_list() { + (accounts, storages) = access_list.fold((0u64, 0u64), |(mut acc, mut stor), item| { + acc = acc.saturating_add(1); + stor = stor.saturating_add(item.storage_slots().count() as u64); + (acc, stor) + }); + } + } + + initial_gas = initial_gas + .saturating_add(accounts.saturating_mul(ACCESS_LIST_ADDRESS)) + .saturating_add(storages.saturating_mul(ACCESS_LIST_STORAGE_KEY)); + + if spec.is_enabled_in(SpecId::PRAGUE) { + initial_gas = initial_gas.saturating_add( + (tx.authorization_list_len() as u64).saturating_mul(eip7702::PER_EMPTY_ACCOUNT_COST), + ); + } else { + floor_gas = 0; + } + + if is_eip7623_disabled { + floor_gas = 0; + } + + if initial_gas > tx.gas_limit() { + return Err( + reth_revm::revm::context_interface::result::InvalidTransaction::CallGasCostMoreThanGasLimit { + gas_limit: tx.gas_limit(), + initial_gas, + }, + ); + } + + if spec.is_enabled_in(SpecId::PRAGUE) && floor_gas > tx.gas_limit() { + return Err( + reth_revm::revm::context_interface::result::InvalidTransaction::GasFloorMoreThanGasLimit { + gas_floor: floor_gas, + gas_limit: tx.gas_limit(), + }, + ); + } + + Ok(InitialAndFloorGas::new(initial_gas, floor_gas)) +} + +fn finalize_batch_gas(frame_result: &mut FrameResult, tx_gas_limit: u64, remaining_gas: u64, refund: i64) { + let instruction_result = frame_result.interpreter_result().result; + let mut gas = Gas::new_spent(tx_gas_limit); + if instruction_result.is_ok_or_revert() { + gas.erase_cost(remaining_gas); + } + if instruction_result.is_ok() { + gas.record_refund(refund); + } + *frame_result.gas_mut() = gas; +} + #[cfg(test)] mod tests { use super::*; - use crate::EvEvm; - use alloy_primitives::{address, Address, Bytes, U256}; + use crate::{EvEvm, EvTxEnv, EvTxEvmFactory}; + use alloy_primitives::{address, Address, Bytes, TxKind, B256, U256}; + use ev_primitives::Call; use reth_revm::{ inspector::NoOpInspector, revm::{ context::Context, - database::EmptyDB, + context_interface::transaction::{AccessList, AccessListItem, TransactionType}, + context_interface::result::ExecutionResult, + database::{CacheDB, EmptyDB}, handler::{EthFrame, FrameResult}, interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult}, primitives::hardfork::SpecId, + primitives::KECCAK_EMPTY, + state::{AccountInfo, EvmState}, }, + State, MainContext, }; use std::convert::Infallible; @@ -187,9 +540,13 @@ mod tests { type TestHandler = EvHandler>; use reth_revm::revm::context::{BlockEnv, CfgEnv, TxEnv}; + use reth_revm::revm::bytecode::Bytecode as RevmBytecode; + use alloy_evm::{Evm, EvmEnv, EvmFactory}; const BASE_FEE: u64 = 100; const GAS_PRICE: u128 = 200; + const STORAGE_RUNTIME: [u8; 6] = [0x60, 0x01, 0x60, 0x00, 0x55, 0x00]; + const REVERT_RUNTIME: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xfd]; #[test] fn reward_beneficiary_redirects_base_fee_sink() { @@ -239,6 +596,245 @@ mod tests { assert!(beneficiary_balance.is_zero()); } + #[test] + fn batch_initial_gas_sums_calls_and_access_list() { + let mut tx_env = TxEnv::default(); + tx_env.gas_limit = 1_000_000; + tx_env.tx_type = TransactionType::Eip1559.into(); + tx_env.access_list = AccessList(vec![AccessListItem { + address: address!("0x00000000000000000000000000000000000000aa"), + storage_keys: vec![B256::ZERO, B256::from([0x11; 32])], + }]); + + let calls = vec![ + Call { + to: TxKind::Call(address!("0x00000000000000000000000000000000000000bb")), + value: U256::ZERO, + input: Bytes::new(), + }, + Call { + to: TxKind::Call(address!("0x00000000000000000000000000000000000000cc")), + value: U256::ZERO, + input: Bytes::from(vec![0x01, 0x00, 0x02]), + }, + ]; + + let gas_call_1 = calculate_initial_tx_gas(SpecId::PRAGUE, calls[0].input.as_ref(), false, 0, 0, 0); + let gas_call_2 = calculate_initial_tx_gas(SpecId::PRAGUE, calls[1].input.as_ref(), false, 0, 0, 0); + let access_list_cost = + ACCESS_LIST_ADDRESS + 2 * ACCESS_LIST_STORAGE_KEY; + + let result = validate_batch_initial_tx_gas(&tx_env, &calls, SpecId::PRAGUE, false) + .expect("batch gas should validate"); + + let expected_initial = gas_call_1 + .initial_gas + .saturating_add(gas_call_2.initial_gas) + .saturating_add(access_list_cost); + let expected_floor = gas_call_1 + .floor_gas + .saturating_add(gas_call_2.floor_gas); + + assert_eq!(result.initial_gas, expected_initial); + assert_eq!(result.floor_gas, expected_floor); + } + + #[test] + fn batch_initial_gas_rejects_when_gas_limit_too_low() { + let mut tx_env = TxEnv::default(); + tx_env.gas_limit = 10_000; + + let calls = vec![Call { + to: TxKind::Call(address!("0x00000000000000000000000000000000000000dd")), + value: U256::ZERO, + input: Bytes::from(vec![0x11; 64]), + }]; + + let err = validate_batch_initial_tx_gas(&tx_env, &calls, SpecId::CANCUN, false) + .expect_err("should reject when gas limit is too low"); + + assert!(matches!( + err, + InvalidTransaction::CallGasCostMoreThanGasLimit { .. } + )); + } + + #[test] + fn batch_execution_reverts_state_on_failure() { + let caller = address!("0x0000000000000000000000000000000000000aaa"); + let storage_contract = address!("0x0000000000000000000000000000000000000bbb"); + let revert_contract = address!("0x0000000000000000000000000000000000000ccc"); + + let mut state = State::builder() + .with_database(CacheDB::::default()) + .with_bundle_update() + .build(); + + state.insert_account( + caller, + AccountInfo { + balance: U256::from(10_000_000_000u64), + nonce: 0, + code_hash: KECCAK_EMPTY, + code: None, + }, + ); + + state.insert_account( + storage_contract, + AccountInfo { + balance: U256::ZERO, + nonce: 1, + code_hash: alloy_primitives::keccak256(STORAGE_RUNTIME.as_slice()), + code: Some(RevmBytecode::new_raw(Bytes::copy_from_slice( + STORAGE_RUNTIME.as_slice(), + ))), + }, + ); + + state.insert_account( + revert_contract, + AccountInfo { + balance: U256::ZERO, + nonce: 1, + code_hash: alloy_primitives::keccak256(REVERT_RUNTIME.as_slice()), + code: Some(RevmBytecode::new_raw(Bytes::copy_from_slice( + REVERT_RUNTIME.as_slice(), + ))), + }, + ); + + let mut evm_env: EvmEnv = EvmEnv::default(); + evm_env.cfg_env.chain_id = 1; + evm_env.cfg_env.spec = SpecId::CANCUN; + evm_env.block_env.basefee = 1; + evm_env.block_env.gas_limit = 30_000_000; + evm_env.block_env.number = U256::from(1); + + let mut evm = EvTxEvmFactory::default().create_evm(state, evm_env); + + let calls = vec![ + Call { + to: TxKind::Call(storage_contract), + value: U256::ZERO, + input: Bytes::new(), + }, + Call { + to: TxKind::Call(revert_contract), + value: U256::ZERO, + input: Bytes::new(), + }, + ]; + + let mut tx_env = TxEnv::default(); + tx_env.caller = caller; + tx_env.gas_limit = 200_000; + tx_env.gas_price = 1; + tx_env.gas_priority_fee = Some(1); + tx_env.chain_id = Some(1); + tx_env.tx_type = TransactionType::Eip1559.into(); + + let tx = EvTxEnv::with_calls(tx_env, calls); + + let result_and_state = evm + .transact_raw(tx) + .expect("batch execution should complete"); + + assert!(matches!(result_and_state.result, ExecutionResult::Revert { .. })); + + let state: EvmState = result_and_state.state; + let storage_account = state + .get(&storage_contract) + .expect("storage contract should be loaded"); + if let Some(slot) = storage_account.storage.get(&U256::ZERO) { + assert!(slot.original_value.is_zero()); + assert!(slot.present_value.is_zero()); + assert!(!slot.is_changed()); + } + } + + #[test] + fn batch_execution_commits_state_on_success() { + let caller = address!("0x0000000000000000000000000000000000000aaa"); + let storage_contract = address!("0x0000000000000000000000000000000000000bbb"); + + let mut state = State::builder() + .with_database(CacheDB::::default()) + .with_bundle_update() + .build(); + + state.insert_account( + caller, + AccountInfo { + balance: U256::from(10_000_000_000u64), + nonce: 0, + code_hash: KECCAK_EMPTY, + code: None, + }, + ); + + state.insert_account( + storage_contract, + AccountInfo { + balance: U256::ZERO, + nonce: 1, + code_hash: alloy_primitives::keccak256(STORAGE_RUNTIME.as_slice()), + code: Some(RevmBytecode::new_raw(Bytes::copy_from_slice( + STORAGE_RUNTIME.as_slice(), + ))), + }, + ); + + let mut evm_env: EvmEnv = EvmEnv::default(); + evm_env.cfg_env.chain_id = 1; + evm_env.cfg_env.spec = SpecId::CANCUN; + evm_env.block_env.basefee = 1; + evm_env.block_env.gas_limit = 30_000_000; + evm_env.block_env.number = U256::from(1); + + let mut evm = EvTxEvmFactory::default().create_evm(state, evm_env); + + let calls = vec![ + Call { + to: TxKind::Call(storage_contract), + value: U256::ZERO, + input: Bytes::new(), + }, + Call { + to: TxKind::Call(storage_contract), + value: U256::ZERO, + input: Bytes::new(), + }, + ]; + + let mut tx_env = TxEnv::default(); + tx_env.caller = caller; + tx_env.gas_limit = 200_000; + tx_env.gas_price = 1; + tx_env.gas_priority_fee = Some(1); + tx_env.chain_id = Some(1); + tx_env.tx_type = TransactionType::Eip1559.into(); + + let tx = EvTxEnv::with_calls(tx_env, calls); + + let result_and_state = evm + .transact_raw(tx) + .expect("batch execution should complete"); + + assert!(matches!(result_and_state.result, ExecutionResult::Success { .. })); + + let state: EvmState = result_and_state.state; + let storage_account = state + .get(&storage_contract) + .expect("storage contract should be loaded"); + let slot = storage_account + .storage + .get(&U256::ZERO) + .expect("slot 0 should be written"); + assert_eq!(slot.present_value, U256::from(1)); + assert!(slot.is_changed()); + } + fn setup_evm(redirect: BaseFeeRedirect, beneficiary: Address) -> (TestEvm, TestHandler) { let mut ctx = Context::mainnet().with_db(EmptyDB::default()); ctx.block.basefee = BASE_FEE; diff --git a/crates/ev-revm/src/lib.rs b/crates/ev-revm/src/lib.rs index 43830d1..22dfc8c 100644 --- a/crates/ev-revm/src/lib.rs +++ b/crates/ev-revm/src/lib.rs @@ -6,6 +6,7 @@ pub mod config; pub mod evm; pub mod factory; pub mod handler; +/// EV-specific transaction environment extensions. pub mod tx_env; pub use api::EvBuilder; diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs index 9e52031..58271e5 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -1,13 +1,13 @@ use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; -use alloy_primitives::{Address, Bytes}; -use ev_primitives::EvTxEnvelope; +use alloy_primitives::{Address, Bytes, U256}; +use ev_primitives::{Call, 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::primitives::{Address as RevmAddress, Bytes as RevmBytes, TxKind, B256}; use reth_revm::revm::context_interface::either::Either; use reth_evm::TransactionEnv; @@ -15,25 +15,91 @@ use reth_evm::TransactionEnv; #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct EvTxEnv { inner: TxEnv, + sponsor: Option
, + sponsor_signature_invalid: bool, + calls: Vec, + batch_value: U256, } impl EvTxEnv { - pub const fn new(inner: TxEnv) -> Self { - Self { inner } + /// Wrap a `TxEnv` with EV-specific metadata. + pub fn new(inner: TxEnv) -> Self { + let batch_value = inner.value; + Self { + inner, + sponsor: None, + sponsor_signature_invalid: false, + calls: Vec::new(), + batch_value, + } } + /// Returns the underlying `TxEnv`. pub const fn inner(&self) -> &TxEnv { &self.inner } + /// Returns the underlying `TxEnv` mutably. pub fn inner_mut(&mut self) -> &mut TxEnv { &mut self.inner } + + /// Returns the recovered sponsor address, if any. + pub const fn sponsor(&self) -> Option
{ + self.sponsor + } + + /// Returns whether the sponsor signature was invalid. + pub const fn sponsor_signature_invalid(&self) -> bool { + self.sponsor_signature_invalid + } + + /// Returns the batch calls for this transaction. + pub fn calls(&self) -> &[Call] { + &self.calls + } + + /// Returns the total value across all calls. + pub const fn batch_value(&self) -> U256 { + self.batch_value + } + + /// Updates the inner `TxEnv` to represent a single call from the batch. + pub fn set_call(&mut self, call: &Call) { + self.inner.kind = call.to; + self.inner.value = call.value; + self.inner.data = call.input.clone(); + } +} + +#[cfg(test)] +impl EvTxEnv { + /// Test helper to build an `EvTxEnv` with batch calls pre-populated. + pub fn with_calls(mut inner: TxEnv, calls: Vec) -> Self { + let batch_value = calls + .iter() + .fold(U256::ZERO, |acc, call| acc.saturating_add(call.value)); + if let Some(first) = calls.first() { + inner.kind = first.to; + inner.data = first.input.clone(); + } + inner.value = batch_value; + let mut env = EvTxEnv::new(inner); + env.calls = calls; + env.batch_value = batch_value; + env + } } impl From for EvTxEnv { fn from(inner: TxEnv) -> Self { - Self { inner } + Self { + batch_value: inner.value, + inner, + sponsor: None, + sponsor_signature_invalid: false, + calls: Vec::new(), + } } } @@ -137,14 +203,32 @@ impl FromRecoveredTx for EvTxEnv { match tx { EvTxEnvelope::Ethereum(inner) => EvTxEnv::new(TxEnv::from_recovered_tx(inner, sender)), EvTxEnvelope::EvNode(ev) => { + let (sponsor, sponsor_signature_invalid) = + if let Some(signature) = ev.tx().fee_payer_signature.as_ref() { + match ev.tx().recover_sponsor(sender, signature) { + Ok(sponsor) => (Some(sponsor), false), + Err(_) => (None, true), + } + } else { + (None, false) + }; + let calls = ev.tx().calls.clone(); + let batch_value = calls + .iter() + .fold(U256::ZERO, |acc, call| acc.saturating_add(call.value)); 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.value = batch_value; env.data = ev.tx().calls.first().map(|call| call.input.clone()).unwrap_or_default(); - EvTxEnv::new(env) + let mut tx_env = EvTxEnv::new(env); + tx_env.sponsor = sponsor; + tx_env.sponsor_signature_invalid = sponsor_signature_invalid; + tx_env.calls = calls; + tx_env.batch_value = batch_value; + tx_env } } } @@ -173,3 +257,71 @@ impl SystemCallTx for EvTxEnv { ) } } + +/// Exposes the optional sponsor payer for EV-specific transactions. +pub trait SponsorPayerTx { + /// Returns the sponsor address, if any. + fn sponsor(&self) -> Option
; + /// Returns whether the sponsor signature was invalid. + fn sponsor_signature_invalid(&self) -> bool; +} + +/// Batch-call helpers for EV transactions. +pub trait BatchCallsTx { + /// Returns the batch calls, if present. + fn batch_calls(&self) -> Option<&[Call]>; + /// Returns the total value across all calls. + fn batch_total_value(&self) -> U256; + /// Sets the inner `TxEnv` to the given call. + fn set_batch_call(&mut self, call: &Call); +} + +impl SponsorPayerTx for EvTxEnv { + fn sponsor(&self) -> Option
{ + self.sponsor + } + + fn sponsor_signature_invalid(&self) -> bool { + self.sponsor_signature_invalid + } +} + +impl BatchCallsTx for EvTxEnv { + fn batch_calls(&self) -> Option<&[Call]> { + Some(&self.calls) + } + + fn batch_total_value(&self) -> U256 { + self.batch_value + } + + fn set_batch_call(&mut self, call: &Call) { + self.set_call(call); + } +} + +impl SponsorPayerTx for TxEnv { + fn sponsor(&self) -> Option
{ + None + } + + fn sponsor_signature_invalid(&self) -> bool { + false + } +} + +impl BatchCallsTx for TxEnv { + fn batch_calls(&self) -> Option<&[Call]> { + None + } + + fn batch_total_value(&self) -> U256 { + self.value + } + + fn set_batch_call(&mut self, call: &Call) { + self.kind = call.to; + self.value = call.value; + self.data = call.input.clone(); + } +} diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 03a6c0c..87e097e 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -158,7 +158,17 @@ where )) })?; - if matches!(recovered_tx.inner(), ev_primitives::EvTxEnvelope::EvNode(_)) { + if let ev_primitives::EvTxEnvelope::EvNode(ev) = recovered_tx.inner() { + if let Some(signature) = ev.tx().fee_payer_signature.as_ref() { + ev.tx() + .recover_sponsor(recovered_tx.signer(), signature) + .map_err(|_| { + PayloadBuilderError::Internal(RethError::Other( + "Invalid sponsor signature".into(), + )) + })?; + } + return Err(PayloadBuilderError::Internal(RethError::Other( "EvNode transaction execution not supported yet".into(), ))); diff --git a/crates/node/src/rpc.rs b/crates/node/src/rpc.rs index 102b8e5..2d7f5ed 100644 --- a/crates/node/src/rpc.rs +++ b/crates/node/src/rpc.rs @@ -4,9 +4,10 @@ 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_network::{Ethereum, ReceiptResponse, TransactionResponse, TxSigner}; +use alloy_primitives::{Address, Signature, U256}; use alloy_rpc_types_eth::{Log, Transaction, TransactionInfo, TransactionRequest, TransactionReceipt}; +use alloy_consensus::Transaction as ConsensusTransaction; use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks, Hardforks}; use reth_evm::{ConfigureEvm, SpecFor, TxEnvFor}; use reth_node_api::{FullNodeComponents, FullNodeTypes, NodeTypes}; @@ -38,11 +39,197 @@ pub struct EvRpcTypes; impl RpcTypes for EvRpcTypes { type Header = ::Header; - type Receipt = TransactionReceipt>; - type TransactionResponse = ::TransactionResponse; + type Receipt = EvRpcReceipt; + type TransactionResponse = EvRpcTransaction; type TransactionRequest = EvTransactionRequest; } +/// RPC transaction response with optional sponsor address. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EvRpcTransaction { + #[serde(flatten)] + inner: Transaction, + #[serde(rename = "feePayer", skip_serializing_if = "Option::is_none")] + fee_payer: Option
, +} + +impl EvRpcTransaction { + fn new(inner: Transaction, fee_payer: Option
) -> Self { + Self { inner, fee_payer } + } +} + +impl ConsensusTransaction for EvRpcTransaction { + fn chain_id(&self) -> Option { + ConsensusTransaction::chain_id(&self.inner) + } + + fn nonce(&self) -> u64 { + ConsensusTransaction::nonce(&self.inner) + } + + fn gas_limit(&self) -> u64 { + ConsensusTransaction::gas_limit(&self.inner) + } + + fn gas_price(&self) -> Option { + ConsensusTransaction::gas_price(&self.inner) + } + + fn max_fee_per_gas(&self) -> u128 { + ConsensusTransaction::max_fee_per_gas(&self.inner) + } + + fn max_priority_fee_per_gas(&self) -> Option { + ConsensusTransaction::max_priority_fee_per_gas(&self.inner) + } + + fn max_fee_per_blob_gas(&self) -> Option { + ConsensusTransaction::max_fee_per_blob_gas(&self.inner) + } + + fn priority_fee_or_price(&self) -> u128 { + ConsensusTransaction::priority_fee_or_price(&self.inner) + } + + fn effective_gas_price(&self, base_fee: Option) -> u128 { + ConsensusTransaction::effective_gas_price(&self.inner, base_fee) + } + + fn is_dynamic_fee(&self) -> bool { + ConsensusTransaction::is_dynamic_fee(&self.inner) + } + + fn kind(&self) -> alloy_primitives::TxKind { + ConsensusTransaction::kind(&self.inner) + } + + fn is_create(&self) -> bool { + ConsensusTransaction::is_create(&self.inner) + } + + fn value(&self) -> U256 { + ConsensusTransaction::value(&self.inner) + } + + fn input(&self) -> &alloy_primitives::Bytes { + ConsensusTransaction::input(&self.inner) + } + + fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> { + ConsensusTransaction::access_list(&self.inner) + } + + fn blob_versioned_hashes(&self) -> Option<&[alloy_primitives::B256]> { + ConsensusTransaction::blob_versioned_hashes(&self.inner) + } + + fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> { + ConsensusTransaction::authorization_list(&self.inner) + } +} + +impl TransactionResponse for EvRpcTransaction { + fn tx_hash(&self) -> alloy_primitives::TxHash { + self.inner.tx_hash() + } + + fn block_hash(&self) -> Option { + self.inner.block_hash() + } + + fn block_number(&self) -> Option { + self.inner.block_number() + } + + fn transaction_index(&self) -> Option { + self.inner.transaction_index() + } + + fn from(&self) -> Address { + self.inner.from() + } +} + +impl alloy_eips::Typed2718 for EvRpcTransaction { + fn ty(&self) -> u8 { + self.inner.ty() + } +} + +/// RPC receipt response with optional sponsor address. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EvRpcReceipt { + #[serde(flatten)] + inner: TransactionReceipt>, + #[serde(rename = "feePayer", skip_serializing_if = "Option::is_none")] + fee_payer: Option
, +} + +impl EvRpcReceipt { + fn new(inner: TransactionReceipt>, fee_payer: Option
) -> Self { + Self { inner, fee_payer } + } +} + +impl ReceiptResponse for EvRpcReceipt { + fn contract_address(&self) -> Option
{ + self.inner.contract_address() + } + + fn status(&self) -> bool { + self.inner.status() + } + + fn block_hash(&self) -> Option { + self.inner.block_hash() + } + + fn block_number(&self) -> Option { + self.inner.block_number() + } + + fn transaction_hash(&self) -> alloy_primitives::TxHash { + self.inner.transaction_hash() + } + + fn transaction_index(&self) -> Option { + self.inner.transaction_index() + } + + fn gas_used(&self) -> u64 { + self.inner.gas_used() + } + + fn effective_gas_price(&self) -> u128 { + self.inner.effective_gas_price() + } + + fn blob_gas_used(&self) -> Option { + self.inner.blob_gas_used() + } + + fn blob_gas_price(&self) -> Option { + self.inner.blob_gas_price() + } + + fn from(&self) -> Address { + self.inner.from() + } + + fn to(&self) -> Option
{ + self.inner.to() + } + + fn cumulative_gas_used(&self) -> u64 { + self.inner.cumulative_gas_used() + } + + fn state_root(&self) -> Option { + self.inner.state_root() + } +} + /// Transaction request wrapper to satisfy local trait bounds. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(transparent)] @@ -119,7 +306,7 @@ impl ReceiptConverter for EvReceiptConverter where ChainSpec: EthChainSpec + 'static, { - type RpcReceipt = TransactionReceipt>; + type RpcReceipt = EvRpcReceipt; type Error = EthApiError; fn convert_receipts( @@ -130,12 +317,21 @@ where 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 fee_payer = match input.tx.inner() { + EvTxEnvelope::EvNode(ev) => ev + .tx() + .fee_payer_signature + .as_ref() + .and_then(|sig| ev.tx().recover_sponsor(input.tx.signer(), sig).ok()), + EvTxEnvelope::Ethereum(_) => None, + }; + let receipt = 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 } - })); + }); + receipts.push(EvRpcReceipt::new(receipt, fee_payer)); } Ok(receipts) @@ -223,13 +419,16 @@ impl RpcTxConverter, TransactionInfo> f 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), - } + let fee_payer = match &tx { + EvTxEnvelope::EvNode(ev) => ev + .tx() + .fee_payer_signature + .as_ref() + .and_then(|sig| ev.tx().recover_sponsor(signer, sig).ok()), + EvTxEnvelope::Ethereum(_) => None, + }; + let recovered = Recovered::new_unchecked(tx, signer); + Ok(EvRpcTransaction::new(Transaction::from_transaction(recovered, tx_info), fee_payer)) } } From 738d8f9edc28fdb628784118da40ad06744cf293 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Mon, 19 Jan 2026 16:27:53 +0100 Subject: [PATCH 05/19] fix lint --- Cargo.lock | 2 + Cargo.toml | 4 + crates/ev-primitives/Cargo.toml | 2 + crates/ev-primitives/src/tx.rs | 94 +++++++++++++++++---- crates/ev-revm/src/api/exec.rs | 27 +++--- crates/ev-revm/src/factory.rs | 24 ++++-- crates/ev-revm/src/handler.rs | 107 +++++++++++------------ crates/ev-revm/src/lib.rs | 4 +- crates/ev-revm/src/tx_env.rs | 28 ++++-- crates/evolve/src/consensus.rs | 2 +- crates/node/src/attributes.rs | 6 +- crates/node/src/builder.rs | 4 +- crates/node/src/evm_executor.rs | 84 ++++++++++++------ crates/node/src/executor.rs | 131 ++++++++++++++++++++--------- crates/node/src/lib.rs | 10 +-- crates/node/src/node.rs | 2 +- crates/node/src/payload_service.rs | 2 +- crates/node/src/payload_types.rs | 35 +++++++- crates/node/src/rpc.rs | 73 ++++++++++------ crates/node/src/txpool.rs | 98 ++++++++++++++------- crates/node/src/validator.rs | 10 ++- crates/tests/src/common.rs | 16 ++-- 22 files changed, 519 insertions(+), 246 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2199890..4c0a428 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2971,7 +2971,9 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", + "bytes", "reth-codecs", + "reth-db-api", "reth-ethereum-primitives", "reth-primitives-traits", "serde", diff --git a/Cargo.toml b/Cargo.toml index 52cf637..c9598ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth.git", tag = reth-network = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-network-types = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-chain-state = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } +reth-db-api = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-ethereum = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-ethereum-cli = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.8.4" } @@ -119,6 +120,9 @@ 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 } +# Utility dependencies +bytes = "1.10.1" + revm-inspector = { version = "10.0.1" } # Core dependencies eyre = "0.6" diff --git a/crates/ev-primitives/Cargo.toml b/crates/ev-primitives/Cargo.toml index 4e18985..2d525f2 100644 --- a/crates/ev-primitives/Cargo.toml +++ b/crates/ev-primitives/Cargo.toml @@ -10,7 +10,9 @@ alloy-consensus = { workspace = true } alloy-eips = { workspace = true, features = ["serde"] } alloy-primitives = { workspace = true, features = ["k256", "rlp", "serde"] } alloy-rlp = { workspace = true, features = ["derive"] } +bytes = { workspace = true } reth-codecs = { workspace = true } +reth-db-api = { workspace = true } reth-ethereum-primitives = { workspace = true } reth-primitives-traits = { workspace = true, features = ["serde-bincode-compat"] } serde = { workspace = true, features = ["derive"] } diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index 4d102db..5d76b87 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -12,6 +12,10 @@ use reth_codecs::{ txtype::COMPACT_EXTENDED_IDENTIFIER_FLAG, Compact, }; +use reth_db_api::{ + table::{Compress, Decompress}, + DatabaseError, +}; use reth_primitives_traits::{InMemorySize, SignedTransaction}; use std::vec::Vec; @@ -21,7 +25,17 @@ pub const EVNODE_TX_TYPE_ID: u8 = 0x76; 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)] +#[derive( + Clone, + Debug, + PartialEq, + Eq, + Hash, + RlpEncodable, + RlpDecodable, + serde::Serialize, + serde::Deserialize, +)] pub struct Call { /// Destination (CALL or CREATE). pub to: TxKind, @@ -82,7 +96,10 @@ impl EvNodeTransaction { } /// Recovers the executor address from the provided signature. - pub fn recover_executor(&self, signature: &Signature) -> Result { + pub fn recover_executor( + &self, + signature: &Signature, + ) -> Result { signature.recover_address_from_prehash(&self.executor_signing_hash()) } @@ -101,14 +118,25 @@ impl EvNodeTransaction { fn encoded_payload(&self, fee_payer_signature: Option<&Signature>) -> Vec { let payload_len = self.payload_fields_length(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); + 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_signature); out } fn encoded_payload_with_executor(&self, executor: Address) -> Vec { - let mut out = Vec::with_capacity(self.payload_fields_length(self.fee_payer_signature.as_ref()) + 32); + let mut out = + Vec::with_capacity(self.payload_fields_length(self.fee_payer_signature.as_ref()) + 32); out.extend_from_slice(executor.as_slice()); self.encode_payload_fields(&mut out, self.fee_payer_signature.as_ref()); out @@ -139,7 +167,7 @@ impl EvNodeTransaction { impl Transaction for EvNodeTransaction { fn chain_id(&self) -> Option { - Some(self.chain_id.into()) + Some(self.chain_id) } fn nonce(&self) -> u64 { @@ -189,7 +217,9 @@ impl Transaction for EvNodeTransaction { } fn kind(&self) -> TxKind { - self.first_call().map(|call| call.to).unwrap_or(TxKind::Create) + self.first_call() + .map(|call| call.to) + .unwrap_or(TxKind::Create) } fn is_create(&self) -> bool { @@ -228,18 +258,26 @@ impl alloy_eips::Typed2718 for EvNodeTransaction { impl SignableTransaction for EvNodeTransaction { fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) { - self.chain_id = chain_id.into(); + self.chain_id = chain_id; } fn encode_for_signing(&self, out: &mut dyn BufMut) { out.put_u8(EVNODE_TX_TYPE_ID); let payload_len = self.payload_fields_length(None); - Header { list: true, payload_length: payload_len }.encode(out); + Header { + list: true, + payload_length: payload_len, + } + .encode(out); self.encode_payload_fields(out, None); } fn payload_len_for_signature(&self) -> usize { - 1 + Header { list: true, payload_length: self.payload_fields_length(None) }.length_with_payload() + 1 + Header { + list: true, + payload_length: self.payload_fields_length(None), + } + .length_with_payload() } } @@ -272,7 +310,11 @@ impl RlpEcdsaDecodableTx for EvNodeTransaction { impl Encodable for EvNodeTransaction { fn length(&self) -> usize { - Header { list: true, payload_length: self.rlp_encoded_fields_length() }.length_with_payload() + Header { + list: true, + payload_length: self.rlp_encoded_fields_length(), + } + .length_with_payload() } fn encode(&self, out: &mut dyn BufMut) { @@ -418,8 +460,9 @@ impl FromTxCompact for EvTxEnvelope { { match tx_type { EvTxType::Ethereum(inner) => { - let (tx, buf) = - reth_ethereum_primitives::TransactionSigned::from_tx_compact(buf, inner, signature); + let (tx, buf) = reth_ethereum_primitives::TransactionSigned::from_tx_compact( + buf, inner, signature, + ); (Self::Ethereum(tx), buf) } EvTxType::EvNode => { @@ -459,6 +502,21 @@ impl SignedTransaction for EvTxEnvelope {} impl reth_primitives_traits::serde_bincode_compat::RlpBincode for EvTxEnvelope {} +impl Compress for EvTxEnvelope { + type Compressed = Vec; + + fn compress_to_buf>(&self, buf: &mut B) { + let _ = Compact::to_compact(self, buf); + } +} + +impl Decompress for EvTxEnvelope { + fn decompress(value: &[u8]) -> Result { + let (obj, _) = Compact::from_compact(value, value.len()); + Ok(obj) + } +} + fn optional_signature_length(value: Option<&Signature>) -> usize { match value { Some(sig) => sig.as_bytes().as_slice().length(), @@ -478,7 +536,9 @@ fn decode_optional_signature(buf: &mut &[u8]) -> alloy_rlp::Result, Tx: SponsorPayerTx + BatchCallsTx, - > - + ContextSetters, + > + ContextSetters, ::Tx: Clone, PRECOMP: PrecompileProvider, { @@ -87,8 +90,10 @@ where impl InspectEvm for EvEvm where - CTX: ContextTr + JournalExt, Tx: SponsorPayerTx + BatchCallsTx> - + ContextSetters, + CTX: ContextTr< + Journal: JournalTr + JournalExt, + Tx: SponsorPayerTx + BatchCallsTx, + > + ContextSetters, ::Tx: Clone, INSP: Inspector, PRECOMP: PrecompileProvider, @@ -114,8 +119,7 @@ where Journal: JournalTr + JournalExt, Db: DatabaseCommit, Tx: SponsorPayerTx + BatchCallsTx, - > - + ContextSetters, + > + ContextSetters, ::Tx: Clone, INSP: Inspector, PRECOMP: PrecompileProvider, @@ -124,8 +128,10 @@ where impl SystemCallEvm for EvEvm where - CTX: ContextTr, Tx: SystemCallTx + SponsorPayerTx + BatchCallsTx> - + ContextSetters, + CTX: ContextTr< + Journal: JournalTr, + Tx: SystemCallTx + SponsorPayerTx + BatchCallsTx, + > + ContextSetters, ::Tx: Clone, PRECOMP: PrecompileProvider, { @@ -154,8 +160,7 @@ where CTX: ContextTr< Journal: JournalTr + JournalExt, Tx: SystemCallTx + SponsorPayerTx + BatchCallsTx, - > - + ContextSetters, + > + ContextSetters, ::Tx: Clone, INSP: Inspector, PRECOMP: PrecompileProvider, diff --git a/crates/ev-revm/src/factory.rs b/crates/ev-revm/src/factory.rs index ce95816..654e20e 100644 --- a/crates/ev-revm/src/factory.rs +++ b/crates/ev-revm/src/factory.rs @@ -9,6 +9,7 @@ use alloy_evm::{ use alloy_primitives::{Address, U256}; use ev_precompiles::mint::{MintPrecompile, MINT_PRECOMPILE_ADDR}; use reth_evm_ethereum::EthEvmConfig; +use reth_revm::revm::context_interface::journaled_state::JournalTr; use reth_revm::{ inspector::NoOpInspector, revm::{ @@ -20,11 +21,10 @@ use reth_revm::{ handler::instructions::EthInstructions, interpreter::interpreter::EthInterpreter, precompile::{PrecompileSpecId, Precompiles}, - Context, Inspector, primitives::hardfork::SpecId, + Context, 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. @@ -227,7 +227,12 @@ pub struct EvTxEvmFactory { contract_size_limit: Option, } -type EvEvmContext = Context, DB>; +type EvEvmContext = Context< + reth_revm::revm::context::BlockEnv, + EvTxEnv, + reth_revm::revm::context::CfgEnv, + DB, +>; type EvRevmEvm = RevmEvm< EvEvmContext, I, @@ -237,12 +242,17 @@ type EvRevmEvm = RevmEvm< >; impl EvTxEvmFactory { + /// Creates a new EV EVM factory with optional redirect and precompile settings. pub const fn new( redirect: Option, mint_precompile: Option, contract_size_limit: Option, ) -> Self { - Self { redirect, mint_precompile, contract_size_limit } + Self { + redirect, + mint_precompile, + contract_size_limit, + } } fn contract_size_limit_for_block(&self, block_number: U256) -> Option { @@ -291,9 +301,9 @@ impl EvTxEvmFactory { env: EvmEnv, inspector: I, ) -> EvRevmEvm { - let precompiles = PrecompilesMap::from_static(Precompiles::new(PrecompileSpecId::from_spec_id( - env.cfg_env.spec, - ))); + 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); diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index 537643f..0eff216 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -55,8 +55,10 @@ impl EvHandler { impl Handler for EvHandler where EVM: EvmTr< - Context: ContextTr, Tx: SponsorPayerTx + BatchCallsTx> - + ContextSetters, + Context: ContextTr< + Journal: JournalTr, + Tx: SponsorPayerTx + BatchCallsTx, + > + ContextSetters, Frame = FRAME, >, <::Context as ContextTr>::Tx: Clone, @@ -76,19 +78,18 @@ where let tx = ctx.tx(); if let Some(calls) = tx.batch_calls() { if calls.is_empty() { - return Err(Self::Error::from_string("evnode transaction must include at least one call".into())); + return Err(Self::Error::from_string( + "evnode transaction must include at least one call".into(), + )); } if calls.iter().skip(1).any(|call| call.to.is_create()) { - return Err(Self::Error::from_string("only the first call may be CREATE".into())); + return Err(Self::Error::from_string( + "only the first call may be CREATE".into(), + )); } if calls.len() > 1 { - return validate_batch_initial_tx_gas( - tx, - calls, - ctx.cfg().spec().into(), - false, - ) - .map_err(From::from); + return validate_batch_initial_tx_gas(tx, calls, ctx.cfg().spec().into(), false) + .map_err(From::from); } } @@ -155,9 +156,10 @@ where } } - let effective_balance_spending = tx - .effective_balance_spending(basefee, blob_price) - .expect("effective balance is always smaller than max balance so it can't overflow"); + let effective_balance_spending = + tx.effective_balance_spending(basefee, blob_price).expect( + "effective balance is always smaller than max balance so it can't overflow", + ); let gas_cost = effective_balance_spending - total_value; let sponsor_account = journal.load_account_code(sponsor)?.data; @@ -183,8 +185,13 @@ where is_eip3607_disabled, is_nonce_check_disabled, )?; - let new_caller_balance = - calculate_caller_fee(caller.info.balance, tx, basefee, blob_price, is_balance_check_disabled)?; + let new_caller_balance = calculate_caller_fee( + caller.info.balance, + tx, + basefee, + blob_price, + is_balance_check_disabled, + )?; caller.info.set_balance(new_caller_balance); if is_call { caller.info.set_nonce(caller.info.nonce.saturating_add(1)); @@ -236,12 +243,7 @@ where if !instruction_result.is_ok() { evm.ctx_mut().journal_mut().checkpoint_revert(checkpoint); - finalize_batch_gas( - &mut frame_result, - gas_limit, - remaining_gas, - 0, - ); + finalize_batch_gas(&mut frame_result, gas_limit, remaining_gas, 0); return Ok(frame_result); } @@ -251,12 +253,7 @@ where evm.ctx_mut().journal_mut().checkpoint_commit(); let mut frame_result = last_result.expect("batch execution requires at least one call"); - finalize_batch_gas( - &mut frame_result, - gas_limit, - remaining_gas, - total_refunded, - ); + finalize_batch_gas(&mut frame_result, gas_limit, remaining_gas, total_refunded); Ok(frame_result) } @@ -398,7 +395,10 @@ fn calculate_caller_fee( basefee: u128, blob_price: u128, is_balance_check_disabled: bool, -) -> Result +) -> Result< + reth_revm::revm::primitives::U256, + reth_revm::revm::context_interface::result::InvalidTransaction, +> where Tx: Transaction, { @@ -435,14 +435,8 @@ fn validate_batch_initial_tx_gas( let mut floor_gas = 0u64; for call in calls { - let call_gas = calculate_initial_tx_gas( - spec, - call.input.as_ref(), - call.to.is_create(), - 0, - 0, - 0, - ); + let call_gas = + calculate_initial_tx_gas(spec, call.input.as_ref(), call.to.is_create(), 0, 0, 0); initial_gas = initial_gas.saturating_add(call_gas.initial_gas); floor_gas = floor_gas.saturating_add(call_gas.floor_gas); } @@ -496,7 +490,12 @@ fn validate_batch_initial_tx_gas( Ok(InitialAndFloorGas::new(initial_gas, floor_gas)) } -fn finalize_batch_gas(frame_result: &mut FrameResult, tx_gas_limit: u64, remaining_gas: u64, refund: i64) { +fn finalize_batch_gas( + frame_result: &mut FrameResult, + tx_gas_limit: u64, + remaining_gas: u64, + refund: i64, +) { let instruction_result = frame_result.interpreter_result().result; let mut gas = Gas::new_spent(tx_gas_limit); if instruction_result.is_ok_or_revert() { @@ -518,8 +517,8 @@ mod tests { inspector::NoOpInspector, revm::{ context::Context, - context_interface::transaction::{AccessList, AccessListItem, TransactionType}, context_interface::result::ExecutionResult, + context_interface::transaction::{AccessList, AccessListItem, TransactionType}, database::{CacheDB, EmptyDB}, handler::{EthFrame, FrameResult}, interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult}, @@ -527,8 +526,7 @@ mod tests { primitives::KECCAK_EMPTY, state::{AccountInfo, EvmState}, }, - State, - MainContext, + MainContext, State, }; use std::convert::Infallible; @@ -539,9 +537,9 @@ mod tests { type TestError = EVMError; type TestHandler = EvHandler>; - use reth_revm::revm::context::{BlockEnv, CfgEnv, TxEnv}; - use reth_revm::revm::bytecode::Bytecode as RevmBytecode; use alloy_evm::{Evm, EvmEnv, EvmFactory}; + use reth_revm::revm::bytecode::Bytecode as RevmBytecode; + use reth_revm::revm::context::{BlockEnv, CfgEnv, TxEnv}; const BASE_FEE: u64 = 100; const GAS_PRICE: u128 = 200; @@ -619,10 +617,11 @@ mod tests { }, ]; - let gas_call_1 = calculate_initial_tx_gas(SpecId::PRAGUE, calls[0].input.as_ref(), false, 0, 0, 0); - let gas_call_2 = calculate_initial_tx_gas(SpecId::PRAGUE, calls[1].input.as_ref(), false, 0, 0, 0); - let access_list_cost = - ACCESS_LIST_ADDRESS + 2 * ACCESS_LIST_STORAGE_KEY; + let gas_call_1 = + calculate_initial_tx_gas(SpecId::PRAGUE, calls[0].input.as_ref(), false, 0, 0, 0); + let gas_call_2 = + calculate_initial_tx_gas(SpecId::PRAGUE, calls[1].input.as_ref(), false, 0, 0, 0); + let access_list_cost = ACCESS_LIST_ADDRESS + 2 * ACCESS_LIST_STORAGE_KEY; let result = validate_batch_initial_tx_gas(&tx_env, &calls, SpecId::PRAGUE, false) .expect("batch gas should validate"); @@ -631,9 +630,7 @@ mod tests { .initial_gas .saturating_add(gas_call_2.initial_gas) .saturating_add(access_list_cost); - let expected_floor = gas_call_1 - .floor_gas - .saturating_add(gas_call_2.floor_gas); + let expected_floor = gas_call_1.floor_gas.saturating_add(gas_call_2.floor_gas); assert_eq!(result.initial_gas, expected_initial); assert_eq!(result.floor_gas, expected_floor); @@ -740,7 +737,10 @@ mod tests { .transact_raw(tx) .expect("batch execution should complete"); - assert!(matches!(result_and_state.result, ExecutionResult::Revert { .. })); + assert!(matches!( + result_and_state.result, + ExecutionResult::Revert { .. } + )); let state: EvmState = result_and_state.state; let storage_account = state @@ -821,7 +821,10 @@ mod tests { .transact_raw(tx) .expect("batch execution should complete"); - assert!(matches!(result_and_state.result, ExecutionResult::Success { .. })); + assert!(matches!( + result_and_state.result, + ExecutionResult::Success { .. } + )); let state: EvmState = result_and_state.state; let storage_account = state diff --git a/crates/ev-revm/src/lib.rs b/crates/ev-revm/src/lib.rs index 22dfc8c..628c4c1 100644 --- a/crates/ev-revm/src/lib.rs +++ b/crates/ev-revm/src/lib.rs @@ -14,8 +14,8 @@ 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, EvTxEvmFactory, - MintPrecompileSettings, + 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 index 58271e5..919bac7 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -1,15 +1,15 @@ use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; use alloy_primitives::{Address, Bytes, U256}; use ev_primitives::{Call, EvTxEnvelope}; +use reth_evm::TransactionEnv; use reth_revm::revm::context::TxEnv; +use reth_revm::revm::context_interface::either::Either; 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}; -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)] @@ -110,8 +110,14 @@ impl From for TxEnv { } impl RevmTransaction for EvTxEnv { - type AccessListItem<'a> = &'a AccessListItem where Self: 'a; - type Authorization<'a> = &'a Either where Self: 'a; + 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 @@ -220,9 +226,19 @@ impl FromRecoveredTx for EvTxEnv { 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.kind = ev + .tx() + .calls + .first() + .map(|call| call.to) + .unwrap_or(TxKind::Create); env.value = batch_value; - env.data = ev.tx().calls.first().map(|call| call.input.clone()).unwrap_or_default(); + env.data = ev + .tx() + .calls + .first() + .map(|call| call.input.clone()) + .unwrap_or_default(); let mut tx_env = EvTxEnv::new(env); tx_env.sponsor = sponsor; tx_env.sponsor_signature_invalid = sponsor_signature_invalid; diff --git a/crates/evolve/src/consensus.rs b/crates/evolve/src/consensus.rs index 4fe433f..8c107bd 100644 --- a/crates/evolve/src/consensus.rs +++ b/crates/evolve/src/consensus.rs @@ -1,5 +1,6 @@ //! Evolve custom consensus implementation that allows same timestamps across blocks. +use ev_primitives::{Block, BlockBody, EvPrimitives, Receipt}; use reth_chainspec::ChainSpec; use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator}; use reth_consensus_common::validation::{ @@ -8,7 +9,6 @@ use reth_consensus_common::validation::{ }; use reth_ethereum::node::builder::{components::ConsensusBuilder, BuilderContext}; use reth_ethereum_consensus::EthBeaconConsensus; -use ev_primitives::{Block, BlockBody, EvPrimitives, Receipt}; use reth_execution_types::BlockExecutionResult; use reth_node_api::{FullNodeTypes, NodeTypes}; use reth_primitives_traits::{RecoveredBlock, SealedBlock, SealedHeader}; diff --git a/crates/node/src/attributes.rs b/crates/node/src/attributes.rs index b44b2a6..06efcde 100644 --- a/crates/node/src/attributes.rs +++ b/crates/node/src/attributes.rs @@ -6,15 +6,13 @@ use alloy_rpc_types::{ }; use reth_chainspec::EthereumHardforks; use reth_engine_local::payload::LocalPayloadAttributesBuilder; -use reth_ethereum::{ - node::api::payload::{PayloadAttributes, PayloadBuilderAttributes}, -}; +use reth_ethereum::node::api::payload::{PayloadAttributes, PayloadBuilderAttributes}; use reth_payload_builder::EthPayloadBuilderAttributes; use reth_payload_primitives::PayloadAttributesBuilder; use serde::{Deserialize, Serialize}; -use ev_primitives::TransactionSigned; use crate::error::EvolveEngineError; +use ev_primitives::TransactionSigned; /// Evolve payload attributes that support passing transactions via Engine API. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 87e097e..8f1fbfa 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -1,5 +1,7 @@ use crate::config::EvolvePayloadBuilderConfig; +use crate::executor::EvEvmConfig; use alloy_consensus::transaction::Transaction; +use alloy_consensus::transaction::TxHashRef; use alloy_primitives::Address; use ev_revm::EvTxEvmFactory; use evolve_ev_reth::EvolvePayloadAttributes; @@ -9,10 +11,8 @@ use reth_evm::{ execute::{BlockBuilder, BlockBuilderOutcome}, ConfigureEvm, NextBlockEnvAttributes, }; -use crate::executor::EvEvmConfig; use reth_payload_builder_primitives::PayloadBuilderError; 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}; diff --git a/crates/node/src/evm_executor.rs b/crates/node/src/evm_executor.rs index 4480ce1..008e44c 100644 --- a/crates/node/src/evm_executor.rs +++ b/crates/node/src/evm_executor.rs @@ -6,8 +6,8 @@ use alloy_evm::{ block::{ state_changes::{balance_increment_state, post_block_balance_increments}, BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory, - BlockExecutorFor, BlockValidationError, ExecutableTx, OnStateHook, StateChangePostBlockSource, - StateChangeSource, SystemCaller, + BlockExecutorFor, BlockValidationError, ExecutableTx, OnStateHook, + StateChangePostBlockSource, StateChangeSource, SystemCaller, }, eth::{ dao_fork, eip6110, @@ -21,7 +21,9 @@ 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}; +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)] @@ -36,7 +38,12 @@ impl ReceiptBuilder for EvReceiptBuilder { &self, ctx: ReceiptBuilderCtx<'_, Self::Transaction, E>, ) -> Self::Receipt { - let ReceiptBuilderCtx { tx, result, cumulative_gas_used, .. } = ctx; + let ReceiptBuilderCtx { + tx, + result, + cumulative_gas_used, + .. + } = ctx; Receipt { tx_type: tx.tx_type(), success: result.is_success(), @@ -50,6 +57,7 @@ impl ReceiptBuilder for EvReceiptBuilder { #[derive(Debug)] pub struct EvBlockExecutor<'a, Evm, Spec, R: ReceiptBuilder> { spec: Spec, + /// Block execution context (parent hash, withdrawals, ommers, etc.). pub ctx: EthBlockExecutionCtx<'a>, evm: Evm, system_caller: SystemCaller, @@ -63,6 +71,7 @@ where Spec: Clone, R: ReceiptBuilder, { + /// Creates a new block executor with the provided EVM, context, spec, and receipt builder. pub fn new(evm: Evm, ctx: EthBlockExecutionCtx<'a>, spec: Spec, receipt_builder: R) -> Self { Self { evm, @@ -91,11 +100,13 @@ where 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()); + 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_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)?; @@ -109,11 +120,13 @@ where 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()); + return Err( + BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { + transaction_gas_limit: tx.tx().gas_limit(), + block_available_gas, + } + .into(), + ); } self.evm.transact(&tx).map_err(|err| { @@ -129,18 +142,20 @@ where ) -> Result { let ResultAndState { result, state } = output; - self.system_caller.on_state(StateChangeSource::Transaction(self.receipts.len()), &state); + 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.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); @@ -163,7 +178,10 @@ where requests.push_request_with_type(eip6110::DEPOSIT_REQUEST_TYPE, deposit_requests); } - requests.extend(self.system_caller.apply_post_execution_changes(&mut self.evm)?); + requests.extend( + self.system_caller + .apply_post_execution_changes(&mut self.evm)?, + ); requests } else { Requests::default() @@ -189,8 +207,9 @@ where .into_iter() .sum(); - *balance_increments.entry(dao_fork::DAO_HARDFORK_BENEFICIARY).or_default() += - drained_balance; + *balance_increments + .entry(dao_fork::DAO_HARDFORK_BENEFICIARY) + .or_default() += drained_balance; } self.evm @@ -209,7 +228,11 @@ where Ok(( self.evm, - BlockExecutionResult { receipts: self.receipts, requests, gas_used: self.gas_used }, + BlockExecutionResult { + receipts: self.receipts, + requests, + gas_used: self.gas_used, + }, )) } @@ -228,25 +251,34 @@ where /// Block executor factory for EV transactions. #[derive(Debug, Clone, Default, Copy)] -pub struct EvBlockExecutorFactory { +pub struct EvBlockExecutorFactory +{ receipt_builder: R, spec: Spec, evm_factory: EvmFactory, } impl EvBlockExecutorFactory { + /// Creates a new EV block executor factory. pub const fn new(receipt_builder: R, spec: Spec, evm_factory: EvmFactory) -> Self { - Self { receipt_builder, spec, evm_factory } + Self { + receipt_builder, + spec, + evm_factory, + } } + /// Returns the receipt builder used by the factory. pub const fn receipt_builder(&self) -> &R { &self.receipt_builder } + /// Returns the spec configuration used by the factory. pub const fn spec(&self) -> &Spec { &self.spec } + /// Returns the underlying EVM factory. pub const fn evm_factory(&self) -> &EvmFactory { &self.evm_factory } diff --git a/crates/node/src/executor.rs b/crates/node/src/executor.rs index 8046257..8bddbfb 100644 --- a/crates/node/src/executor.rs +++ b/crates/node/src/executor.rs @@ -11,6 +11,7 @@ use ev_revm::{ MintPrecompileSettings, }; use reth_chainspec::{ChainSpec, EthChainSpec}; +use reth_errors::RethError; use reth_ethereum::{ chainspec::EthereumHardforks, node::{ @@ -32,7 +33,6 @@ use reth_revm::revm::{ context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId, }; -use reth_errors::RethError; use tracing::info; use crate::evm_executor::{EvBlockExecutorFactory, EvReceiptBuilder}; @@ -46,31 +46,41 @@ pub type EvolveEvmConfig = EvEvmConfig; /// EVM config wired for EvPrimitives. #[derive(Debug, Clone)] pub struct EvEvmConfig { + /// Factory used to create block executors. pub executor_factory: EvBlockExecutorFactory, EvmFactory>, + /// Block assembler used for building block bodies and headers. pub block_assembler: EthBlockAssembler, } impl EvEvmConfig { + /// Creates a new EV EVM config with the default EVM factory. pub fn new(chain_spec: std::sync::Arc) -> Self { Self::new_with_evm_factory(chain_spec, EvTxEvmFactory::default()) } } impl EvEvmConfig { + /// Creates a new EV EVM config using the provided EVM factory. 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), + executor_factory: EvBlockExecutorFactory::new( + EvReceiptBuilder, + chain_spec, + evm_factory, + ), } } + /// Returns the chain spec used by this config. pub const fn chain_spec(&self) -> &std::sync::Arc { self.executor_factory.spec() } + /// Sets the extra data to be included in built blocks. pub fn with_extra_data(mut self, extra_data: alloy_primitives::Bytes) -> Self { self.block_assembler.extra_data = extra_data; self @@ -111,22 +121,32 @@ where 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); + 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) { + 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 } - }); + 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), @@ -163,22 +183,32 @@ where parent.number() + 1, ); - let mut cfg = - CfgEnv::new().with_chain_id(self.chain_spec().chain().id()).with_spec(spec_id); + 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) { + 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 } - }); + 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; @@ -207,7 +237,10 @@ where blob_excess_gas_and_price, }; - Ok(EvmEnv { cfg_env: cfg, block_env }) + Ok(EvmEnv { + cfg_env: cfg, + block_env, + }) } fn context_for_block<'a>( @@ -218,7 +251,11 @@ where 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), + withdrawals: block + .body() + .withdrawals + .as_ref() + .map(std::borrow::Cow::Borrowed), }) } @@ -240,9 +277,7 @@ impl ConfigureEngineEvm for EvEvmConfig + Hardforks + 'static, EvmF: reth_evm::EvmFactory< - Tx: TransactionEnv - + FromRecoveredTx - + FromTxWithEncoded, + Tx: TransactionEnv + FromRecoveredTx + FromTxWithEncoded, Spec = SpecId, Precompiles = reth_evm::precompiles::PrecompilesMap, > + Clone @@ -260,8 +295,9 @@ where 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); + 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); @@ -272,10 +308,17 @@ where } 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 } - }); + 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), @@ -300,17 +343,25 @@ where 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())), + 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)) - }) + 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)) + }) } } @@ -354,14 +405,12 @@ where ContractSizeLimitSettings::new(limit, activation) }); - let factory = EvTxEvmFactory::new( - redirect, - mint_precompile, - contract_size_limit, - ); + 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())) + 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 383854b..10ed10c 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -17,16 +17,16 @@ pub mod chainspec; pub mod config; /// Shared error types for evolve node wiring. pub mod error; -/// Executor wiring for EV aware execution. -pub mod executor; /// EV-specific EVM executor building blocks. pub mod evm_executor; +/// Executor wiring for EV aware execution. +pub mod executor; /// Node composition and payload types. pub mod node; -/// Payload types for EvPrimitives. -pub mod payload_types; /// Payload service integration. pub mod payload_service; +/// Payload types for EvPrimitives. +pub mod payload_types; /// RPC wiring for EvTxEnvelope support. pub mod rpc; /// Transaction pool wiring and validation. @@ -43,6 +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 payload_types::EvBuiltPayload; pub use validator::{EvolveEngineValidator, EvolveEngineValidatorBuilder}; diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index 3919c9b..4a9e814 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -4,6 +4,7 @@ use alloy_rpc_types::engine::{ ExecutionData, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadEnvelopeV4, ExecutionPayloadEnvelopeV5, ExecutionPayloadV1, }; +use ev_primitives::EvPrimitives; use reth_ethereum::{ chainspec::ChainSpec, node::{ @@ -16,7 +17,6 @@ use reth_ethereum::{ node::EthereumNetworkBuilder, }, }; -use ev_primitives::EvPrimitives; use reth_primitives_traits::SealedBlock; use serde::{Deserialize, Serialize}; use tracing::info; diff --git a/crates/node/src/payload_service.rs b/crates/node/src/payload_service.rs index 12fffd4..a0107c0 100644 --- a/crates/node/src/payload_service.rs +++ b/crates/node/src/payload_service.rs @@ -28,8 +28,8 @@ use crate::{ payload_types::EvBuiltPayload, }; -use evolve_ev_reth::config::set_current_block_gas_limit; use ev_primitives::{EvPrimitives, TransactionSigned}; +use evolve_ev_reth::config::set_current_block_gas_limit; /// Evolve payload service builder that integrates with the evolve payload builder. #[derive(Debug, Clone)] diff --git a/crates/node/src/payload_types.rs b/crates/node/src/payload_types.rs index 715acd4..715398b 100644 --- a/crates/node/src/payload_types.rs +++ b/crates/node/src/payload_types.rs @@ -22,47 +22,68 @@ pub struct EvBuiltPayload { requests: Option, } +/// Errors encountered when converting an EV payload into an engine API envelope. #[derive(Debug, thiserror::Error)] pub enum EvBuiltPayloadConversionError { + /// EIP-7594 sidecars are not valid for this envelope version. #[error("unexpected EIP-7594 sidecars for this payload")] UnexpectedEip7594Sidecars, + /// EIP-4844 sidecars are not valid for this envelope version. #[error("unexpected EIP-4844 sidecars for this payload")] UnexpectedEip4844Sidecars, } impl EvBuiltPayload { + /// Creates a new EV built payload. pub const fn new( id: PayloadId, block: Arc>, fees: U256, requests: Option, ) -> Self { - Self { id, block, fees, requests, sidecars: BlobSidecars::Empty } + Self { + id, + block, + fees, + requests, + sidecars: BlobSidecars::Empty, + } } + /// Returns the payload identifier. pub const fn id(&self) -> PayloadId { self.id } + /// Returns the sealed block backing this payload. pub fn block(&self) -> &SealedBlock { &self.block } + /// Returns the total fees for this payload. pub const fn fees(&self) -> U256 { self.fees } + /// Returns the sidecar bundle. pub const fn sidecars(&self) -> &BlobSidecars { &self.sidecars } + /// Attaches the provided sidecars and returns the updated payload. pub fn with_sidecars(mut self, sidecars: impl Into) -> Self { self.sidecars = sidecars.into(); self } + /// Converts this payload into an ExecutionPayloadEnvelopeV3. pub fn try_into_v3(self) -> Result { - let Self { block, fees, sidecars, .. } = self; + let Self { + block, + fees, + sidecars, + .. + } = self; let blobs_bundle = match sidecars { BlobSidecars::Empty => BlobsBundleV1::empty(), @@ -83,6 +104,7 @@ impl EvBuiltPayload { }) } + /// Converts this payload into an ExecutionPayloadEnvelopeV4. pub fn try_into_v4(self) -> Result { Ok(ExecutionPayloadEnvelopeV4 { execution_requests: self.requests.clone().unwrap_or_default(), @@ -90,8 +112,15 @@ impl EvBuiltPayload { }) } + /// Converts this payload into an ExecutionPayloadEnvelopeV5. pub fn try_into_v5(self) -> Result { - let Self { block, fees, sidecars, requests, .. } = self; + let Self { + block, + fees, + sidecars, + requests, + .. + } = self; let blobs_bundle = match sidecars { BlobSidecars::Empty => BlobsBundleV2::empty(), diff --git a/crates/node/src/rpc.rs b/crates/node/src/rpc.rs index 2d7f5ed..9bfdb61 100644 --- a/crates/node/src/rpc.rs +++ b/crates/node/src/rpc.rs @@ -3,11 +3,13 @@ use alloy_consensus::error::ValueError; use alloy_consensus::transaction::Recovered; use alloy_consensus::SignableTransaction; +use alloy_consensus::Transaction as ConsensusTransaction; use alloy_consensus_any::AnyReceiptEnvelope; use alloy_network::{Ethereum, ReceiptResponse, TransactionResponse, TxSigner}; use alloy_primitives::{Address, Signature, U256}; -use alloy_rpc_types_eth::{Log, Transaction, TransactionInfo, TransactionRequest, TransactionReceipt}; -use alloy_consensus::Transaction as ConsensusTransaction; +use alloy_rpc_types_eth::{ + Log, Transaction, TransactionInfo, TransactionReceipt, TransactionRequest, +}; use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks, Hardforks}; use reth_evm::{ConfigureEvm, SpecFor, TxEnvFor}; use reth_node_api::{FullNodeComponents, FullNodeTypes, NodeTypes}; @@ -23,15 +25,15 @@ use reth_rpc_convert::{ }; use reth_rpc_eth_api::{ helpers::{pending_block::BuildPendingEnv, AddDevSigners}, - FullEthApiServer, FromEvmError, RpcNodeCore, + FromEvmError, FullEthApiServer, RpcNodeCore, }; use reth_rpc_eth_types::receipt::build_receipt; use reth_rpc_eth_types::EthApiError; use std::marker::PhantomData; +use crate::EvolveEvmConfig; 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)] @@ -258,11 +260,12 @@ impl SignableTxRequest for EvTransactionRequest { self, signer: impl TxSigner + Send, ) -> Result { - let mut tx = - self.0.build_typed_tx().map_err(|_| SignTxRequestError::InvalidTransactionRequest)?; + 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(); + let signed: reth_ethereum_primitives::TransactionSigned = tx.into_signed(signature).into(); Ok(EvTxEnvelope::Ethereum(signed)) } } @@ -297,6 +300,7 @@ pub struct EvReceiptConverter { } impl EvReceiptConverter { + /// Creates a new receipt converter bound to the provided chain spec. pub const fn new(chain_spec: std::sync::Arc) -> Self { Self { chain_spec } } @@ -316,7 +320,9 @@ where 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); + let blob_params = self + .chain_spec + .blob_params_at_timestamp(input.meta.timestamp); let fee_payer = match input.tx.inner() { EvTxEnvelope::EvNode(ev) => ev .tx() @@ -329,7 +335,10 @@ where 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 } + AnyReceiptEnvelope { + inner, + r#type: tx_type, + } }); receipts.push(EvRpcReceipt::new(receipt, fee_payer)); } @@ -360,21 +369,25 @@ pub struct EvEthApiBuilder; impl EthApiBuilder for EvEthApiBuilder where N: FullNodeComponents< - Types: NodeTypes< + Types: NodeTypes< + Primitives = EvPrimitives, + ChainSpec: Hardforks + + EthereumHardforks + + EthChainSpec + + std::fmt::Debug + + Send + + Sync + + 'static, + >, + Evm = EvolveEvmConfig, + > + RpcNodeCore< Primitives = EvPrimitives, - ChainSpec: - Hardforks + EthereumHardforks + EthChainSpec + std::fmt::Debug + Send + Sync + 'static, + Provider = ::Provider, + Pool = ::Pool, + Evm = EvolveEvmConfig, >, - Evm = EvolveEvmConfig, - > + RpcNodeCore< - Primitives = EvPrimitives, - Provider = ::Provider, - Pool = ::Pool, - Evm = EvolveEvmConfig, - >, - ::Provider: ChainSpecProvider< - ChainSpec = <::Types as NodeTypes>::ChainSpec, - >, + ::Provider: + ChainSpecProvider::Types as NodeTypes>::ChainSpec>, ::Evm: ConfigureEvm>, TxEnvFor<::Evm>: From, @@ -402,7 +415,10 @@ where let rpc_converter = rpc_converter.with_tx_env_converter(EvTxEnvConverter::::default()); - Ok(ctx.eth_api_builder().with_rpc_converter(rpc_converter).build()) + Ok(ctx + .eth_api_builder() + .with_rpc_converter(rpc_converter) + .build()) } } @@ -410,7 +426,9 @@ where #[derive(Clone, Debug, Default)] pub struct EvRpcTxConverter; -impl RpcTxConverter, TransactionInfo> for EvRpcTxConverter { +impl RpcTxConverter, TransactionInfo> + for EvRpcTxConverter +{ type Err = EthApiError; fn convert_rpc_tx( @@ -428,7 +446,10 @@ impl RpcTxConverter, TransactionInfo> f EvTxEnvelope::Ethereum(_) => None, }; let recovered = Recovered::new_unchecked(tx, signer); - Ok(EvRpcTransaction::new(Transaction::from_transaction(recovered, tx_info), fee_payer)) + Ok(EvRpcTransaction::new( + Transaction::from_transaction(recovered, tx_info), + fee_payer, + )) } } diff --git a/crates/node/src/txpool.rs b/crates/node/src/txpool.rs index f7ac2ec..7a63861 100644 --- a/crates/node/src/txpool.rs +++ b/crates/node/src/txpool.rs @@ -5,11 +5,8 @@ use alloy_consensus::{ BlobTransactionValidationError, Signed, Typed2718, }; use alloy_eips::{ - eip2718::Encodable2718, - eip2718::WithEncoded, - eip7594::BlobTransactionSidecarVariant, - eip7840::BlobParams, - merge::EPOCH_SLOTS, + eip2718::Encodable2718, eip2718::WithEncoded, eip7594::BlobTransactionSidecarVariant, + eip7840::BlobParams, merge::EPOCH_SLOTS, }; use alloy_primitives::{Address, U256}; use c_kzg::KzgSettings; @@ -29,16 +26,21 @@ use reth_transaction_pool::{ }; use tracing::{debug, info}; +/// Pool transaction wrapper for `EvTxEnvelope`. #[derive(Debug, Clone)] pub struct EvPooledTransaction { inner: EthPooledTransaction, } impl EvPooledTransaction { + /// Creates a new pooled transaction from a recovered envelope and encoded length. pub fn new(transaction: Recovered, encoded_length: usize) -> Self { - Self { inner: EthPooledTransaction::new(transaction, encoded_length) } + Self { + inner: EthPooledTransaction::new(transaction, encoded_length), + } } + /// Returns the recovered transaction. pub const fn transaction(&self) -> &Recovered { self.inner.transaction() } @@ -198,7 +200,10 @@ impl alloy_consensus::Transaction for EvPooledTransaction { impl EthPoolTransaction for EvPooledTransaction { fn take_blob(&mut self) -> EthBlobTransactionSidecar { if self.is_eip4844() { - std::mem::replace(&mut self.inner.blob_sidecar, EthBlobTransactionSidecar::Missing) + std::mem::replace( + &mut self.inner.blob_sidecar, + EthBlobTransactionSidecar::Missing, + ) } else { EthBlobTransactionSidecar::None } @@ -211,8 +216,9 @@ impl EthPoolTransaction for EvPooledTransaction { 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()?; + 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, @@ -245,21 +251,30 @@ impl EthPoolTransaction for EvPooledTransaction { 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())), + None => Err(BlobTransactionValidationError::NotBlobTransaction( + self.ty(), + )), }, - EvTxEnvelope::EvNode(_) => Err(BlobTransactionValidationError::NotBlobTransaction(self.ty())), + EvTxEnvelope::EvNode(_) => Err(BlobTransactionValidationError::NotBlobTransaction( + self.ty(), + )), } } } +/// Errors returned by EV-specific transaction pool validation. #[derive(Debug, thiserror::Error)] pub enum EvTxPoolError { + /// EvNode transaction must include at least one call. #[error("evnode transaction must include at least one call")] EmptyCalls, + /// Only the first call may be a CREATE. #[error("only the first call may be CREATE")] InvalidCreatePosition, + /// Sponsor signature failed verification. #[error("invalid sponsor signature")] InvalidSponsorSignature, + /// Error while querying account info from the state provider. #[error("state provider error: {0}")] StateProvider(String), } @@ -277,22 +292,33 @@ impl PoolTransactionError for EvTxPoolError { } } +/// Transaction validator that adds EV-specific checks on top of the base validator. #[derive(Debug, Clone)] pub struct EvTransactionValidator { inner: Arc>, } impl EvTransactionValidator { + /// Wraps the provided Ethereum validator with EV-specific validation logic. pub fn new(inner: EthTransactionValidator) -> Self { - Self { inner: Arc::new(inner) } + Self { + inner: Arc::new(inner), + } } - fn validate_evnode_calls(&self, tx: &EvNodeTransaction) -> Result<(), InvalidPoolTransactionError> { + fn validate_evnode_calls( + &self, + tx: &EvNodeTransaction, + ) -> Result<(), InvalidPoolTransactionError> { if tx.calls.is_empty() { - return Err(InvalidPoolTransactionError::other(EvTxPoolError::EmptyCalls)); + return Err(InvalidPoolTransactionError::other( + EvTxPoolError::EmptyCalls, + )); } if tx.calls.iter().skip(1).any(|call| call.to.is_create()) { - return Err(InvalidPoolTransactionError::other(EvTxPoolError::InvalidCreatePosition)); + return Err(InvalidPoolTransactionError::other( + EvTxPoolError::InvalidCreatePosition, + )); } Ok(()) } @@ -305,11 +331,9 @@ impl EvTransactionValidator { Client: StateProviderFactory, { if state.is_none() { - let new_state = self - .inner - .client() - .latest() - .map_err(|err| InvalidPoolTransactionError::other(EvTxPoolError::StateProvider(err.to_string())))?; + let new_state = self.inner.client().latest().map_err(|err| { + InvalidPoolTransactionError::other(EvTxPoolError::StateProvider(err.to_string())) + })?; *state = Some(Box::new(new_state)); } Ok(()) @@ -328,10 +352,15 @@ impl EvTransactionValidator { let state = state.as_ref().expect("state provider is set"); let account = state .basic_account(&sponsor) - .map_err(|err| InvalidPoolTransactionError::other(EvTxPoolError::StateProvider(err.to_string())))? + .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 }); + return Err(InvalidPoolTransactionError::Overdraft { + cost: gas_cost, + balance: account.balance, + }); } Ok(()) } @@ -348,7 +377,10 @@ impl EvTransactionValidator { 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 Err(InvalidPoolTransactionError::Overdraft { + cost: *pooled.cost(), + balance: sender_balance, + }); } return Ok(()); }; @@ -358,9 +390,9 @@ impl EvTransactionValidator { if let Some(signature) = tx.fee_payer_signature.as_ref() { let executor = pooled.transaction().signer(); - let sponsor = tx - .recover_sponsor(executor, signature) - .map_err(|_| InvalidPoolTransactionError::other(EvTxPoolError::InvalidSponsorSignature))?; + let sponsor = tx.recover_sponsor(executor, signature).map_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, sponsor, gas_cost)?; @@ -403,7 +435,9 @@ where propagate, authorities, }, - Err(err) => TransactionValidationOutcome::Invalid(transaction.into_transaction(), err), + Err(err) => { + TransactionValidationOutcome::Invalid(transaction.into_transaction(), err) + } }, other => other, } @@ -437,8 +471,9 @@ where 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 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) @@ -460,7 +495,10 @@ where .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()) + .build_with_tasks::( + ctx.task_executor().clone(), + blob_store.clone(), + ) .map(EvTransactionValidator::new); if validator.validator().inner.eip4844() { diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index fd8e40c..712d670 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use alloy_rpc_types::engine::ExecutionData; +use ev_primitives::EvTxEnvelope; use reth_ethereum::{ chainspec::ChainSpec, node::{ @@ -15,7 +16,6 @@ use reth_ethereum::{ builder::rpc::PayloadValidatorBuilder, }, }; -use ev_primitives::EvTxEnvelope; use reth_ethereum_payload_builder::EthereumExecutionPayloadValidator; use reth_primitives_traits::{Block as _, RecoveredBlock, SealedBlock}; use tracing::info; @@ -57,7 +57,9 @@ impl PayloadValidator for EvolveEngineValidator { Ok(sealed_block) => { info!("Evolve engine validator: payload validation succeeded"); let ev_block = convert_sealed_block(sealed_block); - ev_block.try_recover().map_err(|e| NewPayloadError::Other(e.into())) + ev_block + .try_recover() + .map_err(|e| NewPayloadError::Other(e.into())) } Err(err) => { // Log the error for debugging. @@ -70,7 +72,9 @@ impl PayloadValidator for EvolveEngineValidator { let ExecutionData { payload, sidecar } = payload; let sealed_block = payload.try_into_block_with_sidecar(&sidecar)?.seal_slow(); let ev_block = convert_sealed_block(sealed_block); - ev_block.try_recover().map_err(|e| NewPayloadError::Other(e.into())) + ev_block + .try_recover() + .map_err(|e| NewPayloadError::Other(e.into())) } else { // For other errors, re-throw them. Err(NewPayloadError::Eth(err)) diff --git a/crates/tests/src/common.rs b/crates/tests/src/common.rs index 39d6d56..ae1af57 100644 --- a/crates/tests/src/common.rs +++ b/crates/tests/src/common.rs @@ -10,18 +10,17 @@ 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, + BaseFeeRedirect, BaseFeeRedirectSettings, ContractSizeLimitSettings, EvTxEvmFactory, MintPrecompileSettings, }; use eyre::Result; use reth_chainspec::{ChainSpec, ChainSpecBuilder}; -use reth_evm_ethereum::EthEvmConfig; use reth_primitives::{Header, Transaction}; use reth_provider::test_utils::{ExtendedAccount, MockEthProvider}; use serde_json::json; use tempfile::TempDir; -use ev_node::{EvolvePayloadBuilder, EvolvePayloadBuilderConfig}; +use ev_node::{EvolveEvmConfig, EvolvePayloadBuilder, EvolvePayloadBuilderConfig}; use evolve_ev_reth::EvolvePayloadAttributes; // Test constants @@ -133,7 +132,6 @@ impl EvolveTestFixture { // Create a test chain spec with our test chain ID let test_chainspec = create_test_chain_spec(); - let evm_config = EthEvmConfig::new(test_chainspec.clone()); let config = EvolvePayloadBuilderConfig::from_chain_spec(test_chainspec.as_ref()).unwrap(); config.validate().unwrap(); @@ -148,12 +146,10 @@ impl EvolveTestFixture { let contract_size_limit = config .contract_size_limit_settings() .map(|(limit, activation)| ContractSizeLimitSettings::new(limit, activation)); - let wrapped_evm = with_ev_handler( - evm_config, - base_fee_redirect, - mint_precompile, - contract_size_limit, - ); + let evm_factory = + EvTxEvmFactory::new(base_fee_redirect, mint_precompile, contract_size_limit); + let wrapped_evm = + EvolveEvmConfig::new_with_evm_factory(test_chainspec.clone(), evm_factory); let builder = EvolvePayloadBuilder::new(Arc::new(provider.clone()), wrapped_evm, config); From 099207904d31a18465d251268a947e28ed40ff19 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Mon, 19 Jan 2026 16:53:25 +0100 Subject: [PATCH 06/19] fix linter --- crates/ev-revm/src/factory.rs | 3 +- crates/ev-revm/src/handler.rs | 22 +++++---- crates/ev-revm/src/tx_env.rs | 82 +++++++++++++++++--------------- crates/node/src/builder.rs | 6 +-- crates/node/src/executor.rs | 33 ++++++------- crates/node/src/lib.rs | 2 +- crates/node/src/payload_types.rs | 8 ++-- crates/node/src/rpc.rs | 40 ++++++++-------- crates/node/src/txpool.rs | 16 ++++--- 9 files changed, 112 insertions(+), 100 deletions(-) diff --git a/crates/ev-revm/src/factory.rs b/crates/ev-revm/src/factory.rs index 654e20e..5c39269 100644 --- a/crates/ev-revm/src/factory.rs +++ b/crates/ev-revm/src/factory.rs @@ -9,7 +9,6 @@ use alloy_evm::{ use alloy_primitives::{Address, U256}; use ev_precompiles::mint::{MintPrecompile, MINT_PRECOMPILE_ADDR}; use reth_evm_ethereum::EthEvmConfig; -use reth_revm::revm::context_interface::journaled_state::JournalTr; use reth_revm::{ inspector::NoOpInspector, revm::{ @@ -17,7 +16,7 @@ use reth_revm::{ result::{EVMError, HaltReason}, Evm as RevmEvm, FrameStack, TxEnv, }, - context_interface::result::InvalidTransaction, + context_interface::{journaled_state::JournalTr, result::InvalidTransaction}, handler::instructions::EthInstructions, interpreter::interpreter::EthInterpreter, precompile::{PrecompileSpecId, Precompiles}, diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index 0eff216..892d4bc 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -7,8 +7,7 @@ use crate::{ use reth_revm::{ inspector::{Inspector, InspectorEvmTr, InspectorHandler}, revm::{ - context::result::ExecutionResult, - context::ContextSetters, + context::{result::ExecutionResult, ContextSetters}, context_interface::{ result::HaltReason, transaction::{AccessListItemTr, TransactionType}, @@ -215,7 +214,7 @@ where init_and_floor_gas: &InitialAndFloorGas, ) -> Result { let calls = match evm.ctx().tx().batch_calls() { - Some(calls) if calls.is_empty() => { + Some([]) => { return Err(Self::Error::from_string( "evnode transaction must include at least one call".into(), )); @@ -231,7 +230,7 @@ where let mut total_refunded: i64 = 0; let mut last_result: Option = None; - for call in calls.iter() { + for call in &calls { let mut call_tx = base_tx.clone(); call_tx.set_batch_call(call); evm.ctx_mut().set_tx(call_tx); @@ -517,13 +516,14 @@ mod tests { inspector::NoOpInspector, revm::{ context::Context, - context_interface::result::ExecutionResult, - context_interface::transaction::{AccessList, AccessListItem, TransactionType}, + context_interface::{ + result::ExecutionResult, + transaction::{AccessList, AccessListItem, TransactionType}, + }, database::{CacheDB, EmptyDB}, handler::{EthFrame, FrameResult}, interpreter::{CallOutcome, Gas, InstructionResult, InterpreterResult}, - primitives::hardfork::SpecId, - primitives::KECCAK_EMPTY, + primitives::{hardfork::SpecId, KECCAK_EMPTY}, state::{AccountInfo, EvmState}, }, MainContext, State, @@ -538,8 +538,10 @@ mod tests { type TestHandler = EvHandler>; use alloy_evm::{Evm, EvmEnv, EvmFactory}; - use reth_revm::revm::bytecode::Bytecode as RevmBytecode; - use reth_revm::revm::context::{BlockEnv, CfgEnv, TxEnv}; + use reth_revm::revm::{ + bytecode::Bytecode as RevmBytecode, + context::{BlockEnv, CfgEnv, TxEnv}, + }; const BASE_FEE: u64 = 100; const GAS_PRICE: u128 = 200; diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs index 919bac7..d507a2d 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -2,16 +2,20 @@ use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; use alloy_primitives::{Address, Bytes, U256}; use ev_primitives::{Call, EvTxEnvelope}; use reth_evm::TransactionEnv; -use reth_revm::revm::context::TxEnv; -use reth_revm::revm::context_interface::either::Either; -use reth_revm::revm::context_interface::transaction::{ - AccessList, AccessListItem, RecoveredAuthorization, SignedAuthorization, - Transaction as RevmTransaction, +use reth_revm::revm::{ + context::TxEnv, + context_interface::{ + either::Either, + transaction::{ + AccessList, AccessListItem, RecoveredAuthorization, SignedAuthorization, + Transaction as RevmTransaction, + }, + }, + handler::SystemCallTx, + primitives::{Address as RevmAddress, Bytes as RevmBytes, TxKind, B256}, }; -use reth_revm::revm::handler::SystemCallTx; -use reth_revm::revm::primitives::{Address as RevmAddress, Bytes as RevmBytes, TxKind, B256}; -/// Transaction environment wrapper that supports EvTxEnvelope conversions. +/// Transaction environment wrapper that supports `EvTxEnvelope` conversions. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct EvTxEnv { inner: TxEnv, @@ -23,14 +27,13 @@ pub struct EvTxEnv { impl EvTxEnv { /// Wrap a `TxEnv` with EV-specific metadata. - pub fn new(inner: TxEnv) -> Self { - let batch_value = inner.value; + pub const fn new(inner: TxEnv) -> Self { Self { + batch_value: inner.value, inner, sponsor: None, sponsor_signature_invalid: false, calls: Vec::new(), - batch_value, } } @@ -198,8 +201,8 @@ impl TransactionEnv for EvTxEnv { } } -impl alloy_evm::ToTxEnv for EvTxEnv { - fn to_tx_env(&self) -> EvTxEnv { +impl alloy_evm::ToTxEnv for EvTxEnv { + fn to_tx_env(&self) -> Self { self.clone() } } @@ -207,7 +210,7 @@ impl alloy_evm::ToTxEnv for EvTxEnv { 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::Ethereum(inner) => Self::new(TxEnv::from_recovered_tx(inner, sender)), EvTxEnvelope::EvNode(ev) => { let (sponsor, sponsor_signature_invalid) = if let Some(signature) = ev.tx().fee_payer_signature.as_ref() { @@ -222,29 +225,32 @@ impl FromRecoveredTx for EvTxEnv { let batch_value = calls .iter() .fold(U256::ZERO, |acc, call| acc.saturating_add(call.value)); - 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 = batch_value; - env.data = ev - .tx() - .calls - .first() - .map(|call| call.input.clone()) - .unwrap_or_default(); - let mut tx_env = EvTxEnv::new(env); - tx_env.sponsor = sponsor; - tx_env.sponsor_signature_invalid = sponsor_signature_invalid; - tx_env.calls = calls; - tx_env.batch_value = batch_value; - tx_env + let env = TxEnv { + caller: sender, + gas_limit: ev.tx().gas_limit, + gas_price: ev.tx().max_fee_per_gas, + kind: ev + .tx() + .calls + .first() + .map(|call| call.to) + .unwrap_or(TxKind::Create), + value: batch_value, + data: ev + .tx() + .calls + .first() + .map(|call| call.input.clone()) + .unwrap_or_default(), + ..Default::default() + }; + Self { + inner: env, + sponsor, + sponsor_signature_invalid, + calls, + batch_value, + } } } } @@ -262,7 +268,7 @@ impl SystemCallTx for EvTxEnv { system_contract_address: Address, data: Bytes, ) -> Self { - EvTxEnv::new( + Self::new( TxEnv::builder() .caller(caller) .data(data) diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 8f1fbfa..844e0fd 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -1,7 +1,5 @@ -use crate::config::EvolvePayloadBuilderConfig; -use crate::executor::EvEvmConfig; -use alloy_consensus::transaction::Transaction; -use alloy_consensus::transaction::TxHashRef; +use crate::{config::EvolvePayloadBuilderConfig, executor::EvEvmConfig}; +use alloy_consensus::transaction::{Transaction, TxHashRef}; use alloy_primitives::Address; use ev_revm::EvTxEvmFactory; use evolve_ev_reth::EvolvePayloadAttributes; diff --git a/crates/node/src/executor.rs b/crates/node/src/executor.rs index 8bddbfb..818d5d3 100644 --- a/crates/node/src/executor.rs +++ b/crates/node/src/executor.rs @@ -2,8 +2,7 @@ use alloy_consensus::{BlockHeader, Header}; use alloy_eips::Decodable2718; -use alloy_evm::eth::spec::EthExecutorSpec; -use alloy_evm::{FromRecoveredTx, FromTxWithEncoded}; +use alloy_evm::{eth::spec::EthExecutorSpec, FromRecoveredTx, FromTxWithEncoded}; use alloy_primitives::U256; use alloy_rpc_types_engine::ExecutionData; use ev_revm::{ @@ -35,15 +34,18 @@ use reth_revm::revm::{ }; use tracing::info; -use crate::evm_executor::{EvBlockExecutorFactory, EvReceiptBuilder}; -use crate::{config::EvolvePayloadBuilderConfig, EvolveNode}; +use crate::{ + config::EvolvePayloadBuilderConfig, + evm_executor::{EvBlockExecutorFactory, EvReceiptBuilder}, + 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 = EvEvmConfig; -/// EVM config wired for EvPrimitives. +/// EVM config wired for `EvPrimitives`. #[derive(Debug, Clone)] pub struct EvEvmConfig { /// Factory used to create block executors. @@ -210,10 +212,7 @@ where } }); - let mut gas_limit = parent.gas_limit; - let mut basefee = None; - - if self + let (gas_limit, basefee) = if self .chain_spec() .fork(reth_ethereum_forks::EthereumHardfork::London) .transitions_at_block(parent.number + 1) @@ -222,9 +221,13 @@ where .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); - } + ( + parent.gas_limit * elasticity_multiplier as u64, + Some(alloy_eips::eip1559::INITIAL_BASE_FEE), + ) + } else { + (parent.gas_limit, None) + }; let block_env = BlockEnv { number: U256::from(parent.number + 1), @@ -407,10 +410,8 @@ where 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()), - ) + Ok(EvEvmConfig::new_with_evm_factory(chain_spec, 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 10ed10c..7a9d525 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -25,7 +25,7 @@ pub mod executor; pub mod node; /// Payload service integration. pub mod payload_service; -/// Payload types for EvPrimitives. +/// Payload types for `EvPrimitives`. pub mod payload_types; /// RPC wiring for EvTxEnvelope support. pub mod rpc; diff --git a/crates/node/src/payload_types.rs b/crates/node/src/payload_types.rs index 715398b..81085e6 100644 --- a/crates/node/src/payload_types.rs +++ b/crates/node/src/payload_types.rs @@ -12,7 +12,7 @@ use reth_payload_builder::BlobSidecars; use reth_payload_primitives::BuiltPayload; use reth_primitives_traits::SealedBlock; -/// Built payload for EvPrimitives. +/// Built payload for `EvPrimitives`. #[derive(Debug, Clone)] pub struct EvBuiltPayload { id: PayloadId, @@ -76,7 +76,7 @@ impl EvBuiltPayload { self } - /// Converts this payload into an ExecutionPayloadEnvelopeV3. + /// Converts this payload into an `ExecutionPayloadEnvelopeV3`. pub fn try_into_v3(self) -> Result { let Self { block, @@ -104,7 +104,7 @@ impl EvBuiltPayload { }) } - /// Converts this payload into an ExecutionPayloadEnvelopeV4. + /// Converts this payload into an `ExecutionPayloadEnvelopeV4`. pub fn try_into_v4(self) -> Result { Ok(ExecutionPayloadEnvelopeV4 { execution_requests: self.requests.clone().unwrap_or_default(), @@ -112,7 +112,7 @@ impl EvBuiltPayload { }) } - /// Converts this payload into an ExecutionPayloadEnvelopeV5. + /// Converts this payload into an `ExecutionPayloadEnvelopeV5`. pub fn try_into_v5(self) -> Result { let Self { block, diff --git a/crates/node/src/rpc.rs b/crates/node/src/rpc.rs index 9bfdb61..3a24788 100644 --- a/crates/node/src/rpc.rs +++ b/crates/node/src/rpc.rs @@ -1,9 +1,9 @@ //! RPC wiring for EvTxEnvelope support. -use alloy_consensus::error::ValueError; -use alloy_consensus::transaction::Recovered; -use alloy_consensus::SignableTransaction; -use alloy_consensus::Transaction as ConsensusTransaction; +use alloy_consensus::{ + error::ValueError, transaction::Recovered, SignableTransaction, + Transaction as ConsensusTransaction, +}; use alloy_consensus_any::AnyReceiptEnvelope; use alloy_network::{Ethereum, ReceiptResponse, TransactionResponse, TxSigner}; use alloy_primitives::{Address, Signature, U256}; @@ -15,11 +15,11 @@ 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::{ + transaction::{ + ConvertReceiptInput, EthTxEnvError, ReceiptConverter, RpcTxConverter, SimTxConverter, + TryIntoSimTx, TryIntoTxEnv, TxEnvConverter, + }, RpcConvert, RpcConverter, RpcTransaction, RpcTxReq, RpcTypes, SignTxRequestError, SignableTxRequest, }; @@ -27,8 +27,7 @@ use reth_rpc_eth_api::{ helpers::{pending_block::BuildPendingEnv, AddDevSigners}, FromEvmError, FullEthApiServer, RpcNodeCore, }; -use reth_rpc_eth_types::receipt::build_receipt; -use reth_rpc_eth_types::EthApiError; +use reth_rpc_eth_types::{receipt::build_receipt, EthApiError}; use std::marker::PhantomData; use crate::EvolveEvmConfig; @@ -56,7 +55,7 @@ pub struct EvRpcTransaction { } impl EvRpcTransaction { - fn new(inner: Transaction, fee_payer: Option
) -> Self { + const fn new(inner: Transaction, fee_payer: Option
) -> Self { Self { inner, fee_payer } } } @@ -169,7 +168,10 @@ pub struct EvRpcReceipt { } impl EvRpcReceipt { - fn new(inner: TransactionReceipt>, fee_payer: Option
) -> Self { + const fn new( + inner: TransactionReceipt>, + fee_payer: Option
, + ) -> Self { Self { inner, fee_payer } } } @@ -293,7 +295,7 @@ impl TryIntoTxEnv for EvTransactionRequest { } } -/// Receipt converter for EvPrimitives. +/// Receipt converter for `EvPrimitives`. #[derive(Debug, Clone)] pub struct EvReceiptConverter { chain_spec: std::sync::Arc, @@ -362,7 +364,7 @@ pub type EvRpcConvert = RpcConverter< /// Eth API type for EvTxEnvelope-based nodes. pub type EvEthApiFor = EthApi>; -/// Builds [`EthApi`] for EvTxEnvelope nodes. +/// Builds [`EthApi`] for `EvTxEnvelope` nodes. #[derive(Debug, Default)] pub struct EvEthApiBuilder; @@ -410,8 +412,8 @@ where 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()); + .with_sim_tx_converter(EvSimTxConverter) + .with_rpc_tx_converter(EvRpcTxConverter); let rpc_converter = rpc_converter.with_tx_env_converter(EvTxEnvConverter::::default()); @@ -422,7 +424,7 @@ where } } -/// Converts EvTxEnvelope into RPC transaction responses. +/// Converts `EvTxEnvelope` into RPC transaction responses. #[derive(Clone, Debug, Default)] pub struct EvRpcTxConverter; @@ -453,7 +455,7 @@ impl RpcTxConverter, TransactionInfo> } } -/// Converts transaction requests into simulated EvTxEnvelope transactions. +/// Converts transaction requests into simulated `EvTxEnvelope` transactions. #[derive(Clone, Debug, Default)] pub struct EvSimTxConverter; @@ -469,7 +471,7 @@ impl SimTxConverter, EvTxEnvelope> for EvSimTxConverter { } } -/// Converts transaction requests into EvTxEnv. +/// Converts transaction requests into `EvTxEnv`. #[derive(Clone, Debug)] pub struct EvTxEnvConverter(PhantomData); diff --git a/crates/node/src/txpool.rs b/crates/node/src/txpool.rs index 7a63861..c700f8b 100644 --- a/crates/node/src/txpool.rs +++ b/crates/node/src/txpool.rs @@ -5,16 +5,20 @@ use alloy_consensus::{ BlobTransactionValidationError, Signed, Typed2718, }; use alloy_eips::{ - eip2718::Encodable2718, eip2718::WithEncoded, eip7594::BlobTransactionSidecarVariant, - eip7840::BlobParams, merge::EPOCH_SLOTS, + eip2718::{Encodable2718, 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_node_builder::{ + components::{create_blob_store_with_cache, PoolBuilder, TxPoolBuilder}, + BuilderContext, +}; use reth_primitives_traits::NodePrimitives; use reth_storage_api::{AccountInfoReader, StateProviderFactory}; use reth_transaction_pool::{ @@ -265,7 +269,7 @@ impl EthPoolTransaction for EvPooledTransaction { /// Errors returned by EV-specific transaction pool validation. #[derive(Debug, thiserror::Error)] pub enum EvTxPoolError { - /// EvNode transaction must include at least one call. + /// `EvNode` transaction must include at least one call. #[error("evnode transaction must include at least one call")] EmptyCalls, /// Only the first call may be a CREATE. @@ -444,7 +448,7 @@ where } } -/// Pool builder that wires the custom EvNode transaction validator. +/// Pool builder that wires the custom `EvNode` transaction validator. #[derive(Debug, Default, Clone, Copy)] #[non_exhaustive] pub struct EvolvePoolBuilder; From 400e0726b5be64c5e5ed7e8bb416af91b0651d88 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Mon, 19 Jan 2026 17:04:07 +0100 Subject: [PATCH 07/19] fix clippy --- crates/ev-revm/src/handler.rs | 54 ++++++++++++++++++++--------------- crates/ev-revm/src/tx_env.rs | 2 +- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index 892d4bc..90e5a26 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -598,13 +598,15 @@ mod tests { #[test] fn batch_initial_gas_sums_calls_and_access_list() { - let mut tx_env = TxEnv::default(); - tx_env.gas_limit = 1_000_000; - tx_env.tx_type = TransactionType::Eip1559.into(); - tx_env.access_list = AccessList(vec![AccessListItem { - address: address!("0x00000000000000000000000000000000000000aa"), - storage_keys: vec![B256::ZERO, B256::from([0x11; 32])], - }]); + let tx_env = TxEnv { + gas_limit: 1_000_000, + tx_type: TransactionType::Eip1559.into(), + access_list: AccessList(vec![AccessListItem { + address: address!("0x00000000000000000000000000000000000000aa"), + storage_keys: vec![B256::ZERO, B256::from([0x11; 32])], + }]), + ..Default::default() + }; let calls = vec![ Call { @@ -640,8 +642,10 @@ mod tests { #[test] fn batch_initial_gas_rejects_when_gas_limit_too_low() { - let mut tx_env = TxEnv::default(); - tx_env.gas_limit = 10_000; + let tx_env = TxEnv { + gas_limit: 10_000, + ..Default::default() + }; let calls = vec![Call { to: TxKind::Call(address!("0x00000000000000000000000000000000000000dd")), @@ -725,13 +729,15 @@ mod tests { }, ]; - let mut tx_env = TxEnv::default(); - tx_env.caller = caller; - tx_env.gas_limit = 200_000; - tx_env.gas_price = 1; - tx_env.gas_priority_fee = Some(1); - tx_env.chain_id = Some(1); - tx_env.tx_type = TransactionType::Eip1559.into(); + let tx_env = TxEnv { + caller, + gas_limit: 200_000, + gas_price: 1, + gas_priority_fee: Some(1), + chain_id: Some(1), + tx_type: TransactionType::Eip1559.into(), + ..Default::default() + }; let tx = EvTxEnv::with_calls(tx_env, calls); @@ -809,13 +815,15 @@ mod tests { }, ]; - let mut tx_env = TxEnv::default(); - tx_env.caller = caller; - tx_env.gas_limit = 200_000; - tx_env.gas_price = 1; - tx_env.gas_priority_fee = Some(1); - tx_env.chain_id = Some(1); - tx_env.tx_type = TransactionType::Eip1559.into(); + let tx_env = TxEnv { + caller, + gas_limit: 200_000, + gas_price: 1, + gas_priority_fee: Some(1), + chain_id: Some(1), + tx_type: TransactionType::Eip1559.into(), + ..Default::default() + }; let tx = EvTxEnv::with_calls(tx_env, calls); diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs index d507a2d..a491afa 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -87,7 +87,7 @@ impl EvTxEnv { inner.data = first.input.clone(); } inner.value = batch_value; - let mut env = EvTxEnv::new(inner); + let mut env = Self::new(inner); env.calls = calls; env.batch_value = batch_value; env From d8cf5f5c29179009c01612231bd039801d2089be Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Mon, 19 Jan 2026 17:06:04 +0100 Subject: [PATCH 08/19] fix clippy 2 --- crates/tests/src/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tests/src/common.rs b/crates/tests/src/common.rs index ae1af57..88ba769 100644 --- a/crates/tests/src/common.rs +++ b/crates/tests/src/common.rs @@ -149,7 +149,7 @@ impl EvolveTestFixture { let evm_factory = EvTxEvmFactory::new(base_fee_redirect, mint_precompile, contract_size_limit); let wrapped_evm = - EvolveEvmConfig::new_with_evm_factory(test_chainspec.clone(), evm_factory); + EvolveEvmConfig::new_with_evm_factory(test_chainspec, evm_factory); let builder = EvolvePayloadBuilder::new(Arc::new(provider.clone()), wrapped_evm, config); From 3c1af2b5d70f62a09244dc81ffc7009e7bf7bba0 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Mon, 19 Jan 2026 17:07:10 +0100 Subject: [PATCH 09/19] fmt linter --- crates/tests/src/common.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/tests/src/common.rs b/crates/tests/src/common.rs index 88ba769..2ee0226 100644 --- a/crates/tests/src/common.rs +++ b/crates/tests/src/common.rs @@ -148,8 +148,7 @@ impl EvolveTestFixture { .map(|(limit, activation)| ContractSizeLimitSettings::new(limit, activation)); let evm_factory = EvTxEvmFactory::new(base_fee_redirect, mint_precompile, contract_size_limit); - let wrapped_evm = - EvolveEvmConfig::new_with_evm_factory(test_chainspec, evm_factory); + let wrapped_evm = EvolveEvmConfig::new_with_evm_factory(test_chainspec, evm_factory); let builder = EvolvePayloadBuilder::new(Arc::new(provider.clone()), wrapped_evm, config); From e182cdd8fdf634088ba161ddcded2926cb4edccd Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Mon, 19 Jan 2026 18:04:16 +0100 Subject: [PATCH 10/19] fix e2e tests --- crates/ev-revm/src/tx_env.rs | 6 +++++- crates/node/src/executor.rs | 29 +++++++++++++++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs index a491afa..2adc8f5 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -310,7 +310,11 @@ impl SponsorPayerTx for EvTxEnv { impl BatchCallsTx for EvTxEnv { fn batch_calls(&self) -> Option<&[Call]> { - Some(&self.calls) + if self.calls.is_empty() { + None + } else { + Some(&self.calls) + } } fn batch_total_value(&self) -> U256 { diff --git a/crates/node/src/executor.rs b/crates/node/src/executor.rs index 818d5d3..a2d5755 100644 --- a/crates/node/src/executor.rs +++ b/crates/node/src/executor.rs @@ -1,7 +1,7 @@ //! Helpers to build the ev-reth executor with EV-specific hooks applied. use alloy_consensus::{BlockHeader, Header}; -use alloy_eips::Decodable2718; +use alloy_eips::{eip1559::INITIAL_BASE_FEE, Decodable2718}; use alloy_evm::{eth::spec::EthExecutorSpec, FromRecoveredTx, FromTxWithEncoded}; use alloy_primitives::U256; use alloy_rpc_types_engine::ExecutionData; @@ -212,22 +212,27 @@ where } }); - let (gas_limit, basefee) = if self - .chain_spec() + // Calculate base fee for the next block + let mut basefee = chain_spec.next_block_base_fee(parent, attributes.timestamp); + + let mut gas_limit = attributes.gas_limit; + + // If we are on the London fork boundary, we need to multiply the parent's gas limit by the + // elasticity multiplier to get the new gas limit. + if chain_spec .fork(reth_ethereum_forks::EthereumHardfork::London) .transitions_at_block(parent.number + 1) { - let elasticity_multiplier = self - .chain_spec() + let elasticity_multiplier = chain_spec .base_fee_params_at_timestamp(attributes.timestamp) .elasticity_multiplier; - ( - parent.gas_limit * elasticity_multiplier as u64, - Some(alloy_eips::eip1559::INITIAL_BASE_FEE), - ) - } else { - (parent.gas_limit, None) - }; + + // multiply the gas limit by the elasticity multiplier + gas_limit *= elasticity_multiplier as u64; + + // set the base fee to the initial base fee from the EIP-1559 spec + basefee = Some(INITIAL_BASE_FEE); + } let block_env = BlockEnv { number: U256::from(parent.number + 1), From 185a52ea1505d1f49c8d2f30b53c79db1f6e92d2 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Mon, 19 Jan 2026 18:12:19 +0100 Subject: [PATCH 11/19] remove error on payload builder when ev node tx --- crates/node/src/builder.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 844e0fd..5533c49 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -156,6 +156,7 @@ where )) })?; + // Validate sponsor signature for EvNode transactions (early check before EVM execution) if let ev_primitives::EvTxEnvelope::EvNode(ev) = recovered_tx.inner() { if let Some(signature) = ev.tx().fee_payer_signature.as_ref() { ev.tx() @@ -166,10 +167,6 @@ where )) })?; } - - return Err(PayloadBuilderError::Internal(RethError::Other( - "EvNode transaction execution not supported yet".into(), - ))); } // Execute the transaction From dae3ab69a79c9615f4686d649bfd9f6bcdfc0bd2 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 20 Jan 2026 12:58:14 +0100 Subject: [PATCH 12/19] add e2e test for signature of sponsorship --- crates/ev-primitives/src/tx.rs | 7 +- crates/node/src/rpc.rs | 20 ++++ crates/tests/src/e2e_tests.rs | 198 ++++++++++++++++++++++++++++++++- 3 files changed, 220 insertions(+), 5 deletions(-) diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index 5d76b87..f4ef037 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -135,10 +135,11 @@ impl EvNodeTransaction { } fn encoded_payload_with_executor(&self, executor: Address) -> Vec { - let mut out = - Vec::with_capacity(self.payload_fields_length(self.fee_payer_signature.as_ref()) + 32); + // Sponsor signatures must be computed over the unsigned sponsor field to avoid + // self-referential hashing. + let mut out = Vec::with_capacity(self.payload_fields_length(None) + 32); out.extend_from_slice(executor.as_slice()); - self.encode_payload_fields(&mut out, self.fee_payer_signature.as_ref()); + self.encode_payload_fields(&mut out, None); out } diff --git a/crates/node/src/rpc.rs b/crates/node/src/rpc.rs index 3a24788..cddc891 100644 --- a/crates/node/src/rpc.rs +++ b/crates/node/src/rpc.rs @@ -58,6 +58,16 @@ impl EvRpcTransaction { const fn new(inner: Transaction, fee_payer: Option
) -> Self { Self { inner, fee_payer } } + + /// Returns the optional fee payer address. + pub const fn fee_payer(&self) -> Option
{ + self.fee_payer + } + + /// Returns the inner transaction. + pub const fn inner(&self) -> &Transaction { + &self.inner + } } impl ConsensusTransaction for EvRpcTransaction { @@ -174,6 +184,16 @@ impl EvRpcReceipt { ) -> Self { Self { inner, fee_payer } } + + /// Returns the optional fee payer address. + pub const fn fee_payer(&self) -> Option
{ + self.fee_payer + } + + /// Returns the inner receipt. + pub const fn inner(&self) -> &TransactionReceipt> { + &self.inner + } } impl ReceiptResponse for EvRpcReceipt { diff --git a/crates/tests/src/e2e_tests.rs b/crates/tests/src/e2e_tests.rs index 21a8ef5..af771fd 100644 --- a/crates/tests/src/e2e_tests.rs +++ b/crates/tests/src/e2e_tests.rs @@ -1,5 +1,5 @@ -use alloy_consensus::{TxEnvelope, TxReceipt}; -use alloy_eips::{eip2718::Encodable2718, BlockNumberOrTag}; +use alloy_consensus::{SignableTransaction, TxEnvelope, TxReceipt}; +use alloy_eips::{eip2718::Encodable2718, eip2930::AccessList, BlockNumberOrTag}; use alloy_network::eip2718::Decodable2718; use alloy_primitives::{address, Address, Bytes, TxKind, B256, U256}; use alloy_rpc_types::{ @@ -10,6 +10,7 @@ use alloy_rpc_types::{ BlockId, }; use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes, PayloadStatusEnum}; +use alloy_signer::SignerSync; use alloy_sol_types::{sol, SolCall}; use eyre::Result; use futures::future; @@ -28,6 +29,8 @@ use crate::common::{ create_test_chain_spec, create_test_chain_spec_with_base_fee_sink, create_test_chain_spec_with_mint_admin, TEST_CHAIN_ID, }; +use ev_node::rpc::{EvRpcReceipt, EvRpcTransaction, EvTransactionRequest}; +use ev_primitives::{Call, EvNodeTransaction, EvTxEnvelope}; use ev_precompiles::mint::MINT_PRECOMPILE_ADDR; sol! { @@ -374,6 +377,197 @@ async fn test_e2e_base_fee_sink_receives_base_fee() -> Result<()> { Ok(()) } +/// Tests that a sponsored EvNode transaction charges gas to the sponsor, not the executor. +/// +/// # Test Flow +/// 1. Creates an executor and sponsor account from genesis-funded wallets +/// 2. Builds a sponsored EvNode transfer transaction (type 0x76) +/// 3. Includes the transaction in a block via the Engine API +/// 4. Verifies `feePayer` appears in the RPC tx and receipt responses +/// 5. Asserts balances: executor pays value, sponsor pays gas +/// +/// # Success Criteria +/// - Receipt and transaction expose the sponsor address +/// - Recipient receives the transfer value +/// - Executor balance decreases by exactly `value` +/// - Sponsor balance decreases by exactly `gas_used * effective_gas_price` +#[tokio::test(flavor = "multi_thread")] +async fn test_e2e_sponsored_evnode_transaction() -> Result<()> { + reth_tracing::init_test_tracing(); + + let chain_spec = create_test_chain_spec(); + let chain_id = chain_spec.chain().id(); + + let mut setup = Setup::::default() + .with_chain_spec(chain_spec) + .with_network(NetworkSetup::single_node()) + .with_dev_mode(true); + + let mut env = Environment::::default(); + setup.apply::(&mut env).await?; + + let parent_block = env.node_clients[0] + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("parent block should exist"); + let mut parent_hash = parent_block.header.hash; + let mut parent_timestamp = parent_block.header.inner.timestamp; + let mut parent_number = parent_block.header.inner.number; + let gas_limit = parent_block.header.inner.gas_limit; + + let mut wallets = Wallet::new(3).with_chain_id(chain_id).wallet_gen(); + let executor = wallets.remove(0); + let sponsor = wallets.remove(0); + let recipient = Address::random(); + let executor_address = executor.address(); + let sponsor_address = sponsor.address(); + + let executor_balance_before = + EthApiClient::::balance( + &env.node_clients[0].rpc, + executor_address, + Some(BlockId::latest()), + ) + .await?; + let sponsor_balance_before = + EthApiClient::::balance( + &env.node_clients[0].rpc, + sponsor_address, + Some(BlockId::latest()), + ) + .await?; + let recipient_balance_before = + EthApiClient::::balance( + &env.node_clients[0].rpc, + recipient, + Some(BlockId::latest()), + ) + .await?; + + let executor_nonce = EthApiClient::::transaction_count( + &env.node_clients[0].rpc, + executor_address, + Some(BlockId::latest()), + ) + .await?; + let executor_nonce = u64::try_from(executor_nonce).expect("nonce fits into u64"); + + let transfer_value = U256::from(1_000_000_000_000_000u64); // 0.001 ETH + let call = Call { + to: TxKind::Call(recipient), + value: transfer_value, + input: Bytes::default(), + }; + + let ev_tx = EvNodeTransaction { + chain_id, + nonce: executor_nonce, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 2_000_000_000, + gas_limit: 100_000, + calls: vec![call], + access_list: AccessList::default(), + fee_payer_signature: None, + }; + + let executor_sig = executor + .sign_hash_sync(&ev_tx.signature_hash()) + .expect("executor signature"); + let mut signed = ev_tx.into_signed(executor_sig); + let sponsor_hash = signed.tx().sponsor_signing_hash(executor_address); + let sponsor_sig = sponsor + .sign_hash_sync(&sponsor_hash) + .expect("sponsor signature"); + signed.tx_mut().fee_payer_signature = Some(sponsor_sig); + + let envelope = EvTxEnvelope::EvNode(signed); + let raw_tx: Bytes = envelope.encoded_2718().into(); + let tx_hash = *envelope.tx_hash(); + + build_block_with_transactions( + &mut env, + &mut parent_hash, + &mut parent_number, + &mut parent_timestamp, + Some(gas_limit), + vec![raw_tx], + Address::ZERO, + ) + .await?; + + type EvRpcBlock = Block; + let receipt = EthApiClient::::transaction_receipt( + &env.node_clients[0].rpc, + tx_hash, + ) + .await? + .expect("sponsored transaction receipt available"); + let receipt_inner = receipt.inner(); + assert!(receipt_inner.status(), "sponsored transaction should succeed"); + assert_eq!( + receipt.fee_payer(), + Some(sponsor_address), + "receipt should expose sponsor fee payer" + ); + + let tx = EthApiClient::::transaction_by_hash( + &env.node_clients[0].rpc, + tx_hash, + ) + .await? + .expect("sponsored transaction available"); + assert_eq!( + tx.fee_payer(), + Some(sponsor_address), + "transaction should expose sponsor fee payer" + ); + + let executor_balance_after = + EthApiClient::::balance( + &env.node_clients[0].rpc, + executor_address, + Some(BlockId::latest()), + ) + .await?; + let sponsor_balance_after = + EthApiClient::::balance( + &env.node_clients[0].rpc, + sponsor_address, + Some(BlockId::latest()), + ) + .await?; + let recipient_balance_after = + EthApiClient::::balance( + &env.node_clients[0].rpc, + recipient, + Some(BlockId::latest()), + ) + .await?; + + let executor_spent = executor_balance_before.saturating_sub(executor_balance_after); + let sponsor_spent = sponsor_balance_before.saturating_sub(sponsor_balance_after); + let recipient_gain = recipient_balance_after.saturating_sub(recipient_balance_before); + assert_eq!( + recipient_gain, transfer_value, + "recipient should receive transfer value" + ); + assert_eq!( + executor_spent, transfer_value, + "executor should only pay value when sponsored" + ); + + let expected_gas_cost = U256::from(receipt_inner.gas_used) + .saturating_mul(U256::from(receipt_inner.effective_gas_price)); + assert_eq!( + sponsor_spent, expected_gas_cost, + "sponsor should pay gas cost" + ); + + drop(setup); + + Ok(()) +} + /// Tests minting and burning tokens to/from a dynamically generated wallet not in genesis. /// /// # Test Flow From 0f23ac3d02b0905040e1fee4b7253118944c9389 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Tue, 20 Jan 2026 16:32:30 +0100 Subject: [PATCH 13/19] fmt: format e2e tests --- crates/tests/src/e2e_tests.rs | 39 +++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/crates/tests/src/e2e_tests.rs b/crates/tests/src/e2e_tests.rs index af771fd..0ec0d52 100644 --- a/crates/tests/src/e2e_tests.rs +++ b/crates/tests/src/e2e_tests.rs @@ -1,6 +1,6 @@ -use alloy_consensus::{SignableTransaction, TxEnvelope, TxReceipt}; +use alloy_consensus::{transaction::TxHashRef, SignableTransaction, TxEnvelope, TxReceipt}; use alloy_eips::{eip2718::Encodable2718, eip2930::AccessList, BlockNumberOrTag}; -use alloy_network::eip2718::Decodable2718; +use alloy_network::{eip2718::Decodable2718, ReceiptResponse}; use alloy_primitives::{address, Address, Bytes, TxKind, B256, U256}; use alloy_rpc_types::{ eth::{ @@ -30,8 +30,8 @@ use crate::common::{ create_test_chain_spec_with_mint_admin, TEST_CHAIN_ID, }; use ev_node::rpc::{EvRpcReceipt, EvRpcTransaction, EvTransactionRequest}; -use ev_primitives::{Call, EvNodeTransaction, EvTxEnvelope}; use ev_precompiles::mint::MINT_PRECOMPILE_ADDR; +use ev_primitives::{Call, EvNodeTransaction, EvTxEnvelope}; sol! { /// Interface for the native token precompile used in e2e tests. @@ -377,11 +377,11 @@ async fn test_e2e_base_fee_sink_receives_base_fee() -> Result<()> { Ok(()) } -/// Tests that a sponsored EvNode transaction charges gas to the sponsor, not the executor. +/// Tests that a sponsored `EvNode` transaction charges gas to the sponsor, not the executor. /// /// # Test Flow /// 1. Creates an executor and sponsor account from genesis-funded wallets -/// 2. Builds a sponsored EvNode transfer transaction (type 0x76) +/// 2. Builds a sponsored `EvNode` transfer transaction (type 0x76) /// 3. Includes the transaction in a block via the Engine API /// 4. Verifies `feePayer` appears in the RPC tx and receipt responses /// 5. Asserts balances: executor pays value, sponsor pays gas @@ -444,12 +444,13 @@ async fn test_e2e_sponsored_evnode_transaction() -> Result<()> { ) .await?; - let executor_nonce = EthApiClient::::transaction_count( - &env.node_clients[0].rpc, - executor_address, - Some(BlockId::latest()), - ) - .await?; + let executor_nonce = + EthApiClient::::transaction_count( + &env.node_clients[0].rpc, + executor_address, + Some(BlockId::latest()), + ) + .await?; let executor_nonce = u64::try_from(executor_nonce).expect("nonce fits into u64"); let transfer_value = U256::from(1_000_000_000_000_000u64); // 0.001 ETH @@ -496,14 +497,20 @@ async fn test_e2e_sponsored_evnode_transaction() -> Result<()> { .await?; type EvRpcBlock = Block; - let receipt = EthApiClient::::transaction_receipt( - &env.node_clients[0].rpc, - tx_hash, - ) + let receipt = EthApiClient::< + EvTransactionRequest, + EvRpcTransaction, + EvRpcBlock, + EvRpcReceipt, + Header, + >::transaction_receipt(&env.node_clients[0].rpc, tx_hash) .await? .expect("sponsored transaction receipt available"); let receipt_inner = receipt.inner(); - assert!(receipt_inner.status(), "sponsored transaction should succeed"); + assert!( + receipt_inner.status(), + "sponsored transaction should succeed" + ); assert_eq!( receipt.fee_payer(), Some(sponsor_address), From d10f79e47e55128cf1abb3454b91c4ccb0441431 Mon Sep 17 00:00:00 2001 From: Jonathan Gimeno Date: Wed, 21 Jan 2026 10:38:25 +0100 Subject: [PATCH 14/19] fix issue with fee payer --- Cargo.lock | 1 + Cargo.toml | 1 + crates/ev-primitives/Cargo.toml | 1 + crates/ev-primitives/src/tx.rs | 6 +++ crates/ev-revm/src/handler.rs | 46 +++++++++++++------ crates/ev-revm/src/tx_env.rs | 7 ++- crates/node/src/validator.rs | 79 ++++++++++++++++++++++++++++---- crates/tests/assets/genesis.json | 2 +- 8 files changed, 118 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c0a428..9f0c537 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2971,6 +2971,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", + "alloy-serde", "bytes", "reth-codecs", "reth-db-api", diff --git a/Cargo.toml b/Cargo.toml index c9598ab..bc19abe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ alloy-rpc-types-eth = { version = "1.0.37", default-features = false } alloy-rpc-types-engine = { version = "1.0.37", default-features = false } alloy-signer = { version = "1.0.37", default-features = false } alloy-signer-local = { version = "1.0.37", features = ["mnemonic"] } +alloy-serde = { version = "1.0.37", default-features = false } 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 } diff --git a/crates/ev-primitives/Cargo.toml b/crates/ev-primitives/Cargo.toml index 2d525f2..b3bac41 100644 --- a/crates/ev-primitives/Cargo.toml +++ b/crates/ev-primitives/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" alloy-consensus = { workspace = true } alloy-eips = { workspace = true, features = ["serde"] } alloy-primitives = { workspace = true, features = ["k256", "rlp", "serde"] } +alloy-serde = { workspace = true } alloy-rlp = { workspace = true, features = ["derive"] } bytes = { workspace = true } reth-codecs = { workspace = true } diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index f4ef037..9100752 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -47,11 +47,17 @@ pub struct Call { /// EvNode batch + sponsorship transaction payload. #[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] pub struct EvNodeTransaction { + #[serde(with = "alloy_serde::quantity")] pub chain_id: u64, + #[serde(with = "alloy_serde::quantity")] pub nonce: u64, + #[serde(with = "alloy_serde::quantity")] pub max_priority_fee_per_gas: u128, + #[serde(with = "alloy_serde::quantity")] pub max_fee_per_gas: u128, + #[serde(with = "alloy_serde::quantity")] pub gas_limit: u64, pub calls: Vec, pub access_list: AccessList, diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index 90e5a26..e0cdeec 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -4,6 +4,7 @@ use crate::{ base_fee::{BaseFeeRedirect, BaseFeeRedirectError}, tx_env::{BatchCallsTx, SponsorPayerTx}, }; +use alloy_primitives::U256; use reth_revm::{ inspector::{Inspector, InspectorEvmTr, InspectorHandler}, revm::{ @@ -135,31 +136,25 @@ where is_nonce_check_disabled, )?; - let mut new_caller_balance = caller.info.balance; - if !is_balance_check_disabled && new_caller_balance < total_value { + // Only validate that caller has enough balance for the value transfer. + // Do NOT pre-deduct the value - it will be transferred during execution. + // This matches the mainnet behavior where only gas is pre-deducted. + if !is_balance_check_disabled && caller.info.balance < total_value { return Err(reth_revm::revm::context_interface::result::InvalidTransaction::LackOfFundForMaxFee { fee: Box::new(total_value), - balance: Box::new(new_caller_balance), + balance: Box::new(caller.info.balance), } .into()); } - new_caller_balance = new_caller_balance.saturating_sub(total_value); - if is_balance_check_disabled { - new_caller_balance = new_caller_balance.max(total_value); - } - - caller.info.set_balance(new_caller_balance); if is_call { caller.info.set_nonce(caller.info.nonce.saturating_add(1)); } } - let effective_balance_spending = - tx.effective_balance_spending(basefee, blob_price).expect( - "effective balance is always smaller than max balance so it can't overflow", - ); - let gas_cost = effective_balance_spending - total_value; + let effective_gas_price = tx.effective_gas_price(basefee); + let gas_cost = + U256::from(tx.gas_limit()).saturating_mul(U256::from(effective_gas_price)); let sponsor_account = journal.load_account_code(sponsor)?.data; let sponsor_balance = sponsor_account.info.balance; @@ -176,6 +171,8 @@ where new_sponsor_balance = new_sponsor_balance.max(gas_cost); } sponsor_account.info.set_balance(new_sponsor_balance); + // Mark the sponsor account as touched so state changes are included in the final state + journal.touch_account(sponsor); } else { let caller = journal.load_account_code(caller_address)?.data; validate_account_nonce_and_code( @@ -297,7 +294,26 @@ where evm: &mut Self::Evm, exec_result: &mut ::FrameResult, ) -> Result<(), Self::Error> { - self.inner.reimburse_caller(evm, exec_result) + // For sponsored transactions, reimburse the sponsor instead of the caller + let sponsor = evm.ctx().tx().sponsor(); + if let Some(sponsor) = sponsor { + let gas = exec_result.gas(); + let basefee = evm.ctx().block().basefee() as u128; + let effective_gas_price = evm.ctx().tx().effective_gas_price(basefee); + let reimbursement = U256::from( + effective_gas_price + .saturating_mul((gas.remaining() + gas.refunded() as u64) as u128), + ); + let journal = evm.ctx_mut().journal_mut(); + let sponsor_account = journal.load_account(sponsor)?.data; + let new_balance = sponsor_account.info.balance.saturating_add(reimbursement); + sponsor_account.info.set_balance(new_balance); + // Mark the sponsor account as touched so state changes are included in the final state + journal.touch_account(sponsor); + Ok(()) + } else { + self.inner.reimburse_caller(evm, exec_result) + } } fn reward_beneficiary( diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs index 2adc8f5..12fd4fa 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -8,7 +8,7 @@ use reth_revm::revm::{ either::Either, transaction::{ AccessList, AccessListItem, RecoveredAuthorization, SignedAuthorization, - Transaction as RevmTransaction, + Transaction as RevmTransaction, TransactionType, }, }, handler::SystemCallTx, @@ -229,6 +229,7 @@ impl FromRecoveredTx for EvTxEnv { caller: sender, gas_limit: ev.tx().gas_limit, gas_price: ev.tx().max_fee_per_gas, + gas_priority_fee: Some(ev.tx().max_priority_fee_per_gas), kind: ev .tx() .calls @@ -242,6 +243,10 @@ impl FromRecoveredTx for EvTxEnv { .first() .map(|call| call.input.clone()) .unwrap_or_default(), + nonce: ev.tx().nonce, + chain_id: Some(ev.tx().chain_id), + access_list: ev.tx().access_list.clone(), + tx_type: TransactionType::Eip1559.into(), ..Default::default() }; Self { diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index 712d670..6f8b290 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -2,8 +2,10 @@ use std::sync::Arc; +use alloy_consensus::Header; +use alloy_eips::Decodable2718; use alloy_rpc_types::engine::ExecutionData; -use ev_primitives::EvTxEnvelope; +use ev_primitives::{Block as EvBlock, BlockBody as EvBlockBody, EvTxEnvelope}; use reth_ethereum::{ chainspec::ChainSpec, node::{ @@ -65,13 +67,19 @@ impl PayloadValidator for EvolveEngineValidator { // Log the error for debugging. tracing::debug!("Evolve payload validation error: {:?}", err); - // Check if this is a block hash mismatch error - bypass it for evolve. - if matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) { - info!("Evolve engine validator: bypassing block hash mismatch for ev-reth"); - // 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(); - let ev_block = convert_sealed_block(sealed_block); + // Check if this is an error we can bypass for evolve (block hash mismatch or + // unknown tx type for EvNode transactions). + let should_bypass = + matches!(err, alloy_rpc_types::engine::PayloadError::BlockHash { .. }) + || err.to_string().contains("unexpected tx type"); + + if should_bypass { + info!( + "Evolve engine validator: bypassing validation error for ev-reth: {:?}", + err + ); + // For evolve, we trust the payload builder - parse the block with EvNode support. + let ev_block = parse_evolve_payload(payload)?; ev_block .try_recover() .map_err(|e| NewPayloadError::Other(e.into())) @@ -101,6 +109,61 @@ fn convert_sealed_block( SealedBlock::new_unchecked(ev_block, hash) } +/// Parses an execution payload containing EvNode transactions. +fn parse_evolve_payload( + payload: ExecutionData, +) -> Result, NewPayloadError> { + let ExecutionData { payload, sidecar } = payload; + + // Parse transactions using EvTxEnvelope which supports both Ethereum and EvNode types. + let transactions: Vec = payload + .transactions() + .iter() + .map(|tx| { + EvTxEnvelope::decode_2718(&mut tx.as_ref()) + .map_err(|e| NewPayloadError::Other(Box::new(e))) + }) + .collect::, _>>()?; + + // Build the block header from payload using the common accessor methods. + let v1 = payload.as_v1(); + let header = Header { + parent_hash: payload.parent_hash(), + ommers_hash: alloy_consensus::EMPTY_OMMER_ROOT_HASH, + beneficiary: payload.fee_recipient(), + state_root: v1.state_root, + transactions_root: alloy_consensus::proofs::calculate_transaction_root(&transactions), + receipts_root: v1.receipts_root, + logs_bloom: v1.logs_bloom, + difficulty: alloy_primitives::U256::ZERO, + number: payload.block_number(), + gas_limit: payload.gas_limit(), + gas_used: v1.gas_used, + timestamp: payload.timestamp(), + extra_data: v1.extra_data.clone(), + mix_hash: payload.prev_randao(), + nonce: alloy_primitives::B64::ZERO, + base_fee_per_gas: Some(payload.saturated_base_fee_per_gas()), + withdrawals_root: payload + .withdrawals() + .map(|w| alloy_consensus::proofs::calculate_withdrawals_root(w)), + blob_gas_used: payload.blob_gas_used(), + excess_blob_gas: payload.excess_blob_gas(), + parent_beacon_block_root: sidecar.parent_beacon_block_root(), + requests_hash: None, + }; + + // Build block body. + let body = EvBlockBody { + transactions, + ommers: vec![], + withdrawals: payload.withdrawals().cloned().map(Into::into), + }; + + let block = EvBlock::new(header, body); + Ok(block.seal_slow()) +} + impl EngineApiValidator for EvolveEngineValidator { fn validate_version_specific_fields( &self, diff --git a/crates/tests/assets/genesis.json b/crates/tests/assets/genesis.json index 6f24658..c40e542 100644 --- a/crates/tests/assets/genesis.json +++ b/crates/tests/assets/genesis.json @@ -1,6 +1,6 @@ { "config": { - "chainId": 1, + "chainId": 1234, "homesteadBlock": 0, "daoForkSupport": true, "eip150Block": 0, From 073760c8f79dc0afab554f4c0736c066697d1084 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Wed, 21 Jan 2026 11:08:55 +0100 Subject: [PATCH 15/19] fix clippy --- crates/node/src/validator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/node/src/validator.rs b/crates/node/src/validator.rs index 6f8b290..93052ad 100644 --- a/crates/node/src/validator.rs +++ b/crates/node/src/validator.rs @@ -109,7 +109,7 @@ fn convert_sealed_block( SealedBlock::new_unchecked(ev_block, hash) } -/// Parses an execution payload containing EvNode transactions. +/// Parses an execution payload containing `EvNode` transactions. fn parse_evolve_payload( payload: ExecutionData, ) -> Result, NewPayloadError> { From 5555a95c9b3e13329087928e672a46deae0d118b Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Wed, 21 Jan 2026 11:09:54 +0100 Subject: [PATCH 16/19] fmt --- crates/ev-revm/src/evm.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/ev-revm/src/evm.rs b/crates/ev-revm/src/evm.rs index 0720159..a6e3432 100644 --- a/crates/ev-revm/src/evm.rs +++ b/crates/ev-revm/src/evm.rs @@ -1,8 +1,6 @@ //! EV-specific EVM wrapper that installs the base-fee redirect handler. -use crate::{ - base_fee::BaseFeeRedirect, deploy::DeployAllowlistSettings, tx_env::EvTxEnv, -}; +use crate::{base_fee::BaseFeeRedirect, deploy::DeployAllowlistSettings, tx_env::EvTxEnv}; use alloy_evm::{Evm as AlloyEvm, EvmEnv}; use alloy_primitives::{Address, Bytes}; use reth_revm::{ From c921f2ff73d7889ebf6a004462beb306dfe442a0 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Wed, 21 Jan 2026 13:20:04 +0100 Subject: [PATCH 17/19] add case where invalid signature was included but did not allow to build block --- crates/ev-revm/src/tx_env.rs | 61 ++++++++++++++++++ crates/node/src/builder.rs | 13 ---- crates/tests/src/e2e_tests.rs | 113 +++++++++++++++++++++++++++++++++- 3 files changed, 173 insertions(+), 14 deletions(-) diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs index 12fd4fa..a8a7637 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -261,6 +261,67 @@ impl FromRecoveredTx for EvTxEnv { } } +#[cfg(test)] +mod tests { + use super::EvTxEnv; + use alloy_evm::FromRecoveredTx; + use alloy_primitives::{Address, Bytes, Signature, TxKind, U256}; + use ev_primitives::{Call, EvNodeSignedTx, EvNodeTransaction, EvTxEnvelope}; + + fn sample_evnode_tx() -> EvNodeTransaction { + EvNodeTransaction { + chain_id: 1, + nonce: 1, + max_priority_fee_per_gas: 1, + max_fee_per_gas: 2, + gas_limit: 21_000, + calls: vec![Call { + to: TxKind::Call(Address::ZERO), + value: U256::ZERO, + input: Bytes::default(), + }], + access_list: Default::default(), + fee_payer_signature: None, + } + } + + fn signature_with_parity(v: u8, r: u8, s: u8) -> Signature { + let mut bytes = [0u8; 65]; + bytes[0] = r; + bytes[32] = s; + bytes[64] = v; + Signature::from_raw_array(&bytes).expect("valid parity") + } + + #[test] + fn from_recovered_tx_marks_invalid_sponsor_signature() { + let executor = Address::from([0x11; 20]); + let mut tx = sample_evnode_tx(); + tx.fee_payer_signature = Some(signature_with_parity(27, 0, 0)); + + let signed = EvNodeSignedTx::new_unhashed(tx, signature_with_parity(27, 1, 1)); + let env = EvTxEnv::from_recovered_tx(&EvTxEnvelope::EvNode(signed), executor); + + assert!(env.sponsor().is_none(), "invalid signature should not recover sponsor"); + assert!( + env.sponsor_signature_invalid(), + "invalid signature should be flagged" + ); + } + + #[test] + fn from_recovered_tx_allows_missing_sponsor_signature() { + let executor = Address::from([0x22; 20]); + let tx = sample_evnode_tx(); + + let signed = EvNodeSignedTx::new_unhashed(tx, signature_with_parity(27, 1, 1)); + let env = EvTxEnv::from_recovered_tx(&EvTxEnvelope::EvNode(signed), executor); + + assert!(env.sponsor().is_none()); + assert!(!env.sponsor_signature_invalid()); + } +} + impl FromTxWithEncoded for EvTxEnv { fn from_encoded_tx(tx: &EvTxEnvelope, caller: Address, _encoded: Bytes) -> Self { Self::from_recovered_tx(tx, caller) diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index 5533c49..64586ac 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -156,19 +156,6 @@ where )) })?; - // Validate sponsor signature for EvNode transactions (early check before EVM execution) - if let ev_primitives::EvTxEnvelope::EvNode(ev) = recovered_tx.inner() { - if let Some(signature) = ev.tx().fee_payer_signature.as_ref() { - ev.tx() - .recover_sponsor(recovered_tx.signer(), signature) - .map_err(|_| { - PayloadBuilderError::Internal(RethError::Other( - "Invalid sponsor signature".into(), - )) - })?; - } - } - // Execute the transaction match builder.execute_transaction(recovered_tx) { Ok(gas_used) => { diff --git a/crates/tests/src/e2e_tests.rs b/crates/tests/src/e2e_tests.rs index 3f16331..cf0db3a 100644 --- a/crates/tests/src/e2e_tests.rs +++ b/crates/tests/src/e2e_tests.rs @@ -1,7 +1,7 @@ use alloy_consensus::{transaction::TxHashRef, SignableTransaction, TxEnvelope, TxReceipt}; use alloy_eips::{eip2718::Encodable2718, eip2930::AccessList, BlockNumberOrTag}; use alloy_network::{eip2718::Decodable2718, ReceiptResponse}; -use alloy_primitives::{address, Address, Bytes, TxKind, B256, U256}; +use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; use alloy_rpc_types::{ eth::{ Block, BlockTransactions, Header, Receipt, Transaction, TransactionInput, @@ -576,6 +576,117 @@ async fn test_e2e_sponsored_evnode_transaction() -> Result<()> { Ok(()) } +/// Tests that an invalid sponsor signature is skipped during payload construction. +/// +/// # Test Flow +/// 1. Creates an executor account from genesis-funded wallets +/// 2. Builds an `EvNode` transaction with an invalid sponsor signature +/// 3. Attempts to build a payload via the Engine API +/// +/// # Success Criteria +/// - Payload is built successfully +/// - Invalid transaction is not included +#[tokio::test(flavor = "multi_thread")] +async fn test_e2e_invalid_sponsor_signature_skipped() -> Result<()> { + reth_tracing::init_test_tracing(); + + let chain_spec = create_test_chain_spec(); + let chain_id = chain_spec.chain().id(); + + let mut setup = Setup::::default() + .with_chain_spec(chain_spec) + .with_network(NetworkSetup::single_node()) + .with_dev_mode(true); + + let mut env = Environment::::default(); + setup.apply::(&mut env).await?; + + let parent_block = env.node_clients[0] + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("parent block should exist"); + let mut parent_hash = parent_block.header.hash; + let mut parent_timestamp = parent_block.header.inner.timestamp; + let mut parent_number = parent_block.header.inner.number; + let gas_limit = parent_block.header.inner.gas_limit; + + let mut wallets = Wallet::new(1).with_chain_id(chain_id).wallet_gen(); + let executor = wallets.remove(0); + let executor_address = executor.address(); + + let executor_nonce = + EthApiClient::::transaction_count( + &env.node_clients[0].rpc, + executor_address, + Some(BlockId::latest()), + ) + .await?; + let executor_nonce = u64::try_from(executor_nonce).expect("nonce fits into u64"); + + let call = Call { + to: TxKind::Call(Address::random()), + value: U256::ZERO, + input: Bytes::default(), + }; + + let ev_tx = EvNodeTransaction { + chain_id, + nonce: executor_nonce, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 2_000_000_000, + gas_limit: 100_000, + calls: vec![call], + access_list: AccessList::default(), + fee_payer_signature: None, + }; + + let executor_sig = executor + .sign_hash_sync(&ev_tx.signature_hash()) + .expect("executor signature"); + let mut signed = ev_tx.into_signed(executor_sig); + + let mut invalid_sig_bytes = [0u8; 65]; + invalid_sig_bytes[64] = 27; + let invalid_sig = + Signature::from_raw_array(&invalid_sig_bytes).expect("invalid sponsor signature bytes"); + signed.tx_mut().fee_payer_signature = Some(invalid_sig); + + let envelope = EvTxEnvelope::EvNode(signed); + let raw_tx: Bytes = envelope.encoded_2718().into(); + let tx_hash = *envelope.tx_hash(); + + let payload_envelope = build_block_with_transactions( + &mut env, + &mut parent_hash, + &mut parent_number, + &mut parent_timestamp, + Some(gas_limit), + vec![raw_tx], + Address::ZERO, + ) + .await?; + + let payload_inner = payload_envelope + .execution_payload + .payload_inner + .payload_inner; + assert!( + payload_inner.transactions.is_empty(), + "invalid sponsor tx should be skipped" + ); + + let tx = EthApiClient::, EvRpcReceipt, Header>::transaction_by_hash( + &env.node_clients[0].rpc, + tx_hash, + ) + .await?; + assert!(tx.is_none(), "invalid sponsor tx should not be in the block"); + + drop(setup); + + Ok(()) +} + /// Tests minting and burning tokens to/from a dynamically generated wallet not in genesis. /// /// # Test Flow From 25c810a368296b6a38026be2fc70dccf8609de5e Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Wed, 21 Jan 2026 13:26:54 +0100 Subject: [PATCH 18/19] fix lint problems --- CLAUDE.md | 1 + crates/ev-revm/src/tx_env.rs | 125 +++++++++++++++++----------------- crates/tests/src/e2e_tests.rs | 16 +++-- 3 files changed, 76 insertions(+), 66 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3b1ddc1..9f3a70c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Check formatting**: `make fmt-check` - **Run linter**: `make lint` - **Run all checks**: `make check-all` + - After changes, run `make check-all`. ### Running the Node - **Run with defaults**: `make run` diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs index a8a7637..f6856a8 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -261,67 +261,6 @@ impl FromRecoveredTx for EvTxEnv { } } -#[cfg(test)] -mod tests { - use super::EvTxEnv; - use alloy_evm::FromRecoveredTx; - use alloy_primitives::{Address, Bytes, Signature, TxKind, U256}; - use ev_primitives::{Call, EvNodeSignedTx, EvNodeTransaction, EvTxEnvelope}; - - fn sample_evnode_tx() -> EvNodeTransaction { - EvNodeTransaction { - chain_id: 1, - nonce: 1, - max_priority_fee_per_gas: 1, - max_fee_per_gas: 2, - gas_limit: 21_000, - calls: vec![Call { - to: TxKind::Call(Address::ZERO), - value: U256::ZERO, - input: Bytes::default(), - }], - access_list: Default::default(), - fee_payer_signature: None, - } - } - - fn signature_with_parity(v: u8, r: u8, s: u8) -> Signature { - let mut bytes = [0u8; 65]; - bytes[0] = r; - bytes[32] = s; - bytes[64] = v; - Signature::from_raw_array(&bytes).expect("valid parity") - } - - #[test] - fn from_recovered_tx_marks_invalid_sponsor_signature() { - let executor = Address::from([0x11; 20]); - let mut tx = sample_evnode_tx(); - tx.fee_payer_signature = Some(signature_with_parity(27, 0, 0)); - - let signed = EvNodeSignedTx::new_unhashed(tx, signature_with_parity(27, 1, 1)); - let env = EvTxEnv::from_recovered_tx(&EvTxEnvelope::EvNode(signed), executor); - - assert!(env.sponsor().is_none(), "invalid signature should not recover sponsor"); - assert!( - env.sponsor_signature_invalid(), - "invalid signature should be flagged" - ); - } - - #[test] - fn from_recovered_tx_allows_missing_sponsor_signature() { - let executor = Address::from([0x22; 20]); - let tx = sample_evnode_tx(); - - let signed = EvNodeSignedTx::new_unhashed(tx, signature_with_parity(27, 1, 1)); - let env = EvTxEnv::from_recovered_tx(&EvTxEnvelope::EvNode(signed), executor); - - assert!(env.sponsor().is_none()); - assert!(!env.sponsor_signature_invalid()); - } -} - impl FromTxWithEncoded for EvTxEnv { fn from_encoded_tx(tx: &EvTxEnvelope, caller: Address, _encoded: Bytes) -> Self { Self::from_recovered_tx(tx, caller) @@ -417,3 +356,67 @@ impl BatchCallsTx for TxEnv { self.data = call.input.clone(); } } + +#[cfg(test)] +mod tests { + use super::EvTxEnv; + use alloy_evm::FromRecoveredTx; + use alloy_primitives::{Address, Bytes, Signature, TxKind, U256}; + use ev_primitives::{Call, EvNodeSignedTx, EvNodeTransaction, EvTxEnvelope}; + + fn sample_evnode_tx() -> EvNodeTransaction { + EvNodeTransaction { + chain_id: 1, + nonce: 1, + max_priority_fee_per_gas: 1, + max_fee_per_gas: 2, + gas_limit: 21_000, + calls: vec![Call { + to: TxKind::Call(Address::ZERO), + value: U256::ZERO, + input: Bytes::default(), + }], + access_list: Default::default(), + fee_payer_signature: None, + } + } + + fn signature_with_parity(v: u8, r: u8, s: u8) -> Signature { + let mut bytes = [0u8; 65]; + bytes[0] = r; + bytes[32] = s; + bytes[64] = v; + Signature::from_raw_array(&bytes).expect("valid parity") + } + + #[test] + fn from_recovered_tx_marks_invalid_sponsor_signature() { + let executor = Address::from([0x11; 20]); + let mut tx = sample_evnode_tx(); + tx.fee_payer_signature = Some(signature_with_parity(27, 0, 0)); + + let signed = EvNodeSignedTx::new_unhashed(tx, signature_with_parity(27, 1, 1)); + let env = EvTxEnv::from_recovered_tx(&EvTxEnvelope::EvNode(signed), executor); + + assert!( + env.sponsor().is_none(), + "invalid signature should not recover sponsor" + ); + assert!( + env.sponsor_signature_invalid(), + "invalid signature should be flagged" + ); + } + + #[test] + fn from_recovered_tx_allows_missing_sponsor_signature() { + let executor = Address::from([0x22; 20]); + let tx = sample_evnode_tx(); + + let signed = EvNodeSignedTx::new_unhashed(tx, signature_with_parity(27, 1, 1)); + let env = EvTxEnv::from_recovered_tx(&EvTxEnvelope::EvNode(signed), executor); + + assert!(env.sponsor().is_none()); + assert!(!env.sponsor_signature_invalid()); + } +} diff --git a/crates/tests/src/e2e_tests.rs b/crates/tests/src/e2e_tests.rs index cf0db3a..136ea25 100644 --- a/crates/tests/src/e2e_tests.rs +++ b/crates/tests/src/e2e_tests.rs @@ -675,12 +675,18 @@ async fn test_e2e_invalid_sponsor_signature_skipped() -> Result<()> { "invalid sponsor tx should be skipped" ); - let tx = EthApiClient::, EvRpcReceipt, Header>::transaction_by_hash( - &env.node_clients[0].rpc, - tx_hash, - ) + let tx = EthApiClient::< + EvTransactionRequest, + EvRpcTransaction, + Block, + EvRpcReceipt, + Header, + >::transaction_by_hash(&env.node_clients[0].rpc, tx_hash) .await?; - assert!(tx.is_none(), "invalid sponsor tx should not be in the block"); + assert!( + tx.is_none(), + "invalid sponsor tx should not be in the block" + ); drop(setup); From b7350120df3fe78123494a01569f8508b48de433 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Wed, 21 Jan 2026 17:03:26 +0100 Subject: [PATCH 19/19] add case where the vector is empty. --- crates/ev-revm/src/handler.rs | 50 +++++++++++++++++ crates/ev-revm/src/tx_env.rs | 32 +++++++++-- crates/tests/src/e2e_tests.rs | 102 ++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 4 deletions(-) diff --git a/crates/ev-revm/src/handler.rs b/crates/ev-revm/src/handler.rs index 6cd707a..59b3816 100644 --- a/crates/ev-revm/src/handler.rs +++ b/crates/ev-revm/src/handler.rs @@ -907,6 +907,56 @@ mod tests { assert!(slot.is_changed()); } + #[test] + fn batch_execution_rejects_empty_calls() { + let caller = address!("0x0000000000000000000000000000000000000aaa"); + + let mut state = State::builder() + .with_database(CacheDB::::default()) + .with_bundle_update() + .build(); + + state.insert_account( + caller, + AccountInfo { + balance: U256::from(10_000_000_000u64), + nonce: 0, + code_hash: KECCAK_EMPTY, + code: None, + }, + ); + + let mut evm_env: EvmEnv = EvmEnv::default(); + evm_env.cfg_env.chain_id = 1; + evm_env.cfg_env.spec = SpecId::CANCUN; + evm_env.block_env.basefee = 1; + evm_env.block_env.gas_limit = 30_000_000; + evm_env.block_env.number = U256::from(1); + + let mut evm = EvTxEvmFactory::default().create_evm(state, evm_env); + + let tx_env = TxEnv { + caller, + gas_limit: 200_000, + gas_price: 1, + gas_priority_fee: Some(1), + chain_id: Some(1), + tx_type: TransactionType::Eip1559.into(), + ..Default::default() + }; + + let tx = EvTxEnv::with_calls(tx_env, Vec::new()); + + let err = evm + .transact_raw(tx) + .expect_err("empty call batch should reject"); + assert!( + err.to_string() + .contains("evnode transaction must include at least one call"), + "unexpected error: {err:?}" + ); + } + #[test] fn reject_deploy_for_non_allowlisted_caller() { let allowlisted = address!("0x00000000000000000000000000000000000000aa"); diff --git a/crates/ev-revm/src/tx_env.rs b/crates/ev-revm/src/tx_env.rs index f6856a8..b5d820a 100644 --- a/crates/ev-revm/src/tx_env.rs +++ b/crates/ev-revm/src/tx_env.rs @@ -23,6 +23,7 @@ pub struct EvTxEnv { sponsor_signature_invalid: bool, calls: Vec, batch_value: U256, + is_evnode: bool, } impl EvTxEnv { @@ -34,6 +35,7 @@ impl EvTxEnv { sponsor: None, sponsor_signature_invalid: false, calls: Vec::new(), + is_evnode: false, } } @@ -90,6 +92,7 @@ impl EvTxEnv { let mut env = Self::new(inner); env.calls = calls; env.batch_value = batch_value; + env.is_evnode = true; env } } @@ -102,6 +105,7 @@ impl From for EvTxEnv { sponsor: None, sponsor_signature_invalid: false, calls: Vec::new(), + is_evnode: false, } } } @@ -255,6 +259,7 @@ impl FromRecoveredTx for EvTxEnv { sponsor_signature_invalid, calls, batch_value, + is_evnode: true, } } } @@ -315,10 +320,10 @@ impl SponsorPayerTx for EvTxEnv { impl BatchCallsTx for EvTxEnv { fn batch_calls(&self) -> Option<&[Call]> { - if self.calls.is_empty() { - None - } else { + if self.is_evnode || !self.calls.is_empty() { Some(&self.calls) + } else { + None } } @@ -359,7 +364,7 @@ impl BatchCallsTx for TxEnv { #[cfg(test)] mod tests { - use super::EvTxEnv; + use super::{BatchCallsTx, EvTxEnv}; use alloy_evm::FromRecoveredTx; use alloy_primitives::{Address, Bytes, Signature, TxKind, U256}; use ev_primitives::{Call, EvNodeSignedTx, EvNodeTransaction, EvTxEnvelope}; @@ -419,4 +424,23 @@ mod tests { assert!(env.sponsor().is_none()); assert!(!env.sponsor_signature_invalid()); } + + #[test] + fn batch_calls_exposes_empty_evnode_calls() { + let executor = Address::from([0x33; 20]); + let mut tx = sample_evnode_tx(); + tx.calls = Vec::new(); + + let signed = EvNodeSignedTx::new_unhashed(tx, signature_with_parity(27, 1, 1)); + let env = EvTxEnv::from_recovered_tx(&EvTxEnvelope::EvNode(signed), executor); + + let calls = env.batch_calls().expect("evnode should expose batch calls"); + assert!(calls.is_empty()); + } + + #[test] + fn batch_calls_omits_non_evnode_calls() { + let env = EvTxEnv::from(reth_revm::revm::context::TxEnv::default()); + assert!(env.batch_calls().is_none()); + } } diff --git a/crates/tests/src/e2e_tests.rs b/crates/tests/src/e2e_tests.rs index 136ea25..dbc618f 100644 --- a/crates/tests/src/e2e_tests.rs +++ b/crates/tests/src/e2e_tests.rs @@ -693,6 +693,108 @@ async fn test_e2e_invalid_sponsor_signature_skipped() -> Result<()> { Ok(()) } +/// Tests that an `EvNode` transaction with empty calls is skipped during payload construction. +/// +/// # Test Flow +/// 1. Creates an executor account from genesis-funded wallets +/// 2. Builds an `EvNode` transaction with an empty calls list +/// 3. Attempts to build a payload via the Engine API +/// +/// # Success Criteria +/// - Payload is built successfully +/// - Invalid transaction is not included +#[tokio::test(flavor = "multi_thread")] +async fn test_e2e_empty_calls_skipped() -> Result<()> { + reth_tracing::init_test_tracing(); + + let chain_spec = create_test_chain_spec(); + let chain_id = chain_spec.chain().id(); + + let mut setup = Setup::::default() + .with_chain_spec(chain_spec) + .with_network(NetworkSetup::single_node()) + .with_dev_mode(true); + + let mut env = Environment::::default(); + setup.apply::(&mut env).await?; + + let parent_block = env.node_clients[0] + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("parent block should exist"); + let mut parent_hash = parent_block.header.hash; + let mut parent_timestamp = parent_block.header.inner.timestamp; + let mut parent_number = parent_block.header.inner.number; + let gas_limit = parent_block.header.inner.gas_limit; + + let mut wallets = Wallet::new(1).with_chain_id(chain_id).wallet_gen(); + let executor = wallets.remove(0); + let executor_address = executor.address(); + + let executor_nonce = + EthApiClient::::transaction_count( + &env.node_clients[0].rpc, + executor_address, + Some(BlockId::latest()), + ) + .await?; + let executor_nonce = u64::try_from(executor_nonce).expect("nonce fits into u64"); + + let ev_tx = EvNodeTransaction { + chain_id, + nonce: executor_nonce, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 2_000_000_000, + gas_limit: 100_000, + calls: Vec::new(), + access_list: AccessList::default(), + fee_payer_signature: None, + }; + + let executor_sig = executor + .sign_hash_sync(&ev_tx.signature_hash()) + .expect("executor signature"); + let signed = ev_tx.into_signed(executor_sig); + + let envelope = EvTxEnvelope::EvNode(signed); + let raw_tx: Bytes = envelope.encoded_2718().into(); + let tx_hash = *envelope.tx_hash(); + + let payload_envelope = build_block_with_transactions( + &mut env, + &mut parent_hash, + &mut parent_number, + &mut parent_timestamp, + Some(gas_limit), + vec![raw_tx], + Address::ZERO, + ) + .await?; + + let payload_inner = payload_envelope + .execution_payload + .payload_inner + .payload_inner; + assert!( + payload_inner.transactions.is_empty(), + "empty calls tx should be skipped" + ); + + let tx = EthApiClient::< + EvTransactionRequest, + EvRpcTransaction, + Block, + EvRpcReceipt, + Header, + >::transaction_by_hash(&env.node_clients[0].rpc, tx_hash) + .await?; + assert!(tx.is_none(), "empty calls tx should not be in the block"); + + drop(setup); + + Ok(()) +} + /// Tests minting and burning tokens to/from a dynamically generated wallet not in genesis. /// /// # Test Flow