diff --git a/Cargo.lock b/Cargo.lock index cd8deb311764..415fd6021e6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,6 +277,18 @@ dependencies = [ "url", ] +[[package]] +name = "alloy-rpc-engine-types" +version = "0.1.0" +source = "git+https://github.com/alloy-rs/alloy#7b35f83099f78dcebfb5d1075f2245a85380a774" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types", + "serde", + "thiserror", +] + [[package]] name = "alloy-rpc-trace-types" version = "0.1.0" @@ -515,11 +527,13 @@ dependencies = [ "alloy-chains", "alloy-consensus", "alloy-dyn-abi", + "alloy-eips", "alloy-genesis", "alloy-network", "alloy-primitives", "alloy-providers", "alloy-rlp", + "alloy-rpc-engine-types", "alloy-rpc-trace-types", "alloy-rpc-types", "alloy-signer", @@ -582,6 +596,7 @@ dependencies = [ "alloy-network", "alloy-primitives", "alloy-rlp", + "alloy-rpc-engine-types", "alloy-rpc-trace-types", "alloy-rpc-types", "anvil-core", diff --git a/Cargo.toml b/Cargo.toml index e7e40baf7edb..5da07a0f68d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ alloy-pubsub = { git = "https://github.com/alloy-rs/alloy" } alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy" } alloy-rpc-trace-types = { git = "https://github.com/alloy-rs/alloy" } alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy" } +alloy-rpc-engine-types = { git = "https://github.com/alloy-rs/alloy" } alloy-signer = { git = "https://github.com/alloy-rs/alloy" } alloy-transport = { git = "https://github.com/alloy-rs/alloy" } alloy-transport-http = { git = "https://github.com/alloy-rs/alloy" } diff --git a/crates/anvil/Cargo.toml b/crates/anvil/Cargo.toml index 2eb7da5c255e..5af00df68f27 100644 --- a/crates/anvil/Cargo.toml +++ b/crates/anvil/Cargo.toml @@ -36,11 +36,13 @@ hash-db = "0.15" memory-db = "0.29" alloy-primitives = { workspace = true, features = ["serde"] } alloy-consensus.workspace = true +alloy-eips.workspace = true alloy-network.workspace = true alloy-rlp.workspace = true alloy-signer = { workspace = true, features = ["eip712", "mnemonic"] } alloy-sol-types = { workspace = true, features = ["std"] } alloy-dyn-abi = { workspace = true, features = ["std", "eip712"] } +alloy-rpc-engine-types.workspace = true alloy-rpc-types.workspace = true alloy-rpc-trace-types.workspace = true alloy-providers.workspace = true diff --git a/crates/anvil/core/Cargo.toml b/crates/anvil/core/Cargo.toml index 3b164a9d8120..f5e334aba981 100644 --- a/crates/anvil/core/Cargo.toml +++ b/crates/anvil/core/Cargo.toml @@ -16,6 +16,7 @@ revm = { workspace = true, default-features = false, features = ["std", "serde", alloy-primitives = { workspace = true, features = ["serde"] } alloy-rpc-types = { workspace = true } +alloy-rpc-engine-types = { workspace = true } alloy-rpc-trace-types.workspace = true alloy-rlp.workspace = true alloy-eips.workspace = true diff --git a/crates/anvil/core/src/eth/block.rs b/crates/anvil/core/src/eth/block.rs index c304eefa4a10..4a89396b36b2 100644 --- a/crates/anvil/core/src/eth/block.rs +++ b/crates/anvil/core/src/eth/block.rs @@ -3,8 +3,14 @@ use super::{ trie, }; use alloy_consensus::Header; +use alloy_network::Sealable; use alloy_primitives::{Address, Bloom, Bytes, B256, U256}; use alloy_rlp::{RlpDecodable, RlpEncodable}; +use alloy_rpc_engine_types::{ + BlobsBundleV1, ExecutionPayloadEnvelopeV3, ExecutionPayloadV1, ExecutionPayloadV2, + ExecutionPayloadV3, +}; +use alloy_rpc_types::Withdrawal; // Type alias to optionally support impersonated transactions #[cfg(not(feature = "impersonated-tx"))] @@ -20,6 +26,55 @@ pub struct BlockInfo { pub receipts: Vec, } +impl BlockInfo { + pub fn execution_envelope( + &self, + raw_transactions: &[Bytes], + args: BuildBlockArgs, + fees: U256, + ) -> ExecutionPayloadEnvelopeV3 { + let block_hash = self.block.header.hash(); + ExecutionPayloadEnvelopeV3 { + execution_payload: ExecutionPayloadV3 { + payload_inner: ExecutionPayloadV2 { + payload_inner: ExecutionPayloadV1 { + parent_hash: self.block.header.parent_hash, + fee_recipient: self.block.header.beneficiary, + state_root: self.block.header.state_root, + receipts_root: self.block.header.receipts_root, + logs_bloom: self.block.header.logs_bloom, + prev_randao: self.block.header.mix_hash, + block_number: self.block.header.number, + gas_limit: self.block.header.gas_limit, + gas_used: self.block.header.gas_used, + timestamp: self.block.header.timestamp, + extra_data: self.block.header.extra_data.to_owned(), + base_fee_per_gas: self + .block + .header + .base_fee_per_gas + .map(|f| U256::from(f)) + .unwrap_or("1000000000".parse::().unwrap()), + block_hash, + transactions: raw_transactions.to_vec(), + }, + withdrawals: args + .withdrawals + .into_iter() + .map(|w| w.into()) + .collect::>(), + }, + blob_gas_used: self.block.header.blob_gas_used.unwrap_or_default(), + excess_blob_gas: self.block.header.excess_blob_gas.unwrap_or_default(), + }, + block_value: fees, + blobs_bundle: BlobsBundleV1 { commitments: vec![], blobs: vec![], proofs: vec![] }, + should_override_builder: false, + parent_beacon_block_root: None, + } + } +} + /// An Ethereum Block #[derive(Clone, Debug, PartialEq, Eq, RlpEncodable, RlpDecodable)] pub struct Block { @@ -118,6 +173,24 @@ impl From
for PartialHeader { } } +#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct BuildBlockArgs { + pub slot: u64, + // #[serde(deserialize_with = "base64_to_bytes")] // TODO: use serde macro so we can use Bytes instead of String + pub proposer_pubkey: String, + pub parent: B256, + pub timestamp: u64, + pub fee_recipient: Address, + pub gas_limit: u64, + pub random: B256, + pub withdrawals: Vec, + pub parent_beacon_block_root: Option, + pub extra: Bytes, + pub beacon_root: B256, + pub fill_pending: bool, +} + #[cfg(test)] mod tests { use alloy_network::Sealable; diff --git a/crates/anvil/core/src/eth/bundle.rs b/crates/anvil/core/src/eth/bundle.rs new file mode 100644 index 000000000000..8dad01adf3ed --- /dev/null +++ b/crates/anvil/core/src/eth/bundle.rs @@ -0,0 +1,12 @@ +use alloy_primitives::{Bytes, B256, U256}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct SBundle { + pub block_number: Option, // // if BlockNumber is set it must match DecryptionCondition + pub max_block: Option, + #[serde(rename = "txs")] + pub transactions: Vec, + pub reverting_hashes: Option>, +} diff --git a/crates/anvil/core/src/eth/mod.rs b/crates/anvil/core/src/eth/mod.rs index acf8fb99f0d2..3d602682c56a 100644 --- a/crates/anvil/core/src/eth/mod.rs +++ b/crates/anvil/core/src/eth/mod.rs @@ -12,6 +12,7 @@ use alloy_rpc_types::{ }; pub mod block; +pub mod bundle; pub mod proof; pub mod subscription; pub mod transaction; @@ -23,6 +24,7 @@ pub mod serde_helpers; #[cfg(feature = "serde")] use self::serde_helpers::*; +use self::{block::BuildBlockArgs, bundle::SBundle}; #[cfg(feature = "serde")] use foundry_common::serde_helpers::{ @@ -173,6 +175,12 @@ pub enum EthRequest { #[cfg_attr(feature = "serde", serde(rename = "suavex_call"))] SuavexCall(Address, String), + #[cfg_attr(feature = "serde", serde(rename = "suavex_buildEthBlock"))] + SuavexBuildEthBlock(Option, Vec), + + #[cfg_attr(feature = "serde", serde(rename = "suavex_buildEthBlockFromBundles"))] + SuavexBuildEthBlockFromBundles(BuildBlockArgs, Vec), + #[cfg_attr(feature = "serde", serde(rename = "eth_createAccessList"))] EthCreateAccessList( TransactionRequest, diff --git a/crates/anvil/src/eth/api.rs b/crates/anvil/src/eth/api.rs index 59bbcdd19b99..c0c2abc6a85e 100644 --- a/crates/anvil/src/eth/api.rs +++ b/crates/anvil/src/eth/api.rs @@ -4,8 +4,8 @@ use super::{ }; use crate::{ eth::{ - backend, backend::{ + self, db::SerializableState, mem::{MIN_CREATE_GAS, MIN_TRANSACTION_GAS}, notifications::NewBlockNotifications, @@ -23,19 +23,19 @@ use crate::{ }, Pool, }, - sign, - sign::Signer, + sign::{self, Signer}, }, filter::{EthFilter, Filters, LogsFilter}, mem::transaction_build, revm::primitives::Output, ClientFork, LoggingManager, Miner, MiningMode, StorageInfo, }; -use alloy_consensus::TxLegacy; +use alloy_consensus::{TxEip1559, TxEip2930, TxLegacy}; use alloy_dyn_abi::TypedData; use alloy_network::{Signed, TxKind}; -use alloy_primitives::{Address, Bytes, TxHash, B256, B64, U256, U64}; -use alloy_rlp::Decodable; +use alloy_primitives::{Address, Bytes, FixedBytes, TxHash, B256, B64, U256, U64}; +use alloy_rlp::{Decodable, Encodable}; +use alloy_rpc_engine_types::ExecutionPayloadEnvelopeV3; use alloy_rpc_trace_types::{ geth::{DefaultFrame, GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace}, parity::LocalizedTransactionTrace, @@ -51,7 +51,8 @@ use alloy_rpc_types::{ use alloy_transport::TransportErrorKind; use anvil_core::{ eth::{ - block::BlockInfo, + block::{BlockInfo, BuildBlockArgs}, + bundle::SBundle, transaction::{ transaction_request_to_typed, PendingTransaction, TypedTransaction, TypedTransactionRequest, @@ -65,6 +66,7 @@ use anvil_core::{ }; use anvil_rpc::{error::RpcError, response::ResponseResult}; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; +use bytes::BytesMut; use foundry_common::provider::alloy::ProviderBuilder; use foundry_evm::{ backend::DatabaseError, @@ -75,7 +77,10 @@ use foundry_evm::{ primitives::BlockEnv, }, }; -use futures::channel::{mpsc::Receiver, oneshot}; +use futures::{ + channel::{mpsc::Receiver, oneshot}, + future::join_all, +}; use parking_lot::RwLock; use std::{collections::HashSet, future::Future, sync::Arc, time::Duration}; @@ -417,6 +422,12 @@ impl EthApi { EthRequest::SuavexCall(address, input) => { self.suavex_call(address, input).await.to_rpc_result() } + EthRequest::SuavexBuildEthBlock(args, txs) => { + self.suavex_build_eth_block(args, txs).await.to_rpc_result() + } + EthRequest::SuavexBuildEthBlockFromBundles(args, bundles) => { + self.suavex_build_eth_block_from_bundles(args, bundles).await.to_rpc_result() + } } } @@ -606,7 +617,7 @@ impl EthApi { if let BlockRequest::Number(number) = block_request { if let Some(fork) = self.get_fork() { if fork.predates_fork(number) { - return Ok(fork.get_balance(address, number).await?) + return Ok(fork.get_balance(address, number).await?); } } } @@ -763,7 +774,7 @@ impl EthApi { if let BlockRequest::Number(number) = block_request { if let Some(fork) = self.get_fork() { if fork.predates_fork(number) { - return Ok(fork.get_code(address, number).await?) + return Ok(fork.get_code(address, number).await?); } } } @@ -788,7 +799,7 @@ impl EthApi { if let BlockRequest::Number(number) = block_request { if let Some(fork) = self.get_fork() { if fork.predates_fork_inclusive(number) { - return Ok(fork.get_proof(address, keys, Some(number.into())).await?) + return Ok(fork.get_proof(address, keys, Some(number.into())).await?); } } } @@ -983,7 +994,7 @@ impl EthApi { "not available on past forked blocks".to_string(), )); } - return Ok(fork.call(&request, Some(number.into())).await?) + return Ok(fork.call(&request, Some(number.into())).await?); } } } @@ -1030,7 +1041,7 @@ impl EthApi { if let BlockRequest::Number(number) = block_request { if let Some(fork) = self.get_fork() { if fork.predates_fork(number) { - return Ok(fork.create_access_list(&request, Some(number.into())).await?) + return Ok(fork.create_access_list(&request, Some(number.into())).await?); } } } @@ -1172,7 +1183,7 @@ impl EthApi { self.backend.ensure_block_number(Some(BlockId::Hash(block_hash.into()))).await?; if let Some(fork) = self.get_fork() { if fork.predates_fork_inclusive(number) { - return Ok(fork.uncle_by_block_hash_and_index(block_hash, idx.into()).await?) + return Ok(fork.uncle_by_block_hash_and_index(block_hash, idx.into()).await?); } } // It's impossible to have uncles outside of fork mode @@ -2087,6 +2098,197 @@ impl EthApi { Ok(content) } +} + +// === impl EthApi suavex endpoints === + +struct PoolTxsAndFees { + transactions: Vec>, + fees: U256, +} + +impl EthApi { + // use alloy_rpc + async fn validate_tx(&self, tx: &Bytes) -> Result { + let mut data = tx.as_ref(); + if data.is_empty() { + return Err(BlockchainError::EmptyRawTransactionData); + } + let transaction = if data[0] > 0x7f { + // legacy transaction + match Signed::::decode(&mut data) { + Ok(transaction) => TypedTransaction::Legacy(transaction), + Err(_) => return Err(BlockchainError::FailedToDecodeSignedTransaction), + } + } else { + // the [TypedTransaction] requires a valid rlp input, + // but EIP-1559 prepends a version byte, so we need to encode the data first to get a + // valid rlp and then rlp decode impl of `TypedTransaction` will remove and check the + // version byte + let extend = alloy_rlp::encode(data); + let tx = match TypedTransaction::decode(&mut &extend[..]) { + Ok(transaction) => transaction, + Err(_) => return Err(BlockchainError::FailedToDecodeSignedTransaction), + }; + + self.ensure_typed_transaction_supported(&tx)?; + tx + }; + let pending_transaction = PendingTransaction::new(transaction)?; + + // pre-validate + self.backend.validate_pool_transaction(&pending_transaction).await?; + + let on_chain_nonce = self.backend.current_nonce(*pending_transaction.sender()).await?; + let from = *pending_transaction.sender(); + let nonce = pending_transaction.transaction.nonce(); + let requires = required_marker(nonce, on_chain_nonce, from); + + let priority = self.transaction_priority(&pending_transaction.transaction); + let pool_transaction = PoolTransaction { + requires, + provides: vec![to_marker(nonce.to::(), *pending_transaction.sender())], + pending_transaction, + priority, + }; + + // store in pool + self.pool.add_transaction(pool_transaction.clone())?; + + Ok(pool_transaction) + } + + async fn validate_txs(&self, txs: &[Bytes]) -> Result { + let transactions = join_all(txs.iter().map(|tx| async { self.validate_tx(tx).await })) + .await + .into_iter() + .filter(|res| res.is_ok()) + .map(|res| Arc::new(res.unwrap())) // TODO: probably a cleaner way to do this + .collect::>(); + let fees = transactions + .iter() + .map(|tx| tx.pending_transaction.transaction.clone()) + .map(|tx| tx.gas_price() * tx.gas_limit()) + .sum::(); + Ok(PoolTxsAndFees { transactions, fees }) + } + + fn validate_signed_request(&self, tx: &TransactionRequest) -> Result { + // TransactionRequest puts these in the 'other' field + let mut r = + tx.other.get("r").ok_or(BlockchainError::FailedToDecodeSignedTransaction)?.to_string(); + let mut s = + tx.other.get("s").ok_or(BlockchainError::FailedToDecodeSignedTransaction)?.to_string(); + let mut v = + tx.other.get("v").ok_or(BlockchainError::FailedToDecodeSignedTransaction)?.to_string(); + let mut hash = tx + .other + .get("hash") + .ok_or(BlockchainError::FailedToDecodeSignedTransaction)? + .to_string(); + // strip quotes & leading 0x + r = r[3..(r.length() - 3)].to_string(); + s = s[3..(s.length() - 3)].to_string(); + v = v[3..(v.length() - 2)].to_string(); + hash = hash[3..(hash.length() - 3)].to_string(); + // make sure all values are even-length + let pad_zero = |x: &mut String| { + if x.len() % 2 != 0 { + *x = format!("0{}", x); + } + }; + pad_zero(&mut r); + pad_zero(&mut s); + pad_zero(&mut v); + pad_zero(&mut hash); + + let r = r + .parse::>() + .map_err(|_| BlockchainError::FailedToDecodeSignedTransaction)?; + let s = s + .parse::>() + .map_err(|_| BlockchainError::FailedToDecodeSignedTransaction)?; + let v = u64::from_str_radix(&v, 16) + .map_err(|_| BlockchainError::FailedToDecodeSignedTransaction)?; + let hash = hash + .parse::>() + .map_err(|_| BlockchainError::FailedToDecodeSignedTransaction)?; + + let to_legacy = |txn: &TransactionRequest| TxLegacy { + chain_id: txn.chain_id.map(|cid| cid.to::()), + nonce: txn.nonce.unwrap_or_default().to::(), + gas_price: txn.gas_price.unwrap_or_default().to::(), + gas_limit: txn.gas.unwrap_or_default().to::(), + to: match txn.to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + value: txn.value.unwrap_or_default(), + input: txn.input.input.to_owned().unwrap_or_default(), + }; + let to_eip2930 = |txn: &TransactionRequest| TxEip2930 { + chain_id: txn.chain_id.unwrap_or_default().to::(), + nonce: txn.nonce.unwrap_or_default().to::(), + gas_price: txn.gas_price.unwrap_or_default().to::(), + gas_limit: txn.gas.unwrap_or_default().to::(), + to: match txn.to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + value: txn.value.unwrap_or_default(), + input: txn.input.input.to_owned().unwrap_or_default(), + access_list: alloy_eips::eip2930::AccessList( + txn.access_list + .to_owned() + .unwrap_or_default() + .0 + .iter() + .map(|a| alloy_eips::eip2930::AccessListItem { + address: a.address, + storage_keys: a.storage_keys.to_owned(), + }) + .collect(), + ), + }; + let to_eip1559 = |tx: &TransactionRequest| TxEip1559 { + chain_id: tx.chain_id.unwrap_or_default().to::(), + nonce: tx.nonce.unwrap_or_default().to::(), + gas_limit: tx.gas.unwrap_or_default().to::(), + max_fee_per_gas: tx.max_fee_per_gas.unwrap_or_default().to::(), + max_priority_fee_per_gas: tx.max_priority_fee_per_gas.unwrap_or_default().to::(), + to: match tx.to { + Some(to) => TxKind::Call(to), + None => TxKind::Create, + }, + value: tx.value.unwrap_or_default(), + input: tx.input.input.to_owned().unwrap_or_default(), + access_list: alloy_eips::eip2930::AccessList( + tx.access_list + .to_owned() + .unwrap_or_default() + .0 + .iter() + .map(|a| alloy_eips::eip2930::AccessListItem { + address: a.address, + storage_keys: a.storage_keys.to_owned(), + }) + .collect(), + ), + }; + // TODO: simplify this... + + Ok(alloy_primitives::Signature::from_scalars_and_parity(r, s, v % 2).map(|sig| match tx + .transaction_type + { + None => TypedTransaction::Legacy(Signed::new_unchecked(to_legacy(tx), sig, hash)), + Some(n) => match n.to::() { + 0 => TypedTransaction::Legacy(Signed::new_unchecked(to_legacy(tx), sig, hash)), + 1 => TypedTransaction::EIP2930(Signed::new_unchecked(to_eip2930(tx), sig, hash)), + 2 => TypedTransaction::EIP1559(Signed::new_unchecked(to_eip1559(tx), sig, hash)), + _ => TypedTransaction::Legacy(Signed::new_unchecked(to_legacy(tx), sig, hash)), + }, + })?) + } /// eth_call for SUAVE MEVM. /// Returns base64-encoded bytestring. @@ -2103,6 +2305,50 @@ impl EthApi { let res = self.call(request, Some(BlockNumber::Latest.into()), None).await?; Ok(BASE64_STANDARD.encode(res.to_vec())) } + + pub async fn suavex_build_eth_block( + &self, + args: Option, + txs: Vec, + ) -> Result { + node_info!("suavex_buildEthBlock"); + let mut valid_txs = vec![]; + for tx in txs { + let tx = self.validate_signed_request(&tx)?; + self.ensure_typed_transaction_supported(&tx)?; + valid_txs.push(tx); + } + let tx_bytes = valid_txs + .iter() + .map(|tx| { + let mut buf = BytesMut::new(); + tx.encode(&mut buf); + buf.to_vec().into() + }) + .collect::>(); + let pool = self.validate_txs(&tx_bytes).await?; + let block = self.backend.pending_block(pool.transactions).await; + Ok(block.execution_envelope(&tx_bytes, args.unwrap_or_default(), pool.fees)) + } + + pub async fn suavex_build_eth_block_from_bundles( + &self, + args: BuildBlockArgs, + bundles: Vec, + ) -> Result { + node_info!("suavex_buildEthBlockFromBundles"); + let mut raw_transactions = Vec::new(); + let mut pool_transactions = Vec::new(); + let mut fees = U256::default(); + for bundle in bundles { + raw_transactions.extend(bundle.transactions.to_owned()); + let pool = self.validate_txs(&bundle.transactions).await?; + fees += pool.fees; + pool_transactions.extend(pool.transactions); + } + let block_info = self.backend.pending_block(pool_transactions).await; + Ok(block_info.execution_envelope(&raw_transactions, args, fees)) + } } // === impl EthApi utility functions === @@ -2171,7 +2417,7 @@ impl EthApi { "not available on past forked blocks".to_string(), )); } - return Ok(fork.estimate_gas(&request, Some(number.into())).await?) + return Ok(fork.estimate_gas(&request, Some(number.into())).await?); } } }