Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

accounts/abi/bind/backend: SimulatedBackend options to always honour vm.Config #25072

Closed
wants to merge 6 commits into from
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
74 changes: 66 additions & 8 deletions accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
// 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 provides implementations of bind.ContractBackend. Currently
// only a SimulatedBackend for contract testing against a simulated blockchain
// is provided.
package backends

import (
Expand Down Expand Up @@ -72,23 +75,33 @@ type SimulatedBackend struct {
filterSystem *filters.FilterSystem // for filtering database logs

config *params.ChainConfig

// Historically, CallContract(), PendingCallContract(), and EstimateGas()
// created a new vm.Config instead of cloning the backend's underlying one.
// This flag allows protection of backwards compatibility depending on the
// constructor that created the SimulatedBackend.
alwaysCloneVMConfig bool
}

// NewSimulatedBackendWithDatabase creates a new binding backend based on the given database
// and uses a simulated blockchain for testing purposes.
// NewSimulatedBaNewSimulatedBackendWithDBAndVMConfigckend creates a new binding
// backend using a simulated blockchain for testing purposes.
// A simulated backend always uses chainID 1337.
func NewSimulatedBackendWithDatabase(database ethdb.Database, alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend {
//
// Unlike NewSimulatedBackend() and NewSimulatedBackendWithDatabase(), the
// vm.Config will be used for all contract interaction, not just transactions.
func NewSimulatedBackendWithDBAndVMConfig(database ethdb.Database, config vm.Config, alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend {
genesis := core.Genesis{
Config: params.AllEthashProtocolChanges,
GasLimit: gasLimit,
Alloc: alloc,
}
blockchain, _ := core.NewBlockChain(database, nil, &genesis, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
blockchain, _ := core.NewBlockChain(database, nil, &genesis, nil, ethash.NewFaker(), config, nil, nil)

backend := &SimulatedBackend{
database: database,
blockchain: blockchain,
config: genesis.Config,
database: database,
blockchain: blockchain,
config: genesis.Config,
alwaysCloneVMConfig: true,
}

filterBackend := &filterBackend{database, blockchain, backend}
Expand All @@ -99,13 +112,53 @@ func NewSimulatedBackendWithDatabase(database ethdb.Database, alloc core.Genesis
return backend
}

// NewSimulatedBackendWithDatabase creates a new binding backend based on the given database
// and uses a simulated blockchain for testing purposes.
// A simulated backend always uses chainID 1337.
//
// For historical reasons and to maintain backwards compatibility, if the
// SimulatedBackend's vm.Config is modified directly it will only be used for
// transactions, not for CallContract(), PendingCallContract(), nor
// EstimateGas(). To set a config that is always used, use
// NewSimulatedBackendWithDBAndVMConfig() or SetVMConfig().
func NewSimulatedBackendWithDatabase(database ethdb.Database, alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend {
b := NewSimulatedBackendWithDBAndVMConfig(database, vm.Config{}, alloc, gasLimit)
b.alwaysCloneVMConfig = false // maintains backwards compatibility
return b
}

// NewSimulatedBackendWithVMConfig creates a new binding backend using a simulated blockchain
// for testing purposes.
// A simulated backend always uses chainID 1337.
//
// Unlike NewSimulatedBackend() and NewSimulatedBackendWithDatabase(), the
// vm.Config will be used for all contract interaction, not just transactions.
func NewSimulatedBackendWithVMConfig(config vm.Config, alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend {
return NewSimulatedBackendWithDBAndVMConfig(rawdb.NewMemoryDatabase(), config, alloc, gasLimit)
}

// NewSimulatedBackend creates a new binding backend using a simulated blockchain
// for testing purposes.
// A simulated backend always uses chainID 1337.
//
// For historical reasons and to maintain backwards compatibility, if the
// SimulatedBackend's vm.Config is modified directly it will only be used for
// transactions, not for CallContract(), PendingCallContract(), nor
// EstimateGas(). To set a config that is always used, use
// NewSimulatedBackendWithVMConfig() or SetVMConfig().
func NewSimulatedBackend(alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend {
return NewSimulatedBackendWithDatabase(rawdb.NewMemoryDatabase(), alloc, gasLimit)
}

// SetVMConfig sets b.Blockchain().GetVMConfig() to the provided Config and
// ensures that it is always used for contract interaction. If the value is
// modified directly, not with this method, it will only be used for
// transactions but not calls to maintain backward compatibility.
func (b *SimulatedBackend) SetVMConfig(c vm.Config) {
*b.Blockchain().GetVMConfig() = c
b.alwaysCloneVMConfig = true
}

// Close terminates the underlying blockchain's update loop.
func (b *SimulatedBackend) Close() error {
b.blockchain.Stop()
Expand Down Expand Up @@ -646,9 +699,14 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM

txContext := core.NewEVMTxContext(msg)
evmContext := core.NewEVMBlockContext(block.Header(), b.blockchain, nil)
var vmConfig vm.Config
if c := b.blockchain.GetVMConfig(); c != nil && b.alwaysCloneVMConfig {
vmConfig = *c
}
vmConfig.NoBaseFee = true
// Create a new environment which holds all relevant information
// about the transaction and calling mechanisms.
vmEnv := vm.NewEVM(evmContext, txContext, stateDB, b.config, vm.Config{NoBaseFee: true})
vmEnv := vm.NewEVM(evmContext, txContext, stateDB, b.config, vmConfig)
gasPool := new(core.GasPool).AddGas(math.MaxUint64)

return core.NewStateTransition(vmEnv, msg, gasPool).TransitionDb()
Expand Down
176 changes: 171 additions & 5 deletions accounts/abi/bind/backends/simulated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
"math/big"
"math/rand"
"reflect"
Expand All @@ -32,8 +33,11 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"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/eth/tracers/logger"
"github.com/ethereum/go-ethereum/params"
)

Expand Down Expand Up @@ -112,12 +116,16 @@ const deployedCode = `60806040526004361061003b576000357c010000000000000000000000
// expected return value contains "hello world"
var expectedReturn = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}

const simTestGasLimit = 10000000

func simTestBackend(testAddr common.Address) *SimulatedBackend {
return NewSimulatedBackend(
core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000000000)},
}, 10000000,
)
return NewSimulatedBackend(simTestGenesisAlloc(testAddr), simTestGasLimit)
}

func simTestGenesisAlloc(testAddr common.Address) core.GenesisAlloc {
return core.GenesisAlloc{
testAddr: {Balance: big.NewInt(10000000000000000)},
}
}

func TestNewSimulatedBackend(t *testing.T) {
Expand Down Expand Up @@ -1057,6 +1065,164 @@ func TestPendingAndCallContract(t *testing.T) {
}
}

func TestVMConfigCloning(t *testing.T) {
// vm.Config was previously only honoured by SendTransaction() and ignored
// by CallContract(), PendingCallContract(), and EstimateGas(). Pre-existing
// constructors must still behave in this manner otherwise we may break
// users' code. Only the new constructors that explicitly accept a vm.Config
// will use it for all contract interaction.
//
// To test behaviour of vm.Config cloning, we inspect the number of entries
// in a struct trace. If cloned then there will be an entry, if not then no
// logs will be captured. This is necessary in order to not introduce a
// breaking change with the old vm.Config handling.
//
// Tests share the tracer and reset it as this is simpler than constructing
// a new one for each.
tracer := logger.NewStructLogger(&logger.Config{
Limit: 1,
})
config := vm.Config{
Debug: true,
Tracer: tracer,
}

testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
alloc := simTestGenesisAlloc(testAddr)

tests := []struct {
name string
backend func() *SimulatedBackend
// All constructors use the vm.Config for transactions so it's not
// explicitly specified.
wantNonTxUse bool
}{
{
name: "NewSimulatedBackend() has OLD behaviour",
backend: func() *SimulatedBackend {
b := NewSimulatedBackend(alloc, simTestGasLimit)
*b.Blockchain().GetVMConfig() = config
return b
},
wantNonTxUse: false,
},
{
name: "NewSimulatedBackendWithDatabase() has OLD behaviour",
backend: func() *SimulatedBackend {
b := NewSimulatedBackendWithDatabase(rawdb.NewMemoryDatabase(), alloc, simTestGasLimit)
*b.Blockchain().GetVMConfig() = config
return b
},
wantNonTxUse: false,
},
{
name: "NewSimulatedBackend() + SetVMConfig() has NEW behaviour",
backend: func() *SimulatedBackend {
b := NewSimulatedBackend(alloc, simTestGasLimit)
b.SetVMConfig(config)
return b
},
wantNonTxUse: true,
},
{
name: "NewSimulatedBackendWithVMConfig() has NEW behaviour",
backend: func() *SimulatedBackend {
return NewSimulatedBackendWithVMConfig(config, alloc, simTestGasLimit)
},
wantNonTxUse: true,
},
{
name: "NewSimulatedBackendWithDBAndVMConfig() has NEW behaviour",
backend: func() *SimulatedBackend {
return NewSimulatedBackendWithDBAndVMConfig(rawdb.NewMemoryDatabase(), config, alloc, simTestGasLimit)
},
wantNonTxUse: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sim := tt.backend()
defer sim.Close()
bgCtx := context.Background()

parsed, err := abi.JSON(strings.NewReader(abiJSON))
if err != nil {
t.Errorf("could not get code at test addr: %v", err)
}
contractAuth, _ := bind.NewKeyedTransactorWithChainID(testKey, big.NewInt(1337))
addr, _, contract, err := bind.DeployContract(contractAuth, parsed, common.FromHex(abiBin), sim)
if err != nil {
t.Errorf("could not deploy contract: %v", err)
}

input, err := parsed.Pack("receive", []byte("X"))
if err != nil {
t.Errorf("could not pack receive function on contract: %v", err)
}

callMsg := ethereum.CallMsg{
From: testAddr,
To: &addr,
Data: input,
}

for _, funcTest := range []struct {
name string
fn func(context.Context) error
wantVMConfigUsed bool
}{
{
name: "SendTransaction()",
fn: func(ctx context.Context) error {
if _, err := contract.Transact(contractAuth, "receive", []byte{}); err != nil {
return fmt.Errorf(`%T.Transact(…, "receive"): %v`, contract, err)
}
sim.Commit()
return nil
},
wantVMConfigUsed: true,
},
{
name: "PendingCallContract()",
fn: func(ctx context.Context) error {
_, err := sim.PendingCallContract(ctx, callMsg)
return err
},
wantVMConfigUsed: tt.wantNonTxUse,
},
{
name: "CallContract()",
fn: func(ctx context.Context) error {
_, err := sim.CallContract(ctx, callMsg, nil)
return err
},
wantVMConfigUsed: tt.wantNonTxUse,
},
{
name: "EstimateGas()",
fn: func(ctx context.Context) error {
_, err := sim.EstimateGas(ctx, callMsg)
return err
},
wantVMConfigUsed: tt.wantNonTxUse,
},
} {
t.Run(funcTest.name, func(t *testing.T) {
tracer.Reset()

if err := funcTest.fn(bgCtx); err != nil {
t.Fatalf("%s got err %v; want nil err", funcTest.name, err)
}
if gotVMConfigUsed := len(tracer.StructLogs()) > 0; gotVMConfigUsed != funcTest.wantVMConfigUsed {
t.Errorf("%s got custom vm.Config used = %t; want %t", funcTest.name, gotVMConfigUsed, funcTest.wantVMConfigUsed)
}
})
}
})
}
}

// This test is based on the following contract:
/*
contract Reverter {
Expand Down