diff --git a/examples/demo-rollup/tests/evm/mod.rs b/examples/demo-rollup/tests/evm/mod.rs index cc867c47ae..cebe97a4d5 100644 --- a/examples/demo-rollup/tests/evm/mod.rs +++ b/examples/demo-rollup/tests/evm/mod.rs @@ -92,17 +92,15 @@ impl TestClient { contract_address: H160, set_arg: u32, ) -> PendingTransaction<'_, Http> { - let nonce = self.eth_get_transaction_count(self.from_addr).await; - + // Tx without gas_limit should estimate and include it in send_transaction endpoint + // Tx without nonce should fetch and include it in send_transaction endpoint let req = Eip1559TransactionRequest::new() .from(self.from_addr) .to(contract_address) .chain_id(self.chain_id) - .nonce(nonce) .data(self.contract.set_call_data(set_arg)) .max_priority_fee_per_gas(10u64) - .max_fee_per_gas(MAX_FEE_PER_GAS) - .gas(900000u64); + .max_fee_per_gas(MAX_FEE_PER_GAS); let typed_transaction = TypedTransaction::Eip1559(req); @@ -148,9 +146,17 @@ impl TestClient { .chain_id(self.chain_id) .nonce(nonce) .data(self.contract.set_call_data(set_arg)) - .gas_price(10u64) - .gas(900000u64); + .gas_price(10u64); + + let typed_transaction = TypedTransaction::Legacy(req.clone()); + // Estimate gas on rpc + let gas = self + .eth_estimate_gas(typed_transaction, Some("latest".to_owned())) + .await; + + // Call with the estimated gas + let req = req.gas(gas); let typed_transaction = TypedTransaction::Legacy(req); let response = self @@ -268,6 +274,16 @@ impl TestClient { .map_err(|e| e.into()) } + async fn eth_estimate_gas(&self, tx: TypedTransaction, block_number: Option) -> u64 { + let gas: ethereum_types::U64 = self + .http_client + .request("eth_estimateGas", rpc_params![tx, block_number]) + .await + .unwrap(); + + gas.as_u64() + } + async fn execute(self) -> Result<(), Box> { // Nonce should be 0 in genesis let nonce = self.eth_get_transaction_count(self.from_addr).await; diff --git a/full-node/sov-ethereum/src/lib.rs b/full-node/sov-ethereum/src/lib.rs index b34e57cb34..264e5420a0 100644 --- a/full-node/sov-ethereum/src/lib.rs +++ b/full-node/sov-ethereum/src/lib.rs @@ -18,9 +18,9 @@ pub mod experimental { use jsonrpsee::types::ErrorObjectOwned; use jsonrpsee::RpcModule; use reth_primitives::{ - Address as RethAddress, TransactionSignedNoHash as RethTransactionSignedNoHash, + Address as RethAddress, TransactionSignedNoHash as RethTransactionSignedNoHash, U128, U256, }; - use reth_rpc_types::{TransactionRequest, TypedTransactionRequest}; + use reth_rpc_types::{CallRequest, TransactionRequest, TypedTransactionRequest}; use sov_evm::{CallMessage, Evm, RlpEvmTransaction}; use sov_modules_api::transaction::Transaction; use sov_modules_api::utils::to_jsonrpsee_error_object; @@ -211,6 +211,8 @@ pub mod experimental { let raw_evm_tx = { let mut working_set = WorkingSet::::new(ethereum.storage.clone()); + + // set nonce if none if transaction_request.nonce.is_none() { let nonce = evm .get_transaction_count(from, None, &mut working_set) @@ -219,27 +221,43 @@ pub mod experimental { transaction_request.nonce = Some(nonce); } + // get current chain id let chain_id = evm .chain_id(&mut working_set) .expect("Failed to get chain id") .map(|id| id.as_u64()) .unwrap_or(1); - // TODO: implement gas logic after gas estimation (#906) is implemented - // https://github.com/Sovereign-Labs/sovereign-sdk/issues/906 + // get call request to estimate gas and gas prices + let (call_request, gas_price, max_fee_per_gas) = + get_call_request_and_params(from, chain_id, &transaction_request); + + // estimate gas limit + let gas_limit = U256::from( + evm.eth_estimate_gas(call_request, None, &mut working_set)? + .as_u64(), + ); + + // get typed transaction request let transaction_request = match transaction_request.into_typed_request() { Some(TypedTransactionRequest::Legacy(mut m)) => { m.chain_id = Some(chain_id); + m.gas_limit = gas_limit; + m.gas_price = gas_price; TypedTransactionRequest::Legacy(m) } Some(TypedTransactionRequest::EIP2930(mut m)) => { m.chain_id = chain_id; + m.gas_limit = gas_limit; + m.gas_price = gas_price; TypedTransactionRequest::EIP2930(m) } Some(TypedTransactionRequest::EIP1559(mut m)) => { m.chain_id = chain_id; + m.gas_limit = gas_limit; + m.max_fee_per_gas = max_fee_per_gas; TypedTransactionRequest::EIP1559(m) } @@ -251,10 +269,12 @@ pub mod experimental { } }; + // get raw transaction let transaction = into_transaction(transaction_request).map_err(|_| { to_jsonrpsee_error_object("Invalid types in transaction request", ETH_RPC_ERROR) })?; + // sign transaction let signed_tx = ethereum .eth_rpc_config .eth_signer @@ -338,4 +358,56 @@ pub mod experimental { let bytes: [u8; 16] = bytes[16..].try_into()?; Ok(u128::from_be_bytes(bytes)) } + + fn get_call_request_and_params( + from: reth_primitives::H160, + chain_id: u64, + transaction_request: &TransactionRequest, + ) -> (CallRequest, U128, U128) { + // TODO: we need an oracle to fetch the gas price of the current chain + // https://github.com/Sovereign-Labs/sovereign-sdk/issues/883 + let gas_price = transaction_request.gas_price.unwrap_or_default(); + let max_fee_per_gas = transaction_request.max_fee_per_gas.unwrap_or_default(); + + // TODO: Generate call request better according to the transaction type + // https://github.com/Sovereign-Labs/sovereign-sdk/issues/946 + let call_request = CallRequest { + from: Some(from), + to: transaction_request.to, + gas: transaction_request.gas, + gas_price: { + if transaction_request.max_priority_fee_per_gas.is_some() { + // eip 1559 + None + } else { + // legacy + Some(U256::from(gas_price)) + } + }, + max_fee_per_gas: Some(U256::from(max_fee_per_gas)), + value: transaction_request.value, + input: transaction_request.data.clone().into(), + nonce: transaction_request.nonce, + chain_id: Some(chain_id.into()), + access_list: transaction_request.access_list.clone(), + max_priority_fee_per_gas: { + if transaction_request.max_priority_fee_per_gas.is_some() { + // eip 1559 + Some(U256::from( + transaction_request + .max_priority_fee_per_gas + .unwrap_or(max_fee_per_gas), + )) + } else { + // legacy + None + } + }, + transaction_type: None, + blob_versioned_hashes: vec![], + max_fee_per_blob_gas: None, + }; + + (call_request, gas_price, max_fee_per_gas) + } } diff --git a/module-system/module-implementations/sov-evm/src/lib.rs b/module-system/module-implementations/sov-evm/src/lib.rs index b256b2ae52..a53d197aa7 100644 --- a/module-system/module-implementations/sov-evm/src/lib.rs +++ b/module-system/module-implementations/sov-evm/src/lib.rs @@ -47,6 +47,10 @@ mod experimental { Block, BlockEnv, Receipt, SealedBlock, TransactionSignedAndRecovered, }; + // Gas per transaction not creating a contract. + pub(crate) const MIN_TRANSACTION_GAS: u64 = 21_000u64; + pub(crate) const MIN_CREATE_GAS: u64 = 53_000u64; + /// Evm account. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct AccountData { diff --git a/module-system/module-implementations/sov-evm/src/query.rs b/module-system/module-implementations/sov-evm/src/query.rs index e2314465ae..183cb87f5e 100644 --- a/module-system/module-implementations/sov-evm/src/query.rs +++ b/module-system/module-implementations/sov-evm/src/query.rs @@ -1,18 +1,24 @@ +use std::array::TryFromSliceError; + use ethereum_types::U64; use jsonrpsee::core::RpcResult; use reth_primitives::contract::create_address; use reth_primitives::TransactionKind::{Call, Create}; use reth_primitives::{TransactionSignedEcRecovered, U128, U256}; +use revm::primitives::{ + EVMError, ExecutionResult, Halt, InvalidTransaction, TransactTo, KECCAK_EMPTY, +}; use sov_modules_api::macros::rpc_gen; use sov_modules_api::WorkingSet; use tracing::info; use crate::call::get_cfg_env; -use crate::error::rpc::ensure_success; +use crate::error::rpc::{ensure_success, RevertError, RpcInvalidTransactionError}; use crate::evm::db::EvmDb; use crate::evm::primitive_types::{BlockEnv, Receipt, SealedBlock, TransactionSignedAndRecovered}; use crate::evm::{executor, prepare_call_env}; -use crate::Evm; +use crate::experimental::{MIN_CREATE_GAS, MIN_TRANSACTION_GAS}; +use crate::{EthApiError, Evm}; #[rpc_gen(client, server, namespace = "eth")] impl Evm { @@ -260,15 +266,177 @@ impl Evm { } /// Handler for: `eth_estimateGas` - // TODO https://github.com/Sovereign-Labs/sovereign-sdk/issues/502 + // https://github.com/paradigmxyz/reth/blob/main/crates/rpc/rpc/src/eth/api/call.rs#L172 #[rpc_method(name = "estimateGas")] pub fn eth_estimate_gas( &self, - _data: reth_rpc_types::CallRequest, - _block_number: Option, - _working_set: &mut WorkingSet, - ) -> RpcResult { - unimplemented!("eth_estimateGas not implemented") + request: reth_rpc_types::CallRequest, + block_number: Option, + working_set: &mut WorkingSet, + ) -> RpcResult { + info!("evm module: eth_estimateGas"); + let mut block_env = match block_number { + Some(ref block_number) if block_number == "pending" => { + self.block_env.get(working_set).unwrap_or_default().clone() + } + _ => { + let block = self.get_sealed_block_by_number(block_number, working_set); + BlockEnv::from(&block) + } + }; + + let tx_env = prepare_call_env(&block_env, request.clone()).unwrap(); + + let cfg = self.cfg.get(working_set).unwrap_or_default(); + let cfg_env = get_cfg_env(&block_env, cfg, Some(get_cfg_env_template())); + + let request_gas = request.gas; + let request_gas_price = request.gas_price; + let env_gas_limit = block_env.gas_limit; + + // get the highest possible gas limit, either the request's set value or the currently + // configured gas limit + let mut highest_gas_limit = request.gas.unwrap_or(U256::from(env_gas_limit)); + + let account = self.accounts.get(&tx_env.caller, working_set).unwrap(); + + // if the request is a simple transfer we can optimize + if tx_env.data.is_empty() { + if let TransactTo::Call(to) = tx_env.transact_to { + let to_account = self.accounts.get(&to, working_set).unwrap(); + if KECCAK_EMPTY == to_account.info.code_hash { + // simple transfer, check if caller has sufficient funds + let available_funds = account.info.balance; + + if tx_env.value > available_funds { + return Err(RpcInvalidTransactionError::InsufficientFundsForTransfer.into()); + } + return Ok(U64::from(MIN_TRANSACTION_GAS)); + } + } + } + + // check funds of the sender + if tx_env.gas_price > U256::ZERO { + // allowance is (balance - tx.value) / tx.gas_price + let allowance = (account.info.balance - tx_env.value) / tx_env.gas_price; + + if highest_gas_limit > allowance { + // cap the highest gas limit by max gas caller can afford with given gas price + highest_gas_limit = allowance; + } + } + + // if the provided gas limit is less than computed cap, use that + let gas_limit = std::cmp::min(U256::from(tx_env.gas_limit), highest_gas_limit); + block_env.gas_limit = convert_u256_to_u64(gas_limit).unwrap(); + + let evm_db = self.get_db(working_set); + + // execute the call without writing to db + let result = executor::inspect(evm_db, &block_env, tx_env.clone(), cfg_env.clone()); + + // Exceptional case: init used too much gas, we need to increase the gas limit and try + // again + if let Err(EVMError::Transaction(InvalidTransaction::CallerGasLimitMoreThanBlock)) = result + { + // if price or limit was included in the request then we can execute the request + // again with the block's gas limit to check if revert is gas related or not + if request_gas.is_some() || request_gas_price.is_some() { + let evm_db = self.get_db(working_set); + return Err(map_out_of_gas_err(block_env, tx_env, cfg_env, evm_db).into()); + } + } + + let result = result.unwrap(); + + match result.result { + ExecutionResult::Success { .. } => { + // succeeded + } + ExecutionResult::Halt { reason, gas_used } => { + return Err(RpcInvalidTransactionError::halt(reason, gas_used).into()) + } + ExecutionResult::Revert { output, .. } => { + // if price or limit was included in the request then we can execute the request + // again with the block's gas limit to check if revert is gas related or not + return if request_gas.is_some() || request_gas_price.is_some() { + let evm_db = self.get_db(working_set); + Err(map_out_of_gas_err(block_env, tx_env, cfg_env, evm_db).into()) + } else { + // the transaction did revert + Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into()) + }; + } + } + + // at this point we know the call succeeded but want to find the _best_ (lowest) gas the + // transaction succeeds with. we find this by doing a binary search over the + // possible range NOTE: this is the gas the transaction used, which is less than the + // transaction requires to succeed + let gas_used = result.result.gas_used(); + // the lowest value is capped by the gas it takes for a transfer + let mut lowest_gas_limit = if tx_env.transact_to.is_create() { + MIN_CREATE_GAS + } else { + MIN_TRANSACTION_GAS + }; + let mut highest_gas_limit: u64 = highest_gas_limit.try_into().unwrap_or(u64::MAX); + // pick a point that's close to the estimated gas + let mut mid_gas_limit = std::cmp::min( + gas_used * 3, + ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64, + ); + // binary search + while (highest_gas_limit - lowest_gas_limit) > 1 { + let mut tx_env = tx_env.clone(); + tx_env.gas_limit = mid_gas_limit; + + let evm_db = self.get_db(working_set); + let result = executor::inspect(evm_db, &block_env, tx_env.clone(), cfg_env.clone()); + + // Exceptional case: init used too much gas, we need to increase the gas limit and try + // again + if let Err(EVMError::Transaction(InvalidTransaction::CallerGasLimitMoreThanBlock)) = + result + { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + + // new midpoint + mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64; + continue; + } + + let result = result.unwrap(); + match result.result { + ExecutionResult::Success { .. } => { + // cap the highest gas limit with succeeding gas limit + highest_gas_limit = mid_gas_limit; + } + ExecutionResult::Revert { .. } => { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + } + ExecutionResult::Halt { reason, .. } => { + match reason { + Halt::OutOfGas(_) => { + // increase the lowest gas limit + lowest_gas_limit = mid_gas_limit; + } + err => { + // these should be unreachable because we know the transaction succeeds, + // but we consider these cases an error + return Err(RpcInvalidTransactionError::EvmHalt(err).into()); + } + } + } + } + // new midpoint + mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64; + } + + Ok(U64::from(highest_gas_limit)) } /// Handler for: `eth_gasPrice` @@ -388,3 +556,32 @@ pub(crate) fn build_rpc_receipt( .collect(), } } + +fn map_out_of_gas_err( + block_env: BlockEnv, + mut tx_env: revm::primitives::TxEnv, + cfg_env: revm::primitives::CfgEnv, + db: EvmDb<'_, C>, +) -> EthApiError { + let req_gas_limit = tx_env.gas_limit; + tx_env.gas_limit = block_env.gas_limit; + let res = executor::inspect(db, &block_env, tx_env, cfg_env).unwrap(); + match res.result { + ExecutionResult::Success { .. } => { + // transaction succeeded by manually increasing the gas limit to + // highest, which means the caller lacks funds to pay for the tx + RpcInvalidTransactionError::BasicOutOfGas(U256::from(req_gas_limit)).into() + } + ExecutionResult::Revert { output, .. } => { + // reverted again after bumping the limit + RpcInvalidTransactionError::Revert(RevertError::new(output)).into() + } + ExecutionResult::Halt { reason, .. } => RpcInvalidTransactionError::EvmHalt(reason).into(), + } +} + +fn convert_u256_to_u64(u256: reth_primitives::U256) -> Result { + let bytes: [u8; 32] = u256.to_be_bytes(); + let bytes: [u8; 8] = bytes[24..].try_into()?; + Ok(u64::from_be_bytes(bytes)) +}