Skip to content

Commit

Permalink
all: improve EstimateGas API (ethereum#20830)
Browse files Browse the repository at this point in the history
  • Loading branch information
JukLee0ira committed Nov 27, 2024
1 parent efafaba commit 2d5b73f
Show file tree
Hide file tree
Showing 16 changed files with 403 additions and 120 deletions.
32 changes: 32 additions & 0 deletions accounts/abi/abi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package abi
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"log"
"math/big"
Expand Down Expand Up @@ -713,3 +714,34 @@ func TestABI_MethodById(t *testing.T) {
}

}

func TestUnpackRevert(t *testing.T) {
t.Parallel()

var cases = []struct {
input string
expect string
expectErr error
}{
{"", "", errors.New("invalid data for unpacking")},
{"08c379a1", "", errors.New("invalid data for unpacking")},
{"08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d72657665727420726561736f6e00000000000000000000000000000000000000", "revert reason", nil},
}
for index, c := range cases {
t.Run(fmt.Sprintf("case %d", index), func(t *testing.T) {
got, err := UnpackRevert(common.Hex2Bytes(c.input))
if c.expectErr != nil {
if err == nil {
t.Fatalf("Expected non-nil error")
}
if err.Error() != c.expectErr.Error() {
t.Fatalf("Expected error mismatch, want %v, got %v", c.expectErr, err)
}
return
}
if c.expect != got {
t.Fatalf("Output mismatch, want %v, got %v", c.expect, got)
}
})
}
}
73 changes: 56 additions & 17 deletions accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/XinFinOrg/XDPoSChain/XDCx"
"github.com/XinFinOrg/XDPoSChain/XDCxlending"
"github.com/XinFinOrg/XDPoSChain/accounts"
"github.com/XinFinOrg/XDPoSChain/accounts/abi"
"github.com/XinFinOrg/XDPoSChain/accounts/abi/bind"
"github.com/XinFinOrg/XDPoSChain/accounts/keystore"
"github.com/XinFinOrg/XDPoSChain/common"
Expand All @@ -53,7 +54,6 @@ import (
var _ bind.ContractBackend = (*SimulatedBackend)(nil)

var errBlockNumberUnsupported = errors.New("SimulatedBackend cannot access blocks other than the latest block")
var errGasEstimationFailed = errors.New("gas required exceeds allowance or always failing transaction")

// SimulatedBackend implements bind.ContractBackend, simulating a blockchain in
// the background. Its main purpose is to allow easily testing contract bindings.
Expand Down Expand Up @@ -160,6 +160,12 @@ func NewSimulatedBackend(alloc core.GenesisAlloc) *SimulatedBackend {
return backend
}

// Close terminates the underlying blockchain's update loop.
func (b *SimulatedBackend) Close() error {
b.blockchain.Stop()
return nil
}

// Commit imports all the pending transactions as a single block and starts a
// fresh new state.
func (b *SimulatedBackend) Commit() {
Expand Down Expand Up @@ -289,8 +295,11 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call XDPoSChain.Cal
if err != nil {
return nil, err
}
rval, _, _, err := b.callContract(ctx, call, b.blockchain.CurrentBlock(), state)
return rval, err
res, err := b.callContract(ctx, call, b.blockchain.CurrentBlock(), state)
if err != nil {
return nil, err
}
return res.Return(), nil
}

// PendingCallContract executes a contract call on the pending state.
Expand All @@ -299,8 +308,11 @@ func (b *SimulatedBackend) PendingCallContract(ctx context.Context, call XDPoSCh
defer b.mu.Unlock()
defer b.pendingState.RevertToSnapshot(b.pendingState.Snapshot())

rval, _, _, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState)
return rval, err
res, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState)
if err != nil {
return nil, err
}
return res.Return(), nil
}

// PendingNonceAt implements PendingStateReader.PendingNonceAt, retrieving
Expand Down Expand Up @@ -347,42 +359,70 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call XDPoSChain.Call
cap = hi

// Create a helper to check if a gas allowance results in an executable transaction
executable := func(gas uint64) bool {
executable := func(gas uint64) (bool, *core.ExecutionResult, error) {
call.Gas = gas

snapshot := b.pendingState.Snapshot()
_, _, failed, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState)
res, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState)
b.pendingState.RevertToSnapshot(snapshot)

