From 666b70f70aa5cb2ae84a1b4f87493a57c1c51233 Mon Sep 17 00:00:00 2001
From: orkunkilic <orkunmahirkilic@gmail.com>
Date: Fri, 29 Sep 2023 12:18:13 +0300
Subject: [PATCH] estimate-gas signed

---
 examples/demo-rollup/tests/evm/mod.rs         |  30 ++-
 full-node/sov-ethereum/src/lib.rs             |  80 ++++++-
 .../module-implementations/sov-evm/src/lib.rs |   4 +
 .../sov-evm/src/query.rs                      | 213 +++++++++++++++++-
 4 files changed, 308 insertions(+), 19 deletions(-)

diff --git a/examples/demo-rollup/tests/evm/mod.rs b/examples/demo-rollup/tests/evm/mod.rs
index cc867c47a..cebe97a4d 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<String>) -> 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<dyn std::error::Error>> {
         // 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 b34e57cb3..264e5420a 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::<C>::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 b256b2ae5..a53d197aa 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 e2314465a..183cb87f5 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<C: sov_modules_api::Context> Evm<C> {
@@ -260,15 +266,177 @@ impl<C: sov_modules_api::Context> Evm<C> {
     }
 
     /// 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<reth_primitives::BlockId>,
-        _working_set: &mut WorkingSet<C>,
-    ) -> RpcResult<reth_primitives::U256> {
-        unimplemented!("eth_estimateGas not implemented")
+        request: reth_rpc_types::CallRequest,
+        block_number: Option<String>,
+        working_set: &mut WorkingSet<C>,
+    ) -> RpcResult<reth_primitives::U64> {
+        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<C: sov_modules_api::Context>(
+    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<u64, TryFromSliceError> {
+    let bytes: [u8; 32] = u256.to_be_bytes();
+    let bytes: [u8; 8] = bytes[24..].try_into()?;
+    Ok(u64::from_be_bytes(bytes))
+}