From 47a90f7f9483209274cd93b47c79d219b39dfc99 Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi Date: Wed, 7 Sep 2022 17:20:49 +0200 Subject: [PATCH 1/2] internal/ethapi: add eth_batchCall method Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- eth/api_backend.go | 5 +- eth/tracers/api.go | 26 +- eth/tracers/api_test.go | 1 - ethclient/ethclient_test.go | 2 +- ethclient/gethclient/gethclient_test.go | 2 +- graphql/graphql_test.go | 1 + internal/ethapi/api.go | 117 +++- internal/ethapi/api_test.go | 645 +++++++++++++++++++++++ internal/ethapi/backend.go | 2 +- internal/ethapi/transaction_args_test.go | 2 +- internal/web3ext/web3ext.go | 5 + les/api_backend.go | 5 +- 12 files changed, 777 insertions(+), 36 deletions(-) create mode 100644 internal/ethapi/api_test.go diff --git a/eth/api_backend.go b/eth/api_backend.go index ccc0966f00a5..42e53105b67a 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -213,12 +213,15 @@ func (b *EthAPIBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { return nil } -func (b *EthAPIBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config) (*vm.EVM, func() error, error) { +func (b *EthAPIBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockContext *vm.BlockContext) (*vm.EVM, func() error, error) { if vmConfig == nil { vmConfig = b.eth.blockchain.GetVMConfig() } txContext := core.NewEVMTxContext(msg) context := core.NewEVMBlockContext(header, b.eth.BlockChain(), nil) + if blockContext != nil { + context = *blockContext + } return vm.NewEVM(context, txContext, state, b.eth.blockchain.Config(), *vmConfig), state.Error, nil } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 1e04bea411f3..923269ed10c6 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -93,34 +93,10 @@ func NewAPI(backend Backend) *API { return &API{backend: backend} } -type chainContext struct { - api *API - ctx context.Context -} - -func (context *chainContext) Engine() consensus.Engine { - return context.api.backend.Engine() -} - -func (context *chainContext) GetHeader(hash common.Hash, number uint64) *types.Header { - header, err := context.api.backend.HeaderByNumber(context.ctx, rpc.BlockNumber(number)) - if err != nil { - return nil - } - if header.Hash() == hash { - return header - } - header, err = context.api.backend.HeaderByHash(context.ctx, hash) - if err != nil { - return nil - } - return header -} - // chainContext constructs the context reader which is used by the evm for reading // the necessary chain context. func (api *API) chainContext(ctx context.Context) core.ChainContext { - return &chainContext{api: api, ctx: ctx} + return ethapi.NewChainContext(ctx, api.backend) } // blockByNumber is the wrapper of the chain access function offered by the backend. diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 346813ae2c77..23997da5be33 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -535,7 +535,6 @@ func TestTracingWithOverrides(t *testing.T) { From: &accounts[0].addr, // BLOCKNUMBER PUSH1 MSTORE Input: newRPCBytes(common.Hex2Bytes("4360005260206000f3")), - //&hexutil.Bytes{0x43}, // blocknumber }, config: &TraceCallConfig{ BlockOverrides: ðapi.BlockOverrides{Number: (*hexutil.Big)(big.NewInt(0x1337))}, diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index 67b1fde7569c..a939efed4bae 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -220,7 +220,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { t.Fatalf("can't create new node: %v", err) } // Create Ethereum Service - config := ðconfig.Config{Genesis: genesis} + config := ðconfig.Config{Genesis: genesis, RPCGasCap: 50000000} config.Ethash.PowMode = ethash.ModeFake ethservice, err := eth.New(n, config) if err != nil { diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go index e77c6015a215..55a6a380af39 100644 --- a/ethclient/gethclient/gethclient_test.go +++ b/ethclient/gethclient/gethclient_test.go @@ -54,7 +54,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { t.Fatalf("can't create new node: %v", err) } // Create Ethereum Service - config := ðconfig.Config{Genesis: genesis} + config := ðconfig.Config{Genesis: genesis, RPCGasCap: 50000000} config.Ethash.PowMode = ethash.ModeFake ethservice, err := eth.New(n, config) if err != nil { diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go index 491c73152113..2dc09d6556c3 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -345,6 +345,7 @@ func newGQLService(t *testing.T, stack *node.Node, gspec *core.Genesis, genBlock TrieDirtyCache: 5, TrieTimeout: 60 * time.Minute, SnapshotCache: 5, + RPCGasCap: 50000000, } ethBackend, err := eth.New(stack, ethConf) if err != nil { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 2e410605222c..f8f757f79136 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/core" @@ -946,6 +947,38 @@ func (diff *BlockOverrides) Apply(blockCtx *vm.BlockContext) { } } +// ChainContextBackend provides methods required to implement ChainContext. +type ChainContextBackend interface { + Engine() consensus.Engine + HeaderByNumber(context.Context, rpc.BlockNumber) (*types.Header, error) +} + +// ChainContext is an implementation of core.ChainContext. It's main use-case +// is instantiating a vm.BlockContext without having access to the BlockChain object. +type ChainContext struct { + b ChainContextBackend + ctx context.Context +} + +// NewChainContext creates a new ChainContext object. +func NewChainContext(ctx context.Context, backend ChainContextBackend) *ChainContext { + return &ChainContext{ctx: ctx, b: backend} +} + +func (context *ChainContext) Engine() consensus.Engine { + return context.b.Engine() +} + +func (context *ChainContext) GetHeader(hash common.Hash, number uint64) *types.Header { + // This method is called to get the hash for a block number when executing the BLOCKHASH + // opcode. Hence no need to search for non-canonical blocks. + header, err := context.b.HeaderByNumber(context.ctx, rpc.BlockNumber(number)) + if err != nil || header.Hash() != hash { + return nil + } + return header +} + func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) @@ -967,13 +1000,16 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash // Make sure the context is cancelled when the call has completed // this makes sure resources are cleaned up. defer cancel() + return doCall(ctx, b, args, state, header, timeout, new(core.GasPool).AddGas(globalGasCap), nil) +} +func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, timeout time.Duration, gp *core.GasPool, blockContext *vm.BlockContext) (*core.ExecutionResult, error) { // Get a new instance of the EVM. - msg, err := args.ToMessage(globalGasCap, header.BaseFee) + msg, err := args.ToMessage(gp.Gas(), header.BaseFee) if err != nil { return nil, err } - evm, vmError, err := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}) + evm, vmError, err := b.GetEVM(ctx, msg, state, header, &vm.Config{NoBaseFee: true}, blockContext) if err != nil { return nil, err } @@ -985,7 +1021,6 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash }() // Execute the message. - gp := new(core.GasPool).AddGas(math.MaxUint64) result, err := core.ApplyMessage(evm, msg, gp) if err := vmError(); err != nil { return nil, err @@ -1049,6 +1084,80 @@ func (s *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrO return result.Return(), result.Err } +// BatchCallConfig is the config object to be passed to eth_batchCall. +type BatchCallConfig struct { + Block rpc.BlockNumberOrHash + StateOverrides *StateOverride + Calls []BatchCallArgs +} + +// BatchCallArgs is the object specifying each call within eth_batchCall. It +// extends TransactionArgs with the list of block metadata overrides. +type BatchCallArgs struct { + TransactionArgs + BlockOverrides *BlockOverrides +} + +// CallResult is the result of one call. +type CallResult struct { + Return hexutil.Bytes + Error error +} + +// BatchCall executes a series of transactions on the state of a given block as base. +// The base state can be overridden once before transactions are executed. +// +// Additionally, each call can override block context fields such as number. +// +// Note, this function doesn't make any changes in the state/blockchain and is +// useful to execute and retrieve values. +func (s *BlockChainAPI) BatchCall(ctx context.Context, config BatchCallConfig) ([]CallResult, error) { + state, header, err := s.b.StateAndHeaderByNumberOrHash(ctx, config.Block) + if state == nil || err != nil { + return nil, err + } + // State overrides are applied once before all calls + if err := config.StateOverrides.Apply(state); err != nil { + return nil, err + } + // Setup context so it may be cancelled before the calls completed + // or, in case of unmetered gas, setup a context with a timeout. + var ( + cancel context.CancelFunc + timeout = s.b.RPCEVMTimeout() + ) + if timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, timeout) + } else { + ctx, cancel = context.WithCancel(ctx) + } + // Make sure the context is cancelled when the call has completed + // this makes sure resources are cleaned up. + defer cancel() + var ( + results []CallResult + // Each tx and all the series of txes shouldn't consume more gas than cap + globalGasCap = s.b.RPCGasCap() + gp = new(core.GasPool).AddGas(globalGasCap) + ) + for _, call := range config.Calls { + blockContext := core.NewEVMBlockContext(header, NewChainContext(ctx, s.b), nil) + if call.BlockOverrides != nil { + call.BlockOverrides.Apply(&blockContext) + } + result, err := doCall(ctx, s.b, call.TransactionArgs, state, header, timeout, gp, &blockContext) + if err != nil { + return nil, err + } + // If the result contains a revert reason, try to unpack and return it. + if len(result.Revert()) > 0 { + return nil, newRevertError(result) + } + results = append(results, CallResult{Return: result.Return(), Error: result.Err}) + } + return results, nil +} + func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, error) { // Binary search the gas requirement, as it may be higher than the amount used var ( @@ -1459,7 +1568,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH // Apply the transaction with the access list tracer tracer := logger.NewAccessListTracer(accessList, args.from(), to, precompiles) config := vm.Config{Tracer: tracer, Debug: true, NoBaseFee: true} - vmenv, _, err := b.GetEVM(ctx, msg, statedb, header, &config) + vmenv, _, err := b.GetEVM(ctx, msg, statedb, header, &config, nil) if err != nil { return nil, 0, nil, err } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go new file mode 100644 index 000000000000..fed0dda4cc69 --- /dev/null +++ b/internal/ethapi/api_test.go @@ -0,0 +1,645 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ethapi + +import ( + "bytes" + "context" + "crypto/ecdsa" + "encoding/json" + "errors" + "math/big" + "reflect" + "sort" + "testing" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/bloombits" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" +) + +type testBackend struct { + db ethdb.Database + chain *core.BlockChain +} + +func newTestBackend(t *testing.T, n int, gspec *core.Genesis, generator func(i int, b *core.BlockGen)) *testBackend { + var ( + engine = ethash.NewFaker() + backend = &testBackend{ + db: rawdb.NewMemoryDatabase(), + } + cacheConfig = &core.CacheConfig{ + TrieCleanLimit: 256, + TrieDirtyLimit: 256, + TrieTimeLimit: 5 * time.Minute, + SnapshotLimit: 0, + TrieDirtyDisabled: true, // Archive mode + } + ) + // Generate blocks for testing + _, blocks, _ := core.GenerateChainWithGenesis(gspec, engine, n, generator) + chain, err := core.NewBlockChain(backend.db, cacheConfig, gspec, nil, engine, vm.Config{}, nil, nil) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + if n, err := chain.InsertChain(blocks); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", n, err) + } + backend.chain = chain + return backend +} + +func (b testBackend) SyncProgress() ethereum.SyncProgress { return ethereum.SyncProgress{} } +func (b testBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + return big.NewInt(0), nil +} +func (b testBackend) FeeHistory(ctx context.Context, blockCount int, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, error) { + return nil, nil, nil, nil, nil +} +func (b testBackend) ChainDb() ethdb.Database { return b.db } +func (b testBackend) AccountManager() *accounts.Manager { return nil } +func (b testBackend) ExtRPCEnabled() bool { return false } +func (b testBackend) RPCGasCap() uint64 { return 10000000 } +func (b testBackend) RPCEVMTimeout() time.Duration { return time.Second } +func (b testBackend) RPCTxFeeCap() float64 { return 0 } +func (b testBackend) UnprotectedAllowed() bool { return false } +func (b testBackend) SetHead(number uint64) {} +func (b testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { + if number == rpc.LatestBlockNumber { + return b.chain.CurrentBlock().Header(), nil + } + return b.chain.GetHeaderByNumber(uint64(number)), nil +} +func (b testBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + panic("implement me") +} +func (b testBackend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) { + panic("implement me") +} +func (b testBackend) CurrentHeader() *types.Header { panic("implement me") } +func (b testBackend) CurrentBlock() *types.Block { panic("implement me") } +func (b testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { + if number == rpc.LatestBlockNumber { + return b.chain.CurrentBlock(), nil + } + return b.chain.GetBlockByNumber(uint64(number)), nil +} +func (b testBackend) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + panic("implement me") +} +func (b testBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) { + if blockNr, ok := blockNrOrHash.Number(); ok { + return b.BlockByNumber(ctx, blockNr) + } + panic("implement me") +} +func (b testBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) { + if number == rpc.PendingBlockNumber { + panic("pending state not implemented") + } + header, err := b.HeaderByNumber(ctx, number) + if err != nil { + return nil, nil, err + } + if header == nil { + return nil, nil, errors.New("header not found") + } + stateDb, err := b.chain.StateAt(header.Root) + return stateDb, header, err +} +func (b testBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { + if blockNr, ok := blockNrOrHash.Number(); ok { + return b.StateAndHeaderByNumber(ctx, blockNr) + } + panic("only implemented for number") +} +func (b testBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) { panic("implement me") } +func (b testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { + panic("implement me") +} +func (b testBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { panic("implement me") } +func (b testBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockContext *vm.BlockContext) (*vm.EVM, func() error, error) { + vmError := func() error { return nil } + if vmConfig == nil { + vmConfig = b.chain.GetVMConfig() + } + txContext := core.NewEVMTxContext(msg) + context := core.NewEVMBlockContext(header, b.chain, nil) + if blockContext != nil { + context = *blockContext + } + return vm.NewEVM(context, txContext, state, b.chain.Config(), *vmConfig), vmError, nil +} +func (b testBackend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { + panic("implement me") +} +func (b testBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { + panic("implement me") +} +func (b testBackend) GetPoolTransactions() (types.Transactions, error) { panic("implement me") } +func (b testBackend) GetPoolTransaction(txHash common.Hash) *types.Transaction { panic("implement me") } +func (b testBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { + panic("implement me") +} +func (b testBackend) Stats() (pending int, queued int) { panic("implement me") } +func (b testBackend) TxPoolContent() (map[common.Address]types.Transactions, map[common.Address]types.Transactions) { + panic("implement me") +} +func (b testBackend) TxPoolContentFrom(addr common.Address) (types.Transactions, types.Transactions) { + panic("implement me") +} +func (b testBackend) SubscribeNewTxsEvent(events chan<- core.NewTxsEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) ChainConfig() *params.ChainConfig { return b.chain.Config() } +func (b testBackend) Engine() consensus.Engine { return b.chain.Engine() } +func (b testBackend) GetLogs(ctx context.Context, blockHash common.Hash, number uint64) ([][]*types.Log, error) { + panic("implement me") +} +func (b testBackend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription { + panic("implement me") +} +func (b testBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription { + panic("implement me") +} +func (b testBackend) SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription { + panic("implement me") +} +func (b testBackend) BloomStatus() (uint64, uint64) { panic("implement me") } +func (b testBackend) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { + panic("implement me") +} + +func TestEstimateGas(t *testing.T) { + t.Parallel() + // Initialize test accounts + var ( + accounts = newAccounts(2) + genesis = &core.Genesis{ + Config: params.TestChainConfig, + Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + genBlocks = 10 + signer = types.HomesteadSigner{} + randomAccounts = newAccounts(2) + ) + api := NewBlockChainAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1] + // value: 1000 wei + // fee: 0 wei + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: &accounts[1].addr, Value: big.NewInt(1000), Gas: params.TxGas, GasPrice: b.BaseFee(), Data: nil}), signer, accounts[0].key) + b.AddTx(tx) + })) + var testSuite = []struct { + blockNumber rpc.BlockNumber + call TransactionArgs + expectErr error + want uint64 + }{ + // simple transfer on latest block + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: nil, + want: 21000, + }, + // simple transfer with insufficient funds on latest block + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &randomAccounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: core.ErrInsufficientFunds, + want: 21000, + }, + // empty create + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{}, + expectErr: nil, + want: 53000, + }, + } + for i, tc := range testSuite { + result, err := api.EstimateGas(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}) + if tc.expectErr != nil { + if err == nil { + t.Errorf("test %d: want error %v, have nothing", i, tc.expectErr) + continue + } + if !errors.Is(err, tc.expectErr) { + t.Errorf("test %d: error mismatch, want %v, have %v", i, tc.expectErr, err) + } + continue + } + if err != nil { + t.Errorf("test %d: want no error, have %v", i, err) + continue + } + if uint64(result) != tc.want { + t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, uint64(result), tc.want) + } + } +} + +func TestCall(t *testing.T) { + t.Parallel() + // Initialize test accounts + var ( + accounts = newAccounts(3) + genesis = &core.Genesis{ + Config: params.TestChainConfig, + Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + accounts[2].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + genBlocks = 10 + signer = types.HomesteadSigner{} + ) + api := NewBlockChainAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1] + // value: 1000 wei + // fee: 0 wei + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: &accounts[1].addr, Value: big.NewInt(1000), Gas: params.TxGas, GasPrice: b.BaseFee(), Data: nil}), signer, accounts[0].key) + b.AddTx(tx) + })) + randomAccounts := newAccounts(3) + var testSuite = []struct { + blockNumber rpc.BlockNumber + overrides StateOverride + call TransactionArgs + expectErr error + want string + }{ + // transfer on genesis + { + blockNumber: rpc.BlockNumber(0), + call: TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: nil, + want: "0x", + }, + // transfer on the head + { + blockNumber: rpc.BlockNumber(genBlocks), + call: TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: nil, + want: "0x", + }, + // transfer on a non-existent block, error expects + { + blockNumber: rpc.BlockNumber(genBlocks + 1), + call: TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: errors.New("header not found"), + }, + // transfer on the latest block + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: nil, + want: "0x", + }, + // Call which can only succeed if state is state overridden + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &randomAccounts[0].addr, + To: &randomAccounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + overrides: StateOverride{ + randomAccounts[0].addr: OverrideAccount{Balance: newRPCBalance(new(big.Int).Mul(big.NewInt(1), big.NewInt(params.Ether)))}, + }, + want: "0x", + }, + // Invalid call without state overriding + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &randomAccounts[0].addr, + To: &randomAccounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: core.ErrInsufficientFunds, + }, + // Successful simple contract call + // + // // SPDX-License-Identifier: GPL-3.0 + // + // pragma solidity >=0.7.0 <0.8.0; + // + // /** + // * @title Storage + // * @dev Store & retrieve value in a variable + // */ + // contract Storage { + // uint256 public number; + // constructor() { + // number = block.number; + // } + // } + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &randomAccounts[0].addr, + To: &randomAccounts[2].addr, + Data: hex2Bytes("8381f58a"), // call number() + }, + overrides: StateOverride{ + randomAccounts[2].addr: OverrideAccount{ + Code: hex2Bytes("6080604052348015600f57600080fd5b506004361060285760003560e01c80638381f58a14602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea2646970667358221220eab35ffa6ab2adfe380772a48b8ba78e82a1b820a18fcb6f59aa4efb20a5f60064736f6c63430007040033"), + StateDiff: &map[common.Hash]common.Hash{common.Hash{}: common.BigToHash(big.NewInt(123))}, + }, + }, + want: "0x000000000000000000000000000000000000000000000000000000000000007b", + }, + // TODO add testcase exhausting gas cap + } + for i, tc := range testSuite { + result, err := api.Call(context.Background(), tc.call, rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides) + if tc.expectErr != nil { + if err == nil { + t.Errorf("test %d: want error %v, have nothing", i, tc.expectErr) + continue + } + if !errors.Is(err, tc.expectErr) { + // Second try + if !reflect.DeepEqual(err, tc.expectErr) { + t.Errorf("test %d: error mismatch, want %v, have %v", i, tc.expectErr, err) + } + } + continue + } + if err != nil { + t.Errorf("test %d: want no error, have %v", i, err) + continue + } + if !reflect.DeepEqual(result.String(), tc.want) { + t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, result.String(), tc.want) + } + } +} + +func TestBatchCall(t *testing.T) { + t.Parallel() + // Initialize test accounts + var ( + accounts = newAccounts(3) + genBlocks = 10 + signer = types.HomesteadSigner{} + genesis = &core.Genesis{ + Config: params.TestChainConfig, + Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + accounts[2].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + ) + api := NewBlockChainAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1] + // value: 1000 wei + // fee: 0 wei + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{ + Nonce: uint64(i), + To: &accounts[1].addr, + Value: big.NewInt(1000), + Gas: params.TxGas, + GasPrice: b.BaseFee(), + Data: nil, + }), signer, accounts[0].key) + b.AddTx(tx) + })) + var ( + randomAccounts = newAccounts(3) + latestBlock = rpc.LatestBlockNumber + ) + type res struct { + Return string + Error string + } + var testSuite = []struct { + config BatchCallConfig + expectErr error + want []res + }{ + // First value transfer OK after state override, second one should succeed + // because of first transfer. + { + config: BatchCallConfig{ + Block: rpc.BlockNumberOrHash{BlockNumber: &latestBlock}, + StateOverrides: &StateOverride{ + randomAccounts[0].addr: OverrideAccount{Balance: newRPCBalance(big.NewInt(1000))}, + }, + Calls: []BatchCallArgs{{ + TransactionArgs: TransactionArgs{ + From: &randomAccounts[0].addr, + To: &randomAccounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + }, { + TransactionArgs: TransactionArgs{ + From: &randomAccounts[1].addr, + To: &randomAccounts[2].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + }}, + }, + want: []res{{ + Return: "0x", + }, { + Return: "0x", + }}, + }, + // Block overrides should work, each call is simulated on a different block number + { + config: BatchCallConfig{ + Block: rpc.BlockNumberOrHash{BlockNumber: &latestBlock}, + Calls: []BatchCallArgs{{ + TransactionArgs: TransactionArgs{ + From: &accounts[0].addr, + Input: &hexutil.Bytes{ + 0x43, // NUMBER + 0x60, 0x00, 0x52, // MSTORE offset 0 + 0x60, 0x20, 0x60, 0x00, 0xf3, + }, + }, + BlockOverrides: &BlockOverrides{ + Number: (*hexutil.Big)(big.NewInt(10)), + }, + }, { + TransactionArgs: TransactionArgs{ + From: &accounts[1].addr, + Input: &hexutil.Bytes{ + 0x43, // NUMBER + 0x60, 0x00, 0x52, // MSTORE offset 0 + 0x60, 0x20, 0x60, 0x00, 0xf3, + }, + }, + BlockOverrides: &BlockOverrides{ + Number: (*hexutil.Big)(big.NewInt(11)), + }, + }}, + }, + want: []res{{ + Return: "0x000000000000000000000000000000000000000000000000000000000000000a", + }, { + Return: "0x000000000000000000000000000000000000000000000000000000000000000b", + }}, + }, + // Test on solidity storage example. Set value in one call, read in next. + { + config: BatchCallConfig{ + Block: rpc.BlockNumberOrHash{BlockNumber: &latestBlock}, + StateOverrides: &StateOverride{ + randomAccounts[2].addr: OverrideAccount{ + Code: hex2Bytes("608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea2646970667358221220404e37f487a89a932dca5e77faaf6ca2de3b991f93d230604b1b8daaef64766264736f6c63430008070033"), + }, + }, + Calls: []BatchCallArgs{{ + // Set value to 5 + TransactionArgs: TransactionArgs{ + From: &randomAccounts[0].addr, + To: &randomAccounts[2].addr, + Input: hex2Bytes("6057361d0000000000000000000000000000000000000000000000000000000000000005"), + }, + }, { + // Read value + TransactionArgs: TransactionArgs{ + From: &randomAccounts[0].addr, + To: &randomAccounts[2].addr, + Input: hex2Bytes("2e64cec1"), + }, + }}, + }, + want: []res{{ + Return: "0x", + }, { + Return: "0x0000000000000000000000000000000000000000000000000000000000000005", + }}, + }, + } + + for i, tc := range testSuite { + result, err := api.BatchCall(context.Background(), tc.config) + if tc.expectErr != nil { + if err == nil { + t.Errorf("test %d: want error %v, have nothing", i, tc.expectErr) + continue + } + if !errors.Is(err, tc.expectErr) { + t.Errorf("test %d: error mismatch, want %v, have %v", i, tc.expectErr, err) + } + continue + } + if err != nil { + t.Errorf("test %d: want no error, have %v", i, err) + continue + } + // Turn result into res-struct + var have []res + resBytes, _ := json.Marshal(result) + if err := json.Unmarshal(resBytes, &have); err != nil { + t.Fatalf("failed to unmarshal result: %v", err) + } + if !reflect.DeepEqual(have, tc.want) { + t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, have, tc.want) + } + } +} + +type Account struct { + key *ecdsa.PrivateKey + addr common.Address +} + +type Accounts []Account + +func (a Accounts) Len() int { return len(a) } +func (a Accounts) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Accounts) Less(i, j int) bool { return bytes.Compare(a[i].addr.Bytes(), a[j].addr.Bytes()) < 0 } + +func newAccounts(n int) (accounts Accounts) { + for i := 0; i < n; i++ { + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + accounts = append(accounts, Account{key: key, addr: addr}) + } + sort.Sort(accounts) + return accounts +} + +func newRPCBalance(balance *big.Int) **hexutil.Big { + rpcBalance := (*hexutil.Big)(balance) + return &rpcBalance +} + +func hex2Bytes(str string) *hexutil.Bytes { + rpcBytes := hexutil.Bytes(common.Hex2Bytes(str)) + return &rpcBytes +} diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 5b4ceb631069..50a22b514f0d 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -68,7 +68,7 @@ type Backend interface { PendingBlockAndReceipts() (*types.Block, types.Receipts) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) GetTd(ctx context.Context, hash common.Hash) *big.Int - GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config) (*vm.EVM, func() error, error) + GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockContext *vm.BlockContext) (*vm.EVM, func() error, error) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 28dc561c36e4..ca5e998e6028 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -302,7 +302,7 @@ func (b *backendMock) GetLogs(ctx context.Context, blockHash common.Hash, number return nil, nil } func (b *backendMock) GetTd(ctx context.Context, hash common.Hash) *big.Int { return nil } -func (b *backendMock) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config) (*vm.EVM, func() error, error) { +func (b *backendMock) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockContext *vm.BlockContext) (*vm.EVM, func() error, error) { return nil, nil, nil } func (b *backendMock) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { return nil } diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 134562bde6fc..25ef906d7159 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -600,6 +600,11 @@ web3._extend({ call: 'eth_getLogs', params: 1, }), + new web3._extend.Method({ + name: 'batchCall', + call: 'eth_batchCall', + params: 1, + }), ], properties: [ new web3._extend.Property({ diff --git a/les/api_backend.go b/les/api_backend.go index 71cfbbed1e55..0778377c51d4 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -180,12 +180,15 @@ func (b *LesApiBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { return nil } -func (b *LesApiBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config) (*vm.EVM, func() error, error) { +func (b *LesApiBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockContext *vm.BlockContext) (*vm.EVM, func() error, error) { if vmConfig == nil { vmConfig = new(vm.Config) } txContext := core.NewEVMTxContext(msg) context := core.NewEVMBlockContext(header, b.eth.blockchain, nil) + if blockContext != nil { + context = *blockContext + } return vm.NewEVM(context, txContext, state, b.eth.chainConfig, *vmConfig), state.Error, nil } From bdf0fa783940863a5ed9768ccc789a8d3b422a5b Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi Date: Tue, 27 Sep 2022 16:10:23 -0500 Subject: [PATCH 2/2] not abort on revert of one call --- internal/ethapi/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index f8f757f79136..e105bae0e75c 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1149,9 +1149,9 @@ func (s *BlockChainAPI) BatchCall(ctx context.Context, config BatchCallConfig) ( if err != nil { return nil, err } - // If the result contains a revert reason, try to unpack and return it. + // If the result contains a revert reason, try to unpack it. if len(result.Revert()) > 0 { - return nil, newRevertError(result) + result.Err = newRevertError(result) } results = append(results, CallResult{Return: result.Return(), Error: result.Err}) }