if err != nil || failed {
return false
if err != nil {
if err == core.ErrIntrinsicGas {
return true, nil, nil // Special case, raise gas limit
}
return true, nil, err // Bail out
}
return true
return res.Failed(), res, nil
}
// Execute the binary search and hone in on an executable gas limit
for lo+1 < hi {
mid := (hi + lo) / 2
if !executable(mid) {
failed, _, 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
}
if failed {
lo = mid
} else {
hi = mid
}
}
// Reject the transaction as invalid if it still fails at the highest allowance
if hi == cap {
if !executable(hi) {
return 0, errGasEstimationFailed
failed, result, err := executable(hi)
if err != nil {
return 0, err
}
if failed {
if result != nil && result.Err != vm.ErrOutOfGas {
errMsg := fmt.Sprintf("always failing transaction (%v)", result.Err)
if len(result.Revert()) > 0 {
ret, err := abi.UnpackRevert(result.Revert())
if err != nil {
errMsg += fmt.Sprintf(" (%#x)", result.Revert())
} else {
errMsg += fmt.Sprintf(" (%s)", ret)
}
}
return 0, errors.New(errMsg)
}
// Otherwise, the specified gas cap is too low
return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap)
}
}
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 XDPoSChain.CallMsg, block *types.Block, statedb *state.StateDB) (ret []byte, usedGas uint64, failed bool, err error) {
func (b *SimulatedBackend) callContract(ctx context.Context, call XDPoSChain.CallMsg, block *types.Block, statedb *state.StateDB) (*core.ExecutionResult, error) {
// Gas prices post 1559 need to be initialized
if call.GasPrice != nil && (call.GasFeeCap != nil || call.GasTipCap != nil) {
return nil, 0, false, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
}
head := b.blockchain.CurrentHeader()
if !b.blockchain.Config().IsEIP1559(head.Number) {
Expand Down Expand Up @@ -437,8 +477,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call XDPoSChain.Cal
vmenv := vm.NewEVM(evmContext, txContext, statedb, nil, b.config, vm.Config{NoBaseFee: true})
gaspool := new(core.GasPool).AddGas(math.MaxUint64)
owner := common.Address{}
ret, usedGas, failed, err, _ = core.NewStateTransition(vmenv, msg, gaspool).TransitionDb(owner)
return
return core.NewStateTransition(vmenv, msg, gaspool).TransitionDb(owner)
}

// SendTransaction updates the pending block to include the given transaction.
Expand Down
154 changes: 154 additions & 0 deletions accounts/abi/bind/backends/simulated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2019 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 <http://www.gnu.org/licenses/>.

package backends

import (
"context"
"errors"
"math/big"
"strings"
"testing"

"github.com/XinFinOrg/XDPoSChain"
"github.com/XinFinOrg/XDPoSChain/accounts/abi"
"github.com/XinFinOrg/XDPoSChain/accounts/abi/bind"
"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/core"
"github.com/XinFinOrg/XDPoSChain/crypto"
"github.com/XinFinOrg/XDPoSChain/params"
)

func TestSimulatedBackend_EstimateGas(t *testing.T) {
/*
pragma solidity ^0.6.4;
contract GasEstimation {
function PureRevert() public { revert(); }
function Revert() public { revert("revert reason");}
function OOG() public { for (uint i = 0; ; i++) {}}
function Assert() public { assert(false);}
function Valid() public {}
}*/
const contractAbi = "[{\"inputs\":[],\"name\":\"Assert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"OOG\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"PureRevert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"Revert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"Valid\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]"
const contractBin = "0x60806040523480156100115760006000fd5b50610017565b61016e806100266000396000f3fe60806040523480156100115760006000fd5b506004361061005c5760003560e01c806350f6fe3414610062578063aa8b1d301461006c578063b9b046f914610076578063d8b9839114610080578063e09fface1461008a5761005c565b60006000fd5b61006a610094565b005b6100746100ad565b005b61007e6100b5565b005b6100886100c2565b005b610092610135565b005b6000600090505b5b808060010191505061009b565b505b565b60006000fd5b565b600015156100bf57fe5b5b565b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252600d8152602001807f72657665727420726561736f6e0000000000000000000000000000000000000081526020015060200191505060405180910390fd5b565b5b56fea2646970667358221220345bbcbb1a5ecf22b53a78eaebf95f8ee0eceff6d10d4b9643495084d2ec934a64736f6c63430006040033"

key, _ := crypto.GenerateKey()
addr := crypto.PubkeyToAddress(key.PublicKey)
opts := bind.NewKeyedTransactor(key)

sim := NewXDCSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(params.Ether)}}, 10000000, &params.ChainConfig{
ConstantinopleBlock: big.NewInt(0),
XDPoS: &params.XDPoSConfig{
Epoch: 900,
SkipV1Validation: true,
V2: &params.V2{
SwitchBlock: big.NewInt(900),
CurrentConfig: params.UnitTestV2Configs[0],
},
},
})

