From a701ffa66e5c26f26addbcc9574828f1828d8eda Mon Sep 17 00:00:00 2001 From: Kosala Hemachandra Date: Mon, 11 May 2020 18:03:51 -0700 Subject: [PATCH 1/2] add support for sequential gas estimation add multiple gas estimation merge fix test errors https://github.com/ethereum/go-ethereum/pull/21062 fix previous state error lint set empty previousState if not set cleanup and create state copy during DoCall create eth_estimateGasList method add comments for eth_estimateGasList enfore gasCap on eth_estimateGasList --- graphql/graphql.go | 9 ++-- internal/ethapi/api.go | 110 ++++++++++++++++++++++++++++------------- 2 files changed, 80 insertions(+), 39 deletions(-) diff --git a/graphql/graphql.go b/graphql/graphql.go index 559da8aaaa7a..cff657befa32 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -803,7 +803,7 @@ func (b *Block) Call(ctx context.Context, args struct { return nil, err } } - result, err := ethapi.DoCall(ctx, b.backend, args.Data, *b.numberOrHash, nil, vm.Config{}, 5*time.Second, b.backend.RPCGasCap()) + result, _, err := ethapi.DoCall(ctx, b.backend, args.Data, nil, *b.numberOrHash, nil, vm.Config{}, 5*time.Second, b.backend.RPCGasCap()) if err != nil { return nil, err } @@ -828,7 +828,7 @@ func (b *Block) EstimateGas(ctx context.Context, args struct { return hexutil.Uint64(0), err } } - gas, err := ethapi.DoEstimateGas(ctx, b.backend, args.Data, *b.numberOrHash, b.backend.RPCGasCap()) + gas, _, err := ethapi.DoEstimateGas(ctx, b.backend, args.Data, nil, *b.numberOrHash, b.backend.RPCGasCap()) return gas, err } @@ -873,7 +873,7 @@ func (p *Pending) Call(ctx context.Context, args struct { Data ethapi.CallArgs }) (*CallResult, error) { pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - result, err := ethapi.DoCall(ctx, p.backend, args.Data, pendingBlockNr, nil, vm.Config{}, 5*time.Second, p.backend.RPCGasCap()) + result, _, err := ethapi.DoCall(ctx, p.backend, args.Data, nil, pendingBlockNr, nil, vm.Config{}, 5*time.Second, p.backend.RPCGasCap()) if err != nil { return nil, err } @@ -893,7 +893,8 @@ func (p *Pending) EstimateGas(ctx context.Context, args struct { Data ethapi.CallArgs }) (hexutil.Uint64, error) { pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - return ethapi.DoEstimateGas(ctx, p.backend, args.Data, pendingBlockNr, p.backend.RPCGasCap()) + gas, _, err := ethapi.DoEstimateGas(ctx, p.backend, args.Data, nil, pendingBlockNr, p.backend.RPCGasCap()) + return gas, err } // Resolver is the top-level object in the GraphQL hierarchy. diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index c7d1e0020f77..f67e36e461bc 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -36,6 +36,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/clique" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" + "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" @@ -814,38 +815,51 @@ type account struct { StateDiff *map[common.Hash]common.Hash `json:"stateDiff"` } -func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]account, vmCfg vm.Config, 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()) +type PreviousState struct { + state *state.StateDB + header *types.Header +} - state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) - if state == nil || err != nil { - return nil, err +func DoCall(ctx context.Context, b Backend, args CallArgs, prevState *PreviousState, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]account, vmCfg vm.Config, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, *PreviousState, error) { + defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) + if prevState == nil { + prevState = &PreviousState{} + } + if (prevState.header != nil && prevState.header == nil) || (prevState.header == nil && prevState.header != nil) { + return nil, nil, fmt.Errorf("both header and state must be set to use previous staate") + } + if prevState.header == nil && prevState.state == nil { + var err error + prevState.state, prevState.header, err = b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if prevState.state == nil || err != nil { + return nil, nil, err + } } // Override the fields of specified contracts before execution. for addr, account := range overrides { // Override account nonce. if account.Nonce != nil { - state.SetNonce(addr, uint64(*account.Nonce)) + prevState.state.SetNonce(addr, uint64(*account.Nonce)) } // Override account(contract) code. if account.Code != nil { - state.SetCode(addr, *account.Code) + prevState.state.SetCode(addr, *account.Code) } // Override account balance. if account.Balance != nil { - state.SetBalance(addr, (*big.Int)(*account.Balance)) + prevState.state.SetBalance(addr, (*big.Int)(*account.Balance)) } if account.State != nil && account.StateDiff != nil { - return nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) + return nil, nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) } // Replace entire state if caller requires. if account.State != nil { - state.SetStorage(addr, *account.State) + prevState.state.SetStorage(addr, *account.State) } // Apply state diff into specified accounts. if account.StateDiff != nil { for key, value := range *account.StateDiff { - state.SetState(addr, key, value) + prevState.state.SetState(addr, key, value) } } } @@ -863,9 +877,9 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.Blo // Get a new instance of the EVM. msg := args.ToMessage(globalGasCap) - evm, vmError, err := b.GetEVM(ctx, msg, state, header) + evm, vmError, err := b.GetEVM(ctx, msg, prevState.state, prevState.header) if err != nil { - return nil, err + return nil, nil, err } // Wait for the context to be done and cancel the evm. Even if the // EVM has finished, cancelling may be done (repeatedly) @@ -879,16 +893,17 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.Blo gp := new(core.GasPool).AddGas(math.MaxUint64) result, err := core.ApplyMessage(evm, msg, gp) if err := vmError(); err != nil { - return nil, err + return nil, nil, err } // If the timer caused an abort, return an appropriate error message if evm.Cancelled() { - return nil, fmt.Errorf("execution aborted (timeout = %v)", timeout) + return nil, nil, fmt.Errorf("execution aborted (timeout = %v)", timeout) } if err != nil { - return result, fmt.Errorf("err: %w (supplied gas %d)", err, msg.Gas()) + return result, nil, fmt.Errorf("err: %w (supplied gas %d)", err, msg.Gas()) } - return result, nil + prevState.header.Root = prevState.state.IntermediateRoot(b.ChainConfig().IsEIP158(prevState.header.Number)) + return result, prevState, err } func newRevertError(result *core.ExecutionResult) *revertError { @@ -932,7 +947,7 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr if overrides != nil { accounts = *overrides } - result, err := DoCall(ctx, s.b, args, blockNrOrHash, accounts, vm.Config{}, 5*time.Second, s.b.RPCGasCap()) + result, _, err := DoCall(ctx, s.b, args, nil, blockNrOrHash, accounts, vm.Config{}, 5*time.Second, s.b.RPCGasCap()) if err != nil { return nil, err } @@ -943,12 +958,13 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr return result.Return(), result.Err } -func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, error) { +func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, prevState *PreviousState, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, *PreviousState, error) { // Binary search the gas requirement, as it may be higher than the amount used var ( - lo uint64 = params.TxGas - 1 - hi uint64 - cap uint64 + lo uint64 = params.TxGas - 1 + hi uint64 + cap uint64 + stateData *PreviousState ) // Use zero address if sender unspecified. if args.From == nil { @@ -961,10 +977,10 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash // Retrieve the block to act as the gas ceiling block, err := b.BlockByNumberOrHash(ctx, blockNrOrHash) if err != nil { - return 0, err + return 0, nil, err } if block == nil { - return 0, errors.New("block not found") + return 0, nil, errors.New("block not found") } hi = block.GasLimit() } @@ -972,13 +988,13 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash if args.GasPrice != nil && args.GasPrice.ToInt().BitLen() != 0 { state, _, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if err != nil { - return 0, err + return 0, nil, err } balance := state.GetBalance(*args.From) // from can't be nil available := new(big.Int).Set(balance) if args.Value != nil { if args.Value.ToInt().Cmp(available) >= 0 { - return 0, errors.New("insufficient funds for transfer") + return 0, nil, errors.New("insufficient funds for transfer") } available.Sub(available, args.Value.ToInt()) } @@ -1006,7 +1022,8 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash executable := func(gas uint64) (bool, *core.ExecutionResult, error) { args.Gas = (*hexutil.Uint64)(&gas) - result, err := DoCall(ctx, b, args, blockNrOrHash, nil, vm.Config{}, 0, gasCap) + result, prevS, err := DoCall(ctx, b, args, prevState, blockNrOrHash, nil, vm.Config{}, 0, gasCap) + stateData = prevS if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit @@ -1024,7 +1041,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash // call or transaction will never be accepted no matter how much gas it is // assigned. Return the error directly, don't struggle any more. if err != nil { - return 0, err + return 0, nil, err } if failed { lo = mid @@ -1036,20 +1053,20 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash if hi == cap { failed, result, err := executable(hi) if err != nil { - return 0, err + return 0, nil, err } if failed { if result != nil && result.Err != vm.ErrOutOfGas { if len(result.Revert()) > 0 { - return 0, newRevertError(result) + return 0, nil, newRevertError(result) } - return 0, result.Err + return 0, nil, result.Err } // Otherwise, the specified gas cap is too low - return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) + return 0, nil, fmt.Errorf("gas required exceeds allowance (%d)", cap) } } - return hexutil.Uint64(hi), nil + return hexutil.Uint64(hi), stateData, nil } // EstimateGas returns an estimate of the amount of gas needed to execute the @@ -1059,7 +1076,30 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs, bl if blockNrOrHash != nil { bNrOrHash = *blockNrOrHash } - return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap()) + gas, _, err := DoEstimateGas(ctx, s.b, args, nil, bNrOrHash, s.b.RPCGasCap()) + return gas, err +} + +// EstimateGasList returns an estimate of the amount of gas needed to execute list of +// given transactions against the current pending block. +func (s *PublicBlockChainAPI) EstimateGasList(ctx context.Context, argsList []CallArgs) ([]hexutil.Uint64, error) { + blockNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) + var ( + gas hexutil.Uint64 + err error + stateData *PreviousState + gasCap = s.b.RPCGasCap() + ) + returnVals := make([]hexutil.Uint64, len(argsList)) + for idx, args := range argsList { + gas, stateData, err = DoEstimateGas(ctx, s.b, args, stateData, blockNrOrHash, gasCap) + if err != nil { + return nil, err + } + gasCap -= uint64(gas) + returnVals[idx] = gas + } + return returnVals, nil } // ExecutionResult groups all structured logs emitted by the EVM @@ -1530,7 +1570,7 @@ func (args *SendTxArgs) setDefaults(ctx context.Context, b Backend) error { Data: input, } pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - estimated, err := DoEstimateGas(ctx, b, callArgs, pendingBlockNr, b.RPCGasCap()) + estimated, _, err := DoEstimateGas(ctx, b, callArgs, nil, pendingBlockNr, b.RPCGasCap()) if err != nil { return err } From 8fac719845875b00778aea04f796e4326532d7f5 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 24 Jun 2020 13:47:16 +0800 Subject: [PATCH 2/2] internal/ethapi, accounts/abi/bind/backends: rework estimate gaslist --- accounts/abi/bind/backends/simulated.go | 118 +++++++++++---- accounts/abi/bind/backends/simulated_test.go | 58 ++++++++ internal/ethapi/api.go | 147 +++++++++++-------- 3 files changed, 226 insertions(+), 97 deletions(-) diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index c7efca440b1f..1cb6280c3d50 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -387,7 +387,8 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call ethereum.CallM if err != nil { return nil, err } - res, err := b.callContract(ctx, call, b.blockchain.CurrentBlock(), stateDB) + // We can pass the state here since it will be dropped eventually. + res, _, err := b.callContract(ctx, call, &callContext{state: stateDB, header: b.blockchain.CurrentBlock().Header()}) if err != nil { return nil, err } @@ -402,9 +403,10 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call ethereum.CallM func (b *SimulatedBackend) PendingCallContract(ctx context.Context, call ethereum.CallMsg) ([]byte, error) { b.mu.Lock() defer b.mu.Unlock() - defer b.pendingState.RevertToSnapshot(b.pendingState.Snapshot()) - res, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) + // It's necessary to pass the copied state here, the state will be modified + // during the call. + res, _, err := b.callContract(ctx, call, &callContext{state: b.pendingState.Copy(), header: b.pendingBlock.Header()}) if err != nil { return nil, err } @@ -430,12 +432,19 @@ func (b *SimulatedBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error return big.NewInt(1), nil } -// EstimateGas executes the requested code against the currently pending block/state and -// returns the used amount of gas. -func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { +// estimateGas is the internal version of EstimateGas. It accepts the call message as +// well as the call context. It can be used to execute a single call or a serial calls +// with the supplied call context(the intermediate state can be saved). +func (b *SimulatedBackend) estimateGas(ctx context.Context, call ethereum.CallMsg, callctx *callContext) (uint64, *callContext, error) { b.mu.Lock() defer b.mu.Unlock() + if callctx == nil { + callctx = &callContext{ + state: b.pendingState, + header: b.pendingBlock.Header(), + } + } // Determine the lowest and highest possible gas limits to binary search in between var ( lo uint64 = params.TxGas - 1 @@ -453,7 +462,7 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs available := new(big.Int).Set(balance) if call.Value != nil { if call.Value.Cmp(available) >= 0 { - return 0, errors.New("insufficient funds for transfer") + return 0, nil, errors.New("insufficient funds for transfer") } available.Sub(available, call.Value) } @@ -471,61 +480,100 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs cap = hi // Create a helper to check if a gas allowance results in an executable transaction - executable := func(gas uint64) (bool, *core.ExecutionResult, error) { + executable := func(gas uint64) (bool, *core.ExecutionResult, *callContext, error) { call.Gas = gas - - snapshot := b.pendingState.Snapshot() - res, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) - b.pendingState.RevertToSnapshot(snapshot) - + res, after, err := b.callContract(ctx, call, callctx.copy()) // only pass the copy if err != nil { if err == core.ErrIntrinsicGas { - return true, nil, nil // Special case, raise gas limit + return true, nil, nil, nil // Special case, raise gas limit } - return true, nil, err // Bail out + return true, nil, nil, err // Bail out } - return res.Failed(), res, nil + return res.Failed(), res, after, nil } // Execute the binary search and hone in on an executable gas limit + var after *callContext for lo+1 < hi { mid := (hi + lo) / 2 - failed, _, err := executable(mid) + failed, _, callctx, err := executable(mid) // If the error is not nil(consensus error), it means the provided message // call or transaction will never be accepted no matter how much gas it is // assigned. Return the error directly, don't struggle any more if err != nil { - return 0, err + return 0, nil, err } if failed { lo = mid } else { - hi = mid + hi, after = mid, callctx } } // Reject the transaction as invalid if it still fails at the highest allowance if hi == cap { - failed, result, err := executable(hi) + failed, result, callctx, err := executable(hi) if err != nil { - return 0, err + return 0, nil, err } if failed { if result != nil && result.Err != vm.ErrOutOfGas { if len(result.Revert()) > 0 { - return 0, newRevertError(result) + return 0, nil, newRevertError(result) } - return 0, result.Err + return 0, nil, result.Err } // Otherwise, the specified gas cap is too low - return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) + return 0, nil, fmt.Errorf("gas required exceeds allowance (%d)", cap) } + after = callctx + } + return hi, after, nil +} + +// EstimateGas executes the requested code against the currently pending block/state and +// returns the used amount of gas. +func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { + res, _, err := b.estimateGas(ctx, call, nil) + return res, err +} + +// EstimateGasList returns an estimate of the amount of gas needed to execute list of +// given transactions against the current pending block. +func (b *SimulatedBackend) EstimateGasList(ctx context.Context, calls []ethereum.CallMsg) ([]uint64, error) { + var ( + gas uint64 + err error + callctx *callContext + ) + res := make([]uint64, len(calls)) + for idx, call := range calls { + gas, callctx, err = b.estimateGas(ctx, call, callctx) + if err != nil { + return nil, err + } + res[idx] = gas + } + return res, nil +} + +// callContext is the environment for one call or a serial calls execution +type callContext struct { + state *state.StateDB // The state used to execute call + header *types.Header // The associated header, it should be **immutable** +} + +// copy returns the copied instance of call environment. +func (callctx *callContext) copy() *callContext { + return &callContext{ + state: callctx.state.Copy(), + header: callctx.header, } - return hi, nil } // callContract implements common code between normal and pending contract calls. -// state is modified during execution, make sure to copy it if necessary. -func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallMsg, block *types.Block, stateDB *state.StateDB) (*core.ExecutionResult, error) { +// State in the callctx is modified during execution, make sure to copy it if +// necessary. The callctx is always assumed not nil. +func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallMsg, callctx *callContext) (*core.ExecutionResult, *callContext, error) { // Ensure message is initialized properly. if call.GasPrice == nil { call.GasPrice = big.NewInt(1) @@ -537,18 +585,24 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM call.Value = new(big.Int) } // Set infinite balance to the fake caller account. - from := stateDB.GetOrNewStateObject(call.From) + from := callctx.state.GetOrNewStateObject(call.From) from.SetBalance(math.MaxBig256) // Execute the call. msg := callMsg{call} - evmContext := core.NewEVMContext(msg, block.Header(), b.blockchain, nil) + evmContext := core.NewEVMContext(msg, callctx.header, b.blockchain, nil) // Create a new environment which holds all relevant information // about the transaction and calling mechanisms. - vmEnv := vm.NewEVM(evmContext, stateDB, b.config, vm.Config{}) - gasPool := new(core.GasPool).AddGas(math.MaxUint64) + vmenv := vm.NewEVM(evmContext, callctx.state, b.config, vm.Config{}) + gaspool := new(core.GasPool).AddGas(math.MaxUint64) - return core.NewStateTransition(vmEnv, msg, gasPool).TransitionDb() + res, err := core.NewStateTransition(vmenv, msg, gaspool).TransitionDb() + if err != nil { + return nil, nil, err + } + // It's necessary to call finalise if we are executing a batch of calls. + callctx.state.Finalise(b.blockchain.Config().IsEIP158(callctx.header.Number)) + return res, callctx, nil } // SendTransaction updates the pending block to include the given transaction. diff --git a/accounts/abi/bind/backends/simulated_test.go b/accounts/abi/bind/backends/simulated_test.go index e2597cca0163..e41fb0b29af3 100644 --- a/accounts/abi/bind/backends/simulated_test.go +++ b/accounts/abi/bind/backends/simulated_test.go @@ -581,6 +581,64 @@ func TestSimulatedBackend_EstimateGasWithPrice(t *testing.T) { } } +func TestSimulatedBackend_EstimateGasList(t *testing.T) { + /* + pragma solidity >=0.6.0 <0.7.0; + + contract HelloWorld { + uint256 value; + function setValue(uint256 _value) public { + value = _value; + } + function getValue() public view returns (uint256) { + if (value == 0) { + revert(); + } + return value; + } + } + */ + const contractAbi = "[{\"inputs\":[],\"name\":\"getValue\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"setValue\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" + const contractBin = "0x608060405234801561001057600080fd5b5060d68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c80632096525514603757806355241077146053575b600080fd5b603d607e565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506096565b005b6000806000541415608e57600080fd5b600054905090565b806000819055505056fea264697066735822122076346f88e003f2d4fd66d3f372e7ac9862064d78f35048169307a88d9778b10364736f6c634300060a0033" + + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + opts := bind.NewKeyedTransactor(key) + + sim := NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(params.Ether)}}, 10000000) + defer sim.Close() + + parsed, _ := abi.JSON(strings.NewReader(contractAbi)) + contractAddr, _, _, _ := bind.DeployContract(opts, parsed, common.FromHex(contractBin), sim) + sim.Commit() + + var ( + calls []ethereum.CallMsg + gas []uint64 + ) + calls = append(calls, ethereum.CallMsg{ + From: addr, + To: &contractAddr, + Data: common.Hex2Bytes("552410770000000000000000000000000000000000000000000000000000000000000064"), + }) + gas = append(gas, 41446) + + calls = append(calls, ethereum.CallMsg{ + From: addr, + To: &contractAddr, + Data: common.Hex2Bytes("20965255"), + }) + gas = append(gas, 22881) + + res, err := sim.EstimateGasList(context.Background(), calls) + if err != nil { + t.Fatalf("Failed to estimate gas: %v", err) + } + if !reflect.DeepEqual(res, gas) { + t.Fatalf("Estimated gas mismatch, want: %v, got: %v", gas, res) + } +} + func TestSimulatedBackend_HeaderByHash(t *testing.T) { testAddr := crypto.PubkeyToAddress(testKey.PublicKey) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index f67e36e461bc..31a38d08f390 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -815,52 +815,66 @@ type account struct { StateDiff *map[common.Hash]common.Hash `json:"stateDiff"` } -type PreviousState struct { - state *state.StateDB - header *types.Header +// callContext is the environment for one call or a serial calls execution +type callContext struct { + state *state.StateDB // The state used to execute call + header *types.Header // The associated header, it should be **immutable** + overridden bool // Indicator whether the call state has been overriden } -func DoCall(ctx context.Context, b Backend, args CallArgs, prevState *PreviousState, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]account, vmCfg vm.Config, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, *PreviousState, error) { - defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) - if prevState == nil { - prevState = &PreviousState{} - } - if (prevState.header != nil && prevState.header == nil) || (prevState.header == nil && prevState.header != nil) { - return nil, nil, fmt.Errorf("both header and state must be set to use previous staate") +// copy returns the copied instance of call environment. +func (callctx *callContext) copy() *callContext { + return &callContext{ + state: callctx.state.Copy(), + header: callctx.header, + overridden: callctx.overridden, } - if prevState.header == nil && prevState.state == nil { - var err error - prevState.state, prevState.header, err = b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) - if prevState.state == nil || err != nil { +} + +func DoCall(ctx context.Context, b Backend, args CallArgs, callctx *callContext, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]account, vmCfg vm.Config, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, *callContext, error) { + defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) + + if callctx == nil { + state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if err != nil { return nil, nil, err } + callctx = &callContext{ + state: state, + header: header, + } } // Override the fields of specified contracts before execution. - for addr, account := range overrides { - // Override account nonce. - if account.Nonce != nil { - prevState.state.SetNonce(addr, uint64(*account.Nonce)) - } - // Override account(contract) code. - if account.Code != nil { - prevState.state.SetCode(addr, *account.Code) - } - // Override account balance. - if account.Balance != nil { - prevState.state.SetBalance(addr, (*big.Int)(*account.Balance)) - } - if account.State != nil && account.StateDiff != nil { - return nil, nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) - } - // Replace entire state if caller requires. - if account.State != nil { - prevState.state.SetStorage(addr, *account.State) - } - // Apply state diff into specified accounts. - if account.StateDiff != nil { - for key, value := range *account.StateDiff { - prevState.state.SetState(addr, key, value) + // Note if the call context is not nil(we are executing a serial + // of calls), don't apply the diff multi-time. + if overrides != nil && !callctx.overridden { + for addr, account := range overrides { + // Override account nonce. + if account.Nonce != nil { + callctx.state.SetNonce(addr, uint64(*account.Nonce)) + } + // Override account(contract) code. + if account.Code != nil { + callctx.state.SetCode(addr, *account.Code) + } + // Override account balance. + if account.Balance != nil { + callctx.state.SetBalance(addr, (*big.Int)(*account.Balance)) + } + if account.State != nil && account.StateDiff != nil { + return nil, nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) } + // Replace entire state if caller requires. + if account.State != nil { + callctx.state.SetStorage(addr, *account.State) + } + // Apply state diff into specified accounts. + if account.StateDiff != nil { + for key, value := range *account.StateDiff { + callctx.state.SetState(addr, key, value) + } + } + callctx.overridden = true } } // Setup context so it may be cancelled the call has completed @@ -877,7 +891,7 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, prevState *PreviousSt // Get a new instance of the EVM. msg := args.ToMessage(globalGasCap) - evm, vmError, err := b.GetEVM(ctx, msg, prevState.state, prevState.header) + evm, vmError, err := b.GetEVM(ctx, msg, callctx.state, callctx.header) if err != nil { return nil, nil, err } @@ -902,8 +916,8 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, prevState *PreviousSt if err != nil { return result, nil, fmt.Errorf("err: %w (supplied gas %d)", err, msg.Gas()) } - prevState.header.Root = prevState.state.IntermediateRoot(b.ChainConfig().IsEIP158(prevState.header.Number)) - return result, prevState, err + callctx.state.Finalise(b.ChainConfig().IsEIP158(callctx.header.Number)) + return result, callctx, err } func newRevertError(result *core.ExecutionResult) *revertError { @@ -958,13 +972,12 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr return result.Return(), result.Err } -func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, prevState *PreviousState, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, *PreviousState, error) { +func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, callctx *callContext, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, *callContext, error) { // Binary search the gas requirement, as it may be higher than the amount used var ( - lo uint64 = params.TxGas - 1 - hi uint64 - cap uint64 - stateData *PreviousState + lo uint64 = params.TxGas - 1 + hi uint64 + cap uint64 ) // Use zero address if sender unspecified. if args.From == nil { @@ -1019,23 +1032,28 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, prevState *Pre cap = hi // Create a helper to check if a gas allowance results in an executable transaction - executable := func(gas uint64) (bool, *core.ExecutionResult, error) { + executable := func(gas uint64) (bool, *core.ExecutionResult, *callContext, error) { args.Gas = (*hexutil.Uint64)(&gas) - result, prevS, err := DoCall(ctx, b, args, prevState, blockNrOrHash, nil, vm.Config{}, 0, gasCap) - stateData = prevS + var callctxcopy *callContext + if callctx != nil { + callctxcopy = callctx.copy() + } + result, after, err := DoCall(ctx, b, args, callctxcopy, blockNrOrHash, nil, vm.Config{}, 0, gasCap) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { - return true, nil, nil // Special case, raise gas limit + return true, nil, nil, nil // Special case, raise gas limit } - return true, nil, err // Bail out + return true, nil, nil, err // Bail out } - return result.Failed(), result, nil + return result.Failed(), result, after, nil } // Execute the binary search and hone in on an executable gas limit + var after *callContext + for lo+1 < hi { mid := (hi + lo) / 2 - failed, _, err := executable(mid) + failed, _, callctx, err := executable(mid) // If the error is not nil(consensus error), it means the provided message // call or transaction will never be accepted no matter how much gas it is @@ -1046,12 +1064,12 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, prevState *Pre if failed { lo = mid } else { - hi = mid + hi, after = mid, callctx } } // Reject the transaction as invalid if it still fails at the highest allowance if hi == cap { - failed, result, err := executable(hi) + failed, result, callctx, err := executable(hi) if err != nil { return 0, nil, err } @@ -1065,8 +1083,9 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, prevState *Pre // Otherwise, the specified gas cap is too low return 0, nil, fmt.Errorf("gas required exceeds allowance (%d)", cap) } + after = callctx } - return hexutil.Uint64(hi), stateData, nil + return hexutil.Uint64(hi), after, nil } // EstimateGas returns an estimate of the amount of gas needed to execute the @@ -1085,21 +1104,19 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs, bl func (s *PublicBlockChainAPI) EstimateGasList(ctx context.Context, argsList []CallArgs) ([]hexutil.Uint64, error) { blockNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) var ( - gas hexutil.Uint64 - err error - stateData *PreviousState - gasCap = s.b.RPCGasCap() + gas hexutil.Uint64 + err error + callctx *callContext ) - returnVals := make([]hexutil.Uint64, len(argsList)) + res := make([]hexutil.Uint64, len(argsList)) for idx, args := range argsList { - gas, stateData, err = DoEstimateGas(ctx, s.b, args, stateData, blockNrOrHash, gasCap) + gas, callctx, err = DoEstimateGas(ctx, s.b, args, callctx, blockNrOrHash, s.b.RPCGasCap()) if err != nil { return nil, err } - gasCap -= uint64(gas) - returnVals[idx] = gas + res[idx] = gas } - return returnVals, nil + return res, nil } // ExecutionResult groups all structured logs emitted by the EVM