From 76382fc5c037cd675236d45bb9cda509cfd834ee Mon Sep 17 00:00:00 2001 From: Roman Behma <13855864+begmaroman@users.noreply.github.com> Date: Wed, 4 Oct 2023 14:58:02 +0800 Subject: [PATCH] Skip account balance check for eth_call (#1949) --- e2e-polybft/e2e/jsonrpc_test.go | 106 ++++++++++++++++-- jsonrpc/eth_blockchain_test.go | 2 +- jsonrpc/eth_endpoint.go | 11 +- jsonrpc/eth_state_test.go | 2 +- server/server.go | 3 + state/executor.go | 13 ++- state/runtime/evm/evm_fuzz_test.go | 4 + state/runtime/evm/evm_test.go | 4 + .../precompiled/native_transfer_test.go | 4 + state/runtime/runtime.go | 2 + 10 files changed, 132 insertions(+), 19 deletions(-) diff --git a/e2e-polybft/e2e/jsonrpc_test.go b/e2e-polybft/e2e/jsonrpc_test.go index c4357c462c..9f03a26926 100644 --- a/e2e-polybft/e2e/jsonrpc_test.go +++ b/e2e-polybft/e2e/jsonrpc_test.go @@ -5,14 +5,15 @@ import ( "math/big" "testing" + "github.com/stretchr/testify/require" + "github.com/umbracle/ethgo" + "github.com/umbracle/ethgo/jsonrpc" + "github.com/umbracle/ethgo/wallet" + "github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi" "github.com/0xPolygon/polygon-edge/e2e-polybft/framework" "github.com/0xPolygon/polygon-edge/helper/hex" "github.com/0xPolygon/polygon-edge/types" - "github.com/stretchr/testify/require" - "github.com/umbracle/ethgo" - "github.com/umbracle/ethgo/abi" - "github.com/umbracle/ethgo/wallet" ) var ( @@ -31,7 +32,8 @@ func TestE2E_JsonRPC(t *testing.T) { cluster.WaitForReady(t) - client := cluster.Servers[0].JSONRPC().Eth() + jsonRPC := cluster.Servers[0].JSONRPC() + client := jsonRPC.Eth() // Test eth_call with override in state diff t.Run("eth_call state override", func(t *testing.T) { @@ -41,7 +43,7 @@ func TestE2E_JsonRPC(t *testing.T) { target := deployTxn.Receipt().ContractAddress - input := abi.MustNewMethod("function getValue() public returns (uint256)").ID() + input := contractsapi.TestSimple.Abi.GetMethod("getValue").ID() resp, err := client.Call(ðgo.CallMsg{To: &target, Data: input}, ethgo.Latest) require.NoError(t, err) @@ -62,11 +64,72 @@ func TestE2E_JsonRPC(t *testing.T) { require.Equal(t, "0x0300000000000000000000000000000000000000000000000000000000000000", resp) }) + // Test eth_call with zero account balance + t.Run("eth_call with zero-balance account", func(t *testing.T) { + deployTxn := cluster.Deploy(t, acct, contractsapi.TestSimple.Bytecode) + require.NoError(t, deployTxn.Wait()) + require.True(t, deployTxn.Succeed()) + + target := deployTxn.Receipt().ContractAddress + + input := contractsapi.TestSimple.Abi.GetMethod("getValue").ID() + + acctZeroBalance, err := wallet.GenerateKey() + require.NoError(t, err) + + resp, err := client.Call(ðgo.CallMsg{ + From: acctZeroBalance.Address(), + To: &target, + Data: input, + }, ethgo.Latest) + require.NoError(t, err) + require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", resp) + }) + + t.Run("eth_estimateGas", func(t *testing.T) { + deployTxn := cluster.Deploy(t, acct, contractsapi.TestSimple.Bytecode) + require.NoError(t, deployTxn.Wait()) + require.True(t, deployTxn.Succeed()) + + target := deployTxn.Receipt().ContractAddress + + input := contractsapi.TestSimple.Abi.GetMethod("getValue").ID() + + estimatedGas, err := client.EstimateGas(ðgo.CallMsg{ + From: acct.Address(), + To: &target, + Data: input, + }) + require.NoError(t, err) + require.Equal(t, uint64(0x56a3), estimatedGas) + }) + + t.Run("eth_estimateGas by zero-balance account", func(t *testing.T) { + deployTxn := cluster.Deploy(t, acct, contractsapi.TestSimple.Bytecode) + require.NoError(t, deployTxn.Wait()) + require.True(t, deployTxn.Succeed()) + + target := deployTxn.Receipt().ContractAddress + + input := contractsapi.TestSimple.Abi.GetMethod("getValue").ID() + + acctZeroBalance, err := wallet.GenerateKey() + require.NoError(t, err) + + resp, err := client.EstimateGas(ðgo.CallMsg{ + From: acctZeroBalance.Address(), + To: &target, + Data: input, + }) + require.NoError(t, err) + require.Equal(t, uint64(0x56a3), resp) + }) + t.Run("eth_getBalance", func(t *testing.T) { key1, err := wallet.GenerateKey() require.NoError(t, err) - // Test. return zero if the account does not exists + // Test. return zero if the account does not exist balance1, err := client.GetBalance(key1.Address(), ethgo.Latest) require.NoError(t, err) require.Equal(t, balance1, big.NewInt(0)) @@ -86,13 +149,12 @@ func TestE2E_JsonRPC(t *testing.T) { require.NoError(t, err) toAddr := key1.Address() - msg := ðgo.CallMsg{ + + estimatedGas, err := client.EstimateGas(ðgo.CallMsg{ From: acct.Address(), To: &toAddr, Value: newBalance, - } - - estimatedGas, err := client.EstimateGas(msg) + }) require.NoError(t, err) txPrice := gasPrice * estimatedGas // subtract gasPrice * estimatedGas from the balance and transfer the rest to the other account @@ -163,7 +225,7 @@ func TestE2E_JsonRPC(t *testing.T) { require.NoError(t, err) require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000000", resp.String()) - setValueFn := abi.MustNewMethod("function setValue(uint256 _val) public") + setValueFn := contractsapi.TestSimple.Abi.GetMethod("setValue") newVal := big.NewInt(1) @@ -305,4 +367,24 @@ func TestE2E_JsonRPC(t *testing.T) { // Test. The dynamic 'from' field is populated require.NotEqual(t, ethTxn.From, ethgo.ZeroAddress) }) + + t.Run("debug_traceTransaction", func(t *testing.T) { + key1, err := wallet.GenerateKey() + require.NoError(t, err) + + // Test. We should be able to query the transaction by its hash + txn := cluster.Transfer(t, acct, types.Address(key1.Address()), one) + require.NoError(t, txn.Wait()) + require.True(t, txn.Succeed()) + + txReceipt := txn.Receipt() + + // Use a wrapper function from "jsonrpc" package when the config is introduced. + var trace *jsonrpc.TransactionTrace + err = jsonRPC.Call("debug_traceTransaction", &trace, txReceipt.TransactionHash, map[string]interface{}{ + "tracer": "callTracer", + }) + require.NoError(t, err) + require.Equal(t, txReceipt.GasUsed, trace.Gas) + }) } diff --git a/jsonrpc/eth_blockchain_test.go b/jsonrpc/eth_blockchain_test.go index 62a8cac1c1..aa547e8250 100644 --- a/jsonrpc/eth_blockchain_test.go +++ b/jsonrpc/eth_blockchain_test.go @@ -628,7 +628,7 @@ func (m *mockBlockStore) GetAvgGasPrice() *big.Int { return big.NewInt(m.averageGasPrice) } -func (m *mockBlockStore) ApplyTxn(header *types.Header, txn *types.Transaction, overrides types.StateOverride) (*runtime.ExecutionResult, error) { +func (m *mockBlockStore) ApplyTxn(_ *types.Header, _ *types.Transaction, _ types.StateOverride, _ bool) (*runtime.ExecutionResult, error) { return &runtime.ExecutionResult{ Err: m.ethCallError, ReturnValue: m.returnValue, diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index ed0919e726..b6d3613ea6 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -73,7 +73,12 @@ type ethBlockchainStore interface { GetAvgGasPrice() *big.Int // ApplyTxn applies a transaction object to the blockchain - ApplyTxn(header *types.Header, txn *types.Transaction, override types.StateOverride) (*runtime.ExecutionResult, error) + ApplyTxn( + header *types.Header, + txn *types.Transaction, + override types.StateOverride, + nonPayable bool, + ) (*runtime.ExecutionResult, error) // GetSyncProgression retrieves the current sync progression, if any GetSyncProgression() *progress.Progression @@ -526,7 +531,7 @@ func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash, apiOverride *stateOve } // The return value of the execution is saved in the transition (returnValue field) - result, err := e.store.ApplyTxn(header, transaction, override) + result, err := e.store.ApplyTxn(header, transaction, override, true) if err != nil { return nil, err } @@ -674,7 +679,7 @@ func (e *Eth) EstimateGas(arg *txnArgs, rawNum *BlockNumber) (interface{}, error transaction.Gas = gas - result, applyErr := e.store.ApplyTxn(header, transaction, nil) + result, applyErr := e.store.ApplyTxn(header, transaction, nil, false) if result != nil { data = []byte(hex.EncodeToString(result.ReturnValue)) diff --git a/jsonrpc/eth_state_test.go b/jsonrpc/eth_state_test.go index 56bd2cb015..a027ad8507 100644 --- a/jsonrpc/eth_state_test.go +++ b/jsonrpc/eth_state_test.go @@ -870,7 +870,7 @@ func (m *mockSpecialStore) GetForksInTime(blockNumber uint64) chain.ForksInTime return chain.ForksInTime{} } -func (m *mockSpecialStore) ApplyTxn(header *types.Header, txn *types.Transaction, overrides types.StateOverride) (*runtime.ExecutionResult, error) { +func (m *mockSpecialStore) ApplyTxn(header *types.Header, txn *types.Transaction, _ types.StateOverride, _ bool) (*runtime.ExecutionResult, error) { if m.applyTxnHook != nil { return m.applyTxnHook(header, txn) } diff --git a/server/server.go b/server/server.go index 60e31a1f3f..fd45323648 100644 --- a/server/server.go +++ b/server/server.go @@ -775,6 +775,7 @@ func (j *jsonRPCHub) ApplyTxn( header *types.Header, txn *types.Transaction, override types.StateOverride, + nonPayable bool, ) (result *runtime.ExecutionResult, err error) { blockCreator, err := j.GetConsensus().GetBlockCreator(header) if err != nil { @@ -792,6 +793,8 @@ func (j *jsonRPCHub) ApplyTxn( } } + transition.SetNonPayable(nonPayable) + result, err = transition.Apply(txn) return diff --git a/state/executor.go b/state/executor.go index 398895f43c..730bc74c32 100644 --- a/state/executor.go +++ b/state/executor.go @@ -1044,6 +1044,11 @@ func (t *Transition) SetCodeDirectly(addr types.Address, code []byte) error { return nil } +// SetNonPayable deactivates the check of tx cost against tx executor balance. +func (t *Transition) SetNonPayable(nonPayable bool) { + t.ctx.NonPayable = nonPayable +} + // SetTracer sets tracer to the context in order to enable it func (t *Transition) SetTracer(tracer tracer.Tracer) { t.ctx.Tracer = tracer @@ -1117,8 +1122,12 @@ func checkAndProcessTx(msg *types.Transaction, t *Transition) error { } // 3. caller has enough balance to cover transaction - if err := t.subGasLimitPrice(msg); err != nil { - return NewTransitionApplicationError(err, true) + // Skip this check if the given flag is provided. + // It happens for eth_call and for other operations that do not change the state. + if !t.ctx.NonPayable { + if err := t.subGasLimitPrice(msg); err != nil { + return NewTransitionApplicationError(err, true) + } } return nil diff --git a/state/runtime/evm/evm_fuzz_test.go b/state/runtime/evm/evm_fuzz_test.go index 31a8b66458..c7c1889d4b 100644 --- a/state/runtime/evm/evm_fuzz_test.go +++ b/state/runtime/evm/evm_fuzz_test.go @@ -61,6 +61,10 @@ func (m *mockHostF) SetState(addr types.Address, key types.Hash, value types.Has return } +func (m *mockHostF) SetNonPayable(nonPayable bool) { + return +} + func (m *mockHostF) GetBalance(addr types.Address) *big.Int { if b, ok := m.balances[addr]; !ok { m.balances[addr] = big.NewInt(0) diff --git a/state/runtime/evm/evm_test.go b/state/runtime/evm/evm_test.go index f22900a968..e5ad31c878 100644 --- a/state/runtime/evm/evm_test.go +++ b/state/runtime/evm/evm_test.go @@ -54,6 +54,10 @@ func (m *mockHost) SetStorage( panic("Not implemented in tests") //nolint:gocritic } +func (m *mockHost) SetNonPayable(bool) { + panic("Not implemented in tests") //nolint:gocritic +} + func (m *mockHost) GetBalance(addr types.Address) *big.Int { panic("Not implemented in tests") //nolint:gocritic } diff --git a/state/runtime/precompiled/native_transfer_test.go b/state/runtime/precompiled/native_transfer_test.go index 39c6d556fd..a6309f6d42 100644 --- a/state/runtime/precompiled/native_transfer_test.go +++ b/state/runtime/precompiled/native_transfer_test.go @@ -102,6 +102,10 @@ func (d dummyHost) SetStorage(addr types.Address, key types.Hash, value types.Ha return runtime.StorageAdded } +func (d dummyHost) SetNonPayable(nonPayable bool) { + d.t.Fatalf("SetNonPayable is not implemented") +} + func (d dummyHost) GetBalance(addr types.Address) *big.Int { balance, exists := d.balances[addr] if !exists { diff --git a/state/runtime/runtime.go b/state/runtime/runtime.go index 4ca7c2aa83..248e9f3fd1 100644 --- a/state/runtime/runtime.go +++ b/state/runtime/runtime.go @@ -21,6 +21,7 @@ type TxContext struct { ChainID int64 Difficulty types.Hash Tracer tracer.Tracer + NonPayable bool BaseFee *big.Int BurnContract types.Address } @@ -64,6 +65,7 @@ type Host interface { GetStorage(addr types.Address, key types.Hash) types.Hash SetStorage(addr types.Address, key types.Hash, value types.Hash, config *chain.ForksInTime) StorageStatus SetState(addr types.Address, key types.Hash, value types.Hash) + SetNonPayable(nonPayable bool) GetBalance(addr types.Address) *big.Int GetCodeSize(addr types.Address) int GetCodeHash(addr types.Address) types.Hash