defer sim.Close()

parsed, _ := abi.JSON(strings.NewReader(contractAbi))
contractAddr, _, _, _ := bind.DeployContract(opts, parsed, common.FromHex(contractBin), sim)
sim.Commit()

var cases = []struct {
name string
message XDPoSChain.CallMsg
expect uint64
expectError error
}{
{"plain transfer(valid)", XDPoSChain.CallMsg{
From: addr,
To: &addr,
Gas: 0,
GasPrice: big.NewInt(0),
Value: big.NewInt(1),
Data: nil,
}, params.TxGas, nil},

{"plain transfer(invalid)", XDPoSChain.CallMsg{
From: addr,
To: &contractAddr,
Gas: 0,
GasPrice: big.NewInt(0),
Value: big.NewInt(1),
Data: nil,
}, 0, errors.New("always failing transaction (execution reverted)")},

{"Revert", XDPoSChain.CallMsg{
From: addr,
To: &contractAddr,
Gas: 0,
GasPrice: big.NewInt(0),
Value: nil,
Data: common.Hex2Bytes("d8b98391"),
}, 0, errors.New("always failing transaction (execution reverted) (revert reason)")},

{"PureRevert", XDPoSChain.CallMsg{
From: addr,
To: &contractAddr,
Gas: 0,
GasPrice: big.NewInt(0),
Value: nil,
Data: common.Hex2Bytes("aa8b1d30"),
}, 0, errors.New("always failing transaction (execution reverted)")},

{"OOG", XDPoSChain.CallMsg{
From: addr,
To: &contractAddr,
Gas: 100000,
GasPrice: big.NewInt(0),
Value: nil,
Data: common.Hex2Bytes("50f6fe34"),
}, 0, errors.New("gas required exceeds allowance (100000)")},

{"Assert", XDPoSChain.CallMsg{
From: addr,
To: &contractAddr,
Gas: 100000,
GasPrice: big.NewInt(0),
Value: nil,
Data: common.Hex2Bytes("b9b046f9"),
}, 0, errors.New("always failing transaction (invalid opcode: INVALID)")},

{"Valid", XDPoSChain.CallMsg{
From: addr,
To: &contractAddr,
Gas: 100000,
GasPrice: big.NewInt(0),
Value: nil,
Data: common.Hex2Bytes("e09fface"),
}, 21483, nil},
}
for _, c := range cases {
got, err := sim.EstimateGas(context.Background(), c.message)
if c.expectError != nil {
if err == nil {
t.Fatalf("Expect error, got nil")
}
if c.expectError.Error() != err.Error() {
t.Fatalf("Expect error, want %v, got %v", c.expectError, err)
}
continue
}
if got != c.expect {
t.Fatalf("Gas estimation mismatch, want %d, got %d", c.expect, got)
}
}
}
1 change: 0 additions & 1 deletion core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ import (
var (
blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil)
CheckpointCh = make(chan int)
ErrNoGenesis = errors.New("Genesis not found in chain")

blockReorgMeter = metrics.NewRegisteredMeter("chain/reorg/executes", nil)
blockReorgAddMeter = metrics.NewRegisteredMeter("chain/reorg/add", nil)
Expand Down
16 changes: 16 additions & 0 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ var (
// ErrBlacklistedHash is returned if a block to import is on the blacklist.
ErrBlacklistedHash = errors.New("blacklisted hash")

// ErrNoGenesis is returned when there is no Genesis Block.
ErrNoGenesis = errors.New("genesis not found in chain")
)

// List of evm-call-message pre-checking errors. All state transtion messages will
// be pre-checked before execution. If any invalidation detected, the corresponding
// error should be returned which is defined here.
//
// - If the pre-checking happens in the miner, then the transaction won't be packed.
// - If the pre-checking happens in the block processing procedure, then a "BAD BLOCk"
// error should be emitted.
var (
// ErrNonceTooLow is returned if the nonce of a transaction is lower than the
// one present in the local chain.
ErrNonceTooLow = errors.New("nonce too low")
Expand All @@ -45,6 +57,10 @@ var (
// by a transaction is higher than what's left in the block.
ErrGasLimitReached = errors.New("gas limit reached")

// ErrInsufficientFundsForTransfer is returned if the transaction sender doesn't
// have enough funds for transfer(topmost call only).
ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer")

// ErrMaxInitCodeSizeExceeded is returned if creation transaction provides the init code bigger
// than init code size limit.
ErrMaxInitCodeSizeExceeded = errors.New("max initcode size exceeded")
Expand Down
Loading

0 comments on commit 2d5b73f

Please sign in to comment.