From 174e0ab654a7d2e2c85c43b9b23119e62838ff31 Mon Sep 17 00:00:00 2001 From: Jouzo <15011228+Jouzo@users.noreply.github.com> Date: Fri, 9 Jun 2023 16:39:20 +0200 Subject: [PATCH] Deduct EVM fee (#2039) * Deduct fee from sender * Prevalidate balance for MIN_GAS_PER_TX * Additional check to prevalidate tx and tests --- lib/ain-evm/src/backend.rs | 33 +++++- lib/ain-evm/src/evm.rs | 42 +++++-- lib/ain-evm/src/executor.rs | 103 ++++++++++++----- lib/ain-evm/src/fee.rs | 11 ++ lib/ain-evm/src/handler.rs | 18 +-- lib/ain-evm/src/lib.rs | 1 + lib/ain-evm/src/traits.rs | 6 +- lib/ain-evm/src/transaction/mod.rs | 2 +- test/functional/feature_evm.py | 2 +- test/functional/feature_evm_fee.py | 175 +++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 11 files changed, 340 insertions(+), 54 deletions(-) create mode 100644 lib/ain-evm/src/fee.rs create mode 100755 test/functional/feature_evm_fee.py diff --git a/lib/ain-evm/src/backend.rs b/lib/ain-evm/src/backend.rs index 5a910861ec..b7b0e10962 100644 --- a/lib/ain-evm/src/backend.rs +++ b/lib/ain-evm/src/backend.rs @@ -130,6 +130,37 @@ impl EVMBackend { ..self.vicinity }; } + + pub fn deduct_prepay_gas(&mut self, sender: H160, prepay_gas: U256) { + trace!(target: "backend", "[deduct_prepay_gas] Deducting {:#x} from {:#x}", prepay_gas, sender); + + let basic = self.basic(sender); + let balance = basic.balance.saturating_sub(prepay_gas); + let new_basic = Basic { balance, ..basic }; + self.apply(sender, new_basic, None, Vec::new(), false) + .expect("Error deducting account balance"); + } + + pub fn refund_unused_gas( + &mut self, + sender: H160, + gas_limit: U256, + used_gas: U256, + gas_price: U256, + ) { + let refund_gas = gas_limit.saturating_sub(used_gas); + let refund_amount = refund_gas.saturating_mul(gas_price); + + trace!(target: "backend", "[refund_unused_gas] Refunding {:#x} to {:#x}", refund_amount, sender); + + let basic = self.basic(sender); + let new_basic = Basic { + balance: basic.balance.saturating_add(refund_amount), + ..basic + }; + self.apply(sender, new_basic, None, Vec::new(), false) + .expect("Error refunding account balance"); + } } impl EVMBackend { @@ -284,7 +315,6 @@ impl BridgeBackend for EVMBackend { }; self.apply(address, new_basic, None, Vec::new(), false)?; - self.state.commit(); Ok(()) } @@ -306,7 +336,6 @@ impl BridgeBackend for EVMBackend { }; self.apply(address, new_basic, None, Vec::new(), false)?; - self.state.commit(); Ok(()) } } diff --git a/lib/ain-evm/src/evm.rs b/lib/ain-evm/src/evm.rs index e3418f3e06..9333c305af 100644 --- a/lib/ain-evm/src/evm.rs +++ b/lib/ain-evm/src/evm.rs @@ -1,5 +1,6 @@ use crate::backend::{EVMBackend, EVMBackendError, InsufficientBalance, Vicinity}; use crate::executor::TxResponse; +use crate::fee::calculate_prepay_gas; use crate::storage::traits::{BlockStorage, PersistentState, PersistentStateError}; use crate::storage::Storage; use crate::transaction::bridge::{BalanceUpdate, BridgeTx}; @@ -124,17 +125,14 @@ impl EVMHandler { vicinity, ) .map_err(|e| anyhow!("------ Could not restore backend {}", e))?; - Ok(AinExecutor::new(&mut backend).call( - ExecutorContext { - caller, - to, - value, - data, - gas_limit, - access_list, - }, - false, - )) + Ok(AinExecutor::new(&mut backend).call(ExecutorContext { + caller, + to, + value, + data, + gas_limit, + access_list, + })) } pub fn validate_raw_tx(&self, tx: &str) -> Result> { @@ -175,6 +173,28 @@ impl EVMHandler { .into()); } + const MIN_GAS_PER_TX: U256 = U256([21_000, 0, 0, 0]); + let balance = self + .get_balance(signed_tx.sender, block_number) + .map_err(|e| anyhow!("Error getting balance {e}"))?; + + debug!("[validate_raw_tx] Accout balance : {:x?}", balance); + + let prepay_gas = calculate_prepay_gas(&signed_tx); + if balance < MIN_GAS_PER_TX || balance < prepay_gas { + debug!("[validate_raw_tx] Insufficiant balance to pay fees"); + return Err(anyhow!("Insufficiant balance to pay fees").into()); + } + + let gas_limit = U256::from(signed_tx.gas_limit()); + + // TODO lift MAX_GAS_PER_BLOCK + const MAX_GAS_PER_BLOCK: U256 = U256([30_000_000, 0, 0, 0]); + println!("MAX_GAS_PER_BLOCK : {:#x}", MAX_GAS_PER_BLOCK); + if gas_limit > MAX_GAS_PER_BLOCK { + return Err(anyhow!("Gas limit higher than MAX_GAS_PER_BLOCK").into()); + } + Ok(signed_tx) } diff --git a/lib/ain-evm/src/executor.rs b/lib/ain-evm/src/executor.rs index 7b160ea4de..b47b1f2ef4 100644 --- a/lib/ain-evm/src/executor.rs +++ b/lib/ain-evm/src/executor.rs @@ -1,6 +1,7 @@ use crate::{ backend::{EVMBackend, EVMBackendError}, evm::EVMHandler, + fee::calculate_prepay_gas, traits::{BridgeBackend, Executor, ExecutorContext}, transaction::SignedTx, }; @@ -42,7 +43,8 @@ impl<'backend> AinExecutor<'backend> { impl<'backend> Executor for AinExecutor<'backend> { const CONFIG: Config = Config::shanghai(); - fn call(&mut self, ctx: ExecutorContext, apply: bool) -> TxResponse { + /// Read-only call + fn call(&mut self, ctx: ExecutorContext) -> TxResponse { let metadata = StackSubstateMetadata::new(ctx.gas_limit, &Self::CONFIG); let state = MemoryStackState::new(metadata, self.backend); let precompiles = BTreeMap::new(); // TODO Add precompile crate @@ -52,6 +54,63 @@ impl<'backend> Executor for AinExecutor<'backend> { .into_iter() .map(|x| (x.address, x.storage_keys)) .collect::>(); + + let (exit_reason, data) = match ctx.to { + Some(address) => executor.transact_call( + ctx.caller.unwrap_or_default(), + address, + ctx.value, + ctx.data.to_vec(), + ctx.gas_limit, + access_list, + ), + None => executor.transact_create( + ctx.caller.unwrap_or_default(), + ctx.value, + ctx.data.to_vec(), + ctx.gas_limit, + access_list, + ), + }; + + TxResponse { + exit_reason, + data, + logs: Vec::new(), + used_gas: executor.used_gas(), + } + } + + /// Update state + fn exec(&mut self, signed_tx: &SignedTx) -> (TxResponse, ReceiptV3) { + self.backend.update_vicinity_from_tx(signed_tx); + trace!( + "[Executor] Executing EVM TX with vicinity : {:?}", + self.backend.vicinity + ); + let ctx = ExecutorContext { + caller: Some(signed_tx.sender), + to: signed_tx.to(), + value: signed_tx.value(), + data: signed_tx.data(), + gas_limit: signed_tx.gas_limit().as_u64(), + access_list: signed_tx.access_list(), + }; + + let prepay_gas = calculate_prepay_gas(&signed_tx); + + self.backend.deduct_prepay_gas(signed_tx.sender, prepay_gas); + + let metadata = StackSubstateMetadata::new(ctx.gas_limit, &Self::CONFIG); + let state = MemoryStackState::new(metadata, self.backend); + let precompiles = BTreeMap::new(); // TODO Add precompile crate + let mut executor = StackExecutor::new_with_precompiles(state, &Self::CONFIG, &precompiles); + let access_list = ctx + .access_list + .into_iter() + .map(|x| (x.address, x.storage_keys)) + .collect::>(); + let (exit_reason, data) = match ctx.to { Some(address) => executor.transact_call( ctx.caller.unwrap_or_default(), @@ -73,10 +132,17 @@ impl<'backend> Executor for AinExecutor<'backend> { let used_gas = executor.used_gas(); let (values, logs) = executor.into_state().deconstruct(); let logs = logs.into_iter().collect::>(); - if apply && exit_reason.is_succeed() { + if exit_reason.is_succeed() { ApplyBackend::apply(self.backend, values, logs.clone(), true); } + self.backend.refund_unused_gas( + signed_tx.sender, + signed_tx.gas_limit(), + U256::from(used_gas), + signed_tx.gas_price(), + ); + let receipt = ReceiptV3::EIP1559(EIP658ReceiptData { logs_bloom: { let mut bloom: Bloom = Bloom::default(); @@ -88,32 +154,14 @@ impl<'backend> Executor for AinExecutor<'backend> { used_gas: U256::from(used_gas), }); - TxResponse { - exit_reason, - data, - logs, - used_gas, - receipt, - } - } - - fn exec(&mut self, signed_tx: &SignedTx) -> TxResponse { - let apply = true; - self.backend.update_vicinity_from_tx(signed_tx); - trace!( - "[Executor] Executing EVM TX with vicinity : {:?}", - self.backend.vicinity - ); - self.call( - ExecutorContext { - caller: Some(signed_tx.sender), - to: signed_tx.to(), - value: signed_tx.value(), - data: signed_tx.data(), - gas_limit: signed_tx.gas_limit().as_u64(), - access_list: signed_tx.access_list(), + ( + TxResponse { + exit_reason, + data, + logs, + used_gas, }, - apply, + receipt, ) } } @@ -124,5 +172,4 @@ pub struct TxResponse { pub data: Vec, pub logs: Vec, pub used_gas: u64, - pub receipt: ReceiptV3, } diff --git a/lib/ain-evm/src/fee.rs b/lib/ain-evm/src/fee.rs new file mode 100644 index 0000000000..39e6325c4d --- /dev/null +++ b/lib/ain-evm/src/fee.rs @@ -0,0 +1,11 @@ +use ethereum_types::U256; + +use crate::transaction::SignedTx; + +pub fn calculate_prepay_gas(signed_tx: &SignedTx) -> U256 { + match &signed_tx.transaction { + ethereum::TransactionV2::Legacy(tx) => tx.gas_limit.saturating_mul(tx.gas_price), + ethereum::TransactionV2::EIP2930(tx) => tx.gas_limit.saturating_mul(tx.gas_price), + ethereum::TransactionV2::EIP1559(tx) => tx.gas_limit.saturating_mul(tx.max_fee_per_gas), + } +} diff --git a/lib/ain-evm/src/handler.rs b/lib/ain-evm/src/handler.rs index dde09b6d99..5fc763e952 100644 --- a/lib/ain-evm/src/handler.rs +++ b/lib/ain-evm/src/handler.rs @@ -100,13 +100,15 @@ impl Handlers { for (queue_tx, hash) in self.evm.tx_queues.get_cloned_vec(context) { match queue_tx { QueueTx::SignedTx(signed_tx) => { - let TxResponse { - exit_reason, - logs, - used_gas, + let ( + TxResponse { + exit_reason, + logs, + used_gas, + .. + }, receipt, - .. - } = executor.exec(&signed_tx); + ) = executor.exec(&signed_tx); debug!( "receipt : {:#?} for signed_tx : {:#x}", receipt, @@ -122,8 +124,6 @@ impl Handlers { gas_used += used_gas; EVMHandler::logs_bloom(logs, &mut logs_bloom); receipts_v3.push(receipt); - - executor.commit(); } QueueTx::BridgeTx(BridgeTx::EvmIn(BalanceUpdate { address, amount })) => { debug!( @@ -147,6 +147,8 @@ impl Handlers { } } } + + executor.commit(); } if update_state { diff --git a/lib/ain-evm/src/lib.rs b/lib/ain-evm/src/lib.rs index f9e24b4227..74815b6fbc 100644 --- a/lib/ain-evm/src/lib.rs +++ b/lib/ain-evm/src/lib.rs @@ -3,6 +3,7 @@ mod block; mod ecrecover; pub mod evm; pub mod executor; +mod fee; pub mod handler; pub mod receipt; pub mod runtime; diff --git a/lib/ain-evm/src/traits.rs b/lib/ain-evm/src/traits.rs index 6f821df4f9..3fab8b4cb7 100644 --- a/lib/ain-evm/src/traits.rs +++ b/lib/ain-evm/src/traits.rs @@ -1,5 +1,5 @@ use crate::{backend::EVMBackendError, executor::TxResponse, transaction::SignedTx}; -use ethereum::AccessList; +use ethereum::{AccessList, ReceiptV3}; use evm::Config; use primitive_types::{H160, U256}; @@ -16,9 +16,9 @@ pub struct ExecutorContext<'a> { pub trait Executor { const CONFIG: Config = Config::shanghai(); - fn call(&mut self, ctx: ExecutorContext, apply: bool) -> TxResponse; + fn call(&mut self, ctx: ExecutorContext) -> TxResponse; - fn exec(&mut self, tx: &SignedTx) -> TxResponse; + fn exec(&mut self, tx: &SignedTx) -> (TxResponse, ReceiptV3); } pub trait BridgeBackend { diff --git a/lib/ain-evm/src/transaction/mod.rs b/lib/ain-evm/src/transaction/mod.rs index d15f21057b..c0f11ddd57 100644 --- a/lib/ain-evm/src/transaction/mod.rs +++ b/lib/ain-evm/src/transaction/mod.rs @@ -231,7 +231,7 @@ impl SignedTx { match &self.transaction { TransactionV2::Legacy(tx) => tx.gas_price, TransactionV2::EIP2930(tx) => tx.gas_price, - TransactionV2::EIP1559(tx) => tx.max_fee_per_gas.min(tx.max_priority_fee_per_gas), // TODO verify calculation + TransactionV2::EIP1559(tx) => tx.max_fee_per_gas, } } diff --git a/test/functional/feature_evm.py b/test/functional/feature_evm.py index a9da18a3b8..b2da3bcbbd 100755 --- a/test/functional/feature_evm.py +++ b/test/functional/feature_evm.py @@ -305,7 +305,7 @@ def run_test(self): assert_equal(block_txs[4], tx4) # Check Eth balances before transfer - assert_equal(int(self.nodes[0].eth_getBalance(ethAddress)[2:], 16), 6000000000000000000) + assert_equal(int(self.nodes[0].eth_getBalance(ethAddress)[2:], 16), 5998236000000000000) assert_equal(int(self.nodes[0].eth_getBalance(to_address)[2:], 16), 4000000000000000000) # Check miner account balance after transfer diff --git a/test/functional/feature_evm_fee.py b/test/functional/feature_evm_fee.py new file mode 100755 index 0000000000..78f75ded0b --- /dev/null +++ b/test/functional/feature_evm_fee.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) DeFi Blockchain Developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. +"""Test EVM behaviour""" + +from test_framework.test_framework import DefiTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error +) + +class EVMFeeTest(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [ + ['-dummypos=0', '-txnotokens=0', '-amkheight=50', '-bayfrontheight=51', '-eunosheight=80', '-fortcanningheight=82', '-fortcanninghillheight=84', '-fortcanningroadheight=86', '-fortcanningcrunchheight=88', '-fortcanningspringheight=90', '-fortcanninggreatworldheight=94', '-fortcanningepilogueheight=96', '-grandcentralheight=101', '-nextnetworkupgradeheight=105', '-subsidytest=1', '-txindex=1'], + ] + + def setup(self): + self.address = self.nodes[0].get_genesis_keys().ownerAuthAddress + self.ethAddress = '0x9b8a4af42140d8a4c153a822f02571a1dd037e89' + self.toAddress = '0x6c34cbb9219d8caa428835d2073e8ec88ba0a110' + self.nodes[0].importprivkey('af990cc3ba17e776f7f57fcc59942a82846d75833fa17d2ba59ce6858d886e23') # ethAddress + self.nodes[0].importprivkey('17b8cb134958b3d8422b6c43b0732fcdb8c713b524df2d45de12f0c7e214ba35') # toAddress + + # Generate chain + self.nodes[0].generate(101) + + assert_raises_rpc_error(-32600, "called before NextNetworkUpgrade height", self.nodes[0].evmtx, self.ethAddress, 0, 21, 21000, self.toAddress, 0.1) + + # Move to fork height + self.nodes[0].generate(4) + + self.nodes[0].getbalance() + self.nodes[0].utxostoaccount({self.address: "201@DFI"}) + self.nodes[0].setgov({"ATTRIBUTES": {'v0/params/feature/evm': 'true'}}) + self.nodes[0].generate(1) + + def test_fee_deduction(self): + height = self.nodes[0].getblockcount() + + balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") + assert_equal(int(balance[2:], 16), 100000000000000000000) + + self.nodes[0].eth_sendTransaction({ + 'from': self.ethAddress, + 'to': self.toAddress, + 'value': '0x7148', # 29_000 + 'gas': '0x7a120', + 'gasPrice': '0x1', + }) + self.nodes[0].generate(1) + + balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") + # Deduct 50000. 29000 value + min 21000 call fee + assert_equal(int(balance[2:], 16), 99999999999999950000) + + self.rollback_to(height) + + def test_high_gas_price(self): + height = self.nodes[0].getblockcount() + + balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") + assert_equal(int(balance[2:], 16), 100000000000000000000) + + self.nodes[0].eth_sendTransaction({ + 'from': self.ethAddress, + 'to': self.toAddress, + 'value': '0x7148', # 29_000 + 'gas': '0x7a120', + 'gasPrice': '0x3B9ACA00', # 1_000_000_000 + }) + self.nodes[0].generate(1) + + balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") + # Deduct 21_000_000_029_000. 29_000 value + 21_000 * 1_000_000_000 + assert_equal(int(balance[2:], 16), 99999978999999971000) + + self.rollback_to(height) + + def test_max_gas_price(self): + height = self.nodes[0].getblockcount() + + balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") + assert_equal(int(balance[2:], 16), 100000000000000000000) + + assert_raises_rpc_error(-32001, "evm tx failed to validate Insufficiant balance to pay fees", self.nodes[0].eth_sendTransaction, { + 'from': self.ethAddress, + 'to': self.toAddress, + 'value': '0x7148', # 29_000 + 'gas': '0x7a120', + 'gasPrice': '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + }) + + self.rollback_to(height) + + def test_gas_limit_higher_than_block_limit(self): + height = self.nodes[0].getblockcount() + + balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") + assert_equal(int(balance[2:], 16), 100000000000000000000) + + assert_raises_rpc_error(-32001, "evm tx failed to validate Gas limit higher than MAX_GAS_PER_BLOCK", self.nodes[0].eth_sendTransaction, { + 'from': self.ethAddress, + 'to': self.toAddress, + 'value': '0x7148', # 29_000 + 'gas': '0x1C9C381', # 30_000_001 + 'gasPrice': '0x1', + }) + + self.rollback_to(height) + + def test_fee_deduction_empty_balance(self): + height = self.nodes[0].getblockcount() + + emptyAddress = self.nodes[0].getnewaddress("", "eth") + balance = self.nodes[0].eth_getBalance(emptyAddress, "latest") + assert_equal(int(balance[2:], 16), 000000000000000000000) + + assert_raises_rpc_error(-32001, "evm tx failed to validate Insufficiant balance to pay fees", self.nodes[0].eth_sendTransaction, { + 'from': emptyAddress, + 'to': self.toAddress, + 'value': '0x7148', # 29_000 + 'gas': '0x7a120', + 'gasPrice': '0x1', + }) + + self.rollback_to(height) + + def test_fee_deduction_send_full_balance(self): + height = self.nodes[0].getblockcount() + + balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") + assert_equal(int(balance[2:], 16), 100000000000000000000) + + self.nodes[0].eth_sendTransaction({ + 'from': self.ethAddress, + 'to': self.toAddress, + 'value': balance, + 'gas': '0x7a120', + 'gasPrice': '0x1', + }) + self.nodes[0].generate(1) + + balance = self.nodes[0].eth_getBalance(self.ethAddress, "latest") + + # Don't consume balance as not enough to cover send value + fee. + # Deduct only 21000 call fee + assert_equal(int(balance[2:], 16), 99999999999999979000) + + self.rollback_to(height) + + def run_test(self): + self.setup() + + self.nodes[0].transferdomain([{"src": {"address":self.address, "amount":"100@DFI", "domain": 2}, "dst":{"address":self.ethAddress, "amount":"100@DFI", "domain": 3}}]) + self.nodes[0].generate(1) + + self.test_fee_deduction() + + self.test_high_gas_price() + + self.test_max_gas_price() + + self.test_gas_limit_higher_than_block_limit() + + self.test_fee_deduction_empty_balance() + + self.test_fee_deduction_send_full_balance() + +if __name__ == '__main__': + EVMFeeTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 2bece828d7..1004514281 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -293,6 +293,7 @@ 'feature_loan.py', 'feature_evm.py', 'feature_evm_rpc.py', + 'feature_evm_fee.py', 'feature_evm_rollback.py', 'feature_evm_rpc_transaction.py', 'feature_evm_smart_contract.py',