Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 86 additions & 32 deletions accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer EstimateGasSequenced or SequencedEstimateGas -- then we could later have other Sequenced... RPC apis, and try to gather the info about all eth.sequencedXXX in one place

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer a less generic name, perhaps sequencedCallContext

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)
Expand All @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions accounts/abi/bind/backends/simulated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
9 changes: 5 additions & 4 deletions graphql/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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.
Expand Down
Loading