From 1580ec180414bce1e37acc614bc2445f778efb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 20 May 2016 12:29:28 +0300 Subject: [PATCH] accounts/abi/bind, eth: rely on getCode for sanity checks, not estimate and call --- accounts/abi/bind/backend.go | 49 ++++++++++++++++++++++--- accounts/abi/bind/backends/nil.go | 1 + accounts/abi/bind/backends/remote.go | 20 ++++++++++ accounts/abi/bind/backends/simulated.go | 10 +++++ accounts/abi/bind/base.go | 27 ++++++++++++++ eth/api.go | 15 -------- eth/bind.go | 18 +++++---- 7 files changed, 112 insertions(+), 28 deletions(-) diff --git a/accounts/abi/bind/backend.go b/accounts/abi/bind/backend.go index 604e1ef26..65806aef4 100644 --- a/accounts/abi/bind/backend.go +++ b/accounts/abi/bind/backend.go @@ -27,15 +27,16 @@ import ( // ErrNoCode is returned by call and transact operations for which the requested // recipient contract to operate on does not exist in the state db or does not // have any code associated with it (i.e. suicided). -// -// Please note, this error string is part of the RPC API and is expected by the -// native contract bindings to signal this particular error. Do not change this -// as it will break all dependent code! var ErrNoCode = errors.New("no contract code at given address") // ContractCaller defines the methods needed to allow operating with contract on a read // only basis. type ContractCaller interface { + // HasCode checks if the contract at the given address has any code associated + // with it or not. This is needed to differentiate between contract internal + // errors and the local chain being out of sync. + HasCode(contract common.Address, pending bool) (bool, error) + // ContractCall executes an Ethereum contract call with the specified data as // the input. The pending flag requests execution against the pending block, not // the stable head of the chain. @@ -55,6 +56,11 @@ type ContractTransactor interface { // execution of a transaction. SuggestGasPrice() (*big.Int, error) + // HasCode checks if the contract at the given address has any code associated + // with it or not. This is needed to differentiate between contract internal + // errors and the local chain being out of sync. + HasCode(contract common.Address, pending bool) (bool, error) + // EstimateGasLimit tries to estimate the gas needed to execute a specific // transaction based on the current pending state of the backend blockchain. // There is no guarantee that this is the true gas limit requirement as other @@ -68,7 +74,38 @@ type ContractTransactor interface { // ContractBackend defines the methods needed to allow operating with contract // on a read-write basis. +// +// This interface is essentially the union of ContractCaller and ContractTransactor +// but due to a bug in the Go compiler (https://github.com/golang/go/issues/6977), +// we cannot simply list it as the two interfaces. The other solution is to add a +// third interface containing the common methods, but that convolutes the user API +// as it introduces yet another parameter to require for initialization. type ContractBackend interface { - ContractCaller - ContractTransactor + // HasCode checks if the contract at the given address has any code associated + // with it or not. This is needed to differentiate between contract internal + // errors and the local chain being out of sync. + HasCode(contract common.Address, pending bool) (bool, error) + + // ContractCall executes an Ethereum contract call with the specified data as + // the input. The pending flag requests execution against the pending block, not + // the stable head of the chain. + ContractCall(contract common.Address, data []byte, pending bool) ([]byte, error) + + // PendingAccountNonce retrieves the current pending nonce associated with an + // account. + PendingAccountNonce(account common.Address) (uint64, error) + + // SuggestGasPrice retrieves the currently suggested gas price to allow a timely + // execution of a transaction. + SuggestGasPrice() (*big.Int, error) + + // EstimateGasLimit tries to estimate the gas needed to execute a specific + // transaction based on the current pending state of the backend blockchain. + // There is no guarantee that this is the true gas limit requirement as other + // transactions may be added or removed by miners, but it should provide a basis + // for setting a reasonable default. + EstimateGasLimit(sender common.Address, contract *common.Address, value *big.Int, data []byte) (*big.Int, error) + + // SendTransaction injects the transaction into the pending pool for execution. + SendTransaction(tx *types.Transaction) error } diff --git a/accounts/abi/bind/backends/nil.go b/accounts/abi/bind/backends/nil.go index 3b1e6dce7..f10bb61ac 100644 --- a/accounts/abi/bind/backends/nil.go +++ b/accounts/abi/bind/backends/nil.go @@ -38,6 +38,7 @@ func (*nilBackend) ContractCall(common.Address, []byte, bool) ([]byte, error) { func (*nilBackend) EstimateGasLimit(common.Address, *common.Address, *big.Int, []byte) (*big.Int, error) { panic("not implemented") } +func (*nilBackend) HasCode(common.Address, bool) (bool, error) { panic("not implemented") } func (*nilBackend) SuggestGasPrice() (*big.Int, error) { panic("not implemented") } func (*nilBackend) PendingAccountNonce(common.Address) (uint64, error) { panic("not implemented") } func (*nilBackend) SendTransaction(*types.Transaction) error { panic("not implemented") } diff --git a/accounts/abi/bind/backends/remote.go b/accounts/abi/bind/backends/remote.go index 9b3647192..d903cbc8f 100644 --- a/accounts/abi/bind/backends/remote.go +++ b/accounts/abi/bind/backends/remote.go @@ -111,6 +111,26 @@ func (b *rpcBackend) request(method string, params []interface{}) (json.RawMessa return res.Result, nil } +// HasCode implements ContractVerifier.HasCode by retrieving any code associated +// with the contract from the remote node, and checking its size. +func (b *rpcBackend) HasCode(contract common.Address, pending bool) (bool, error) { + // Execute the RPC code retrieval + block := "latest" + if pending { + block = "pending" + } + res, err := b.request("eth_getCode", []interface{}{contract.Hex(), block}) + if err != nil { + return false, err + } + var hex string + if err := json.Unmarshal(res, &hex); err != nil { + return false, err + } + // Convert the response back to a Go byte slice and return + return len(common.FromHex(hex)) > 0, nil +} + // ContractCall implements ContractCaller.ContractCall, delegating the execution of // a contract call to the remote node, returning the reply to for local processing. func (b *rpcBackend) ContractCall(contract common.Address, data []byte, pending bool) ([]byte, error) { diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 4866c4f58..54b1ce603 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -78,6 +78,16 @@ func (b *SimulatedBackend) Rollback() { b.pendingState, _ = state.New(b.pendingBlock.Root(), b.database) } +// HasCode implements ContractVerifier.HasCode, checking whether there is any +// code associated with a certain account in the blockchain. +func (b *SimulatedBackend) HasCode(contract common.Address, pending bool) (bool, error) { + if pending { + return len(b.pendingState.GetCode(contract)) > 0, nil + } + statedb, _ := b.blockchain.State() + return len(statedb.GetCode(contract)) > 0, nil +} + // ContractCall implements ContractCaller.ContractCall, executing the specified // contract with the given input data. func (b *SimulatedBackend) ContractCall(contract common.Address, data []byte, pending bool) ([]byte, error) { diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index 06621c5ad..75e8d5bc8 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "math/big" + "sync/atomic" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -56,6 +57,9 @@ type BoundContract struct { abi abi.ABI // Reflect based ABI to access the correct Ethereum methods caller ContractCaller // Read interface to interact with the blockchain transactor ContractTransactor // Write interface to interact with the blockchain + + latestHasCode uint32 // Cached verification that the latest state contains code for this contract + pendingHasCode uint32 // Cached verification that the pending state contains code for this contract } // NewBoundContract creates a low level contract interface through which calls @@ -96,6 +100,19 @@ func (c *BoundContract) Call(opts *CallOpts, result interface{}, method string, if opts == nil { opts = new(CallOpts) } + // Make sure we have a contract to operate on, and bail out otherwise + if (opts.Pending && atomic.LoadUint32(&c.pendingHasCode) == 0) || (!opts.Pending && atomic.LoadUint32(&c.latestHasCode) == 0) { + if code, err := c.caller.HasCode(c.address, opts.Pending); err != nil { + return err + } else if !code { + return ErrNoCode + } + if opts.Pending { + atomic.StoreUint32(&c.pendingHasCode, 1) + } else { + atomic.StoreUint32(&c.latestHasCode, 1) + } + } // Pack the input, call and unpack the results input, err := c.abi.Pack(method, params...) if err != nil { @@ -153,6 +170,16 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i } gasLimit := opts.GasLimit if gasLimit == nil { + // Gas estimation cannot succeed without code for method invocations + if contract != nil && atomic.LoadUint32(&c.pendingHasCode) == 0 { + if code, err := c.transactor.HasCode(c.address, true); err != nil { + return nil, err + } else if !code { + return nil, ErrNoCode + } + atomic.StoreUint32(&c.pendingHasCode, 1) + } + // If the contract surely has code (or code is not needed), estimate the transaction gasLimit, err = c.transactor.EstimateGasLimit(opts.From, contract, value, input) if err != nil { return nil, fmt.Errorf("failed to exstimate gas needed: %v", err) diff --git a/eth/api.go b/eth/api.go index c8ccbd51b..8203424ae 100644 --- a/eth/api.go +++ b/eth/api.go @@ -52,15 +52,6 @@ import ( "golang.org/x/net/context" ) -// errNoCode is returned by call and transact operations for which the requested -// recipient contract to operate on does not exist in the state db or does not -// have any code associated with it (i.e. suicided). -// -// Please note, this error string is part of the RPC API and is expected by the -// native contract bindings to signal this particular error. Do not change this -// as it will break all dependent code! -var errNoCode = errors.New("no contract code at given address") - const defaultGas = uint64(90000) // blockByNumber is a commonly used helper function which retrieves and returns @@ -717,12 +708,6 @@ func (s *PublicBlockChainAPI) doCall(args CallArgs, blockNr rpc.BlockNumber) (st } stateDb = stateDb.Copy() - // If there's no code to interact with, respond with an appropriate error - if args.To != nil { - if code := stateDb.GetCode(*args.To); len(code) == 0 { - return "0x", nil, errNoCode - } - } // Retrieve the account state object to interact with var from *state.StateObject if args.From == (common.Address{}) { diff --git a/eth/bind.go b/eth/bind.go index 3a3eca062..fb7f29f60 100644 --- a/eth/bind.go +++ b/eth/bind.go @@ -19,7 +19,6 @@ package eth import ( "math/big" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" @@ -49,6 +48,17 @@ func NewContractBackend(eth *Ethereum) *ContractBackend { } } +// HasCode implements bind.ContractVerifier.HasCode by retrieving any code associated +// with the contract from the local API, and checking its size. +func (b *ContractBackend) HasCode(contract common.Address, pending bool) (bool, error) { + block := rpc.LatestBlockNumber + if pending { + block = rpc.PendingBlockNumber + } + out, err := b.bcapi.GetCode(contract, block) + return len(common.FromHex(out)) > 0, err +} + // ContractCall implements bind.ContractCaller executing an Ethereum contract // call with the specified data as the input. The pending flag requests execution // against the pending block, not the stable head of the chain. @@ -64,9 +74,6 @@ func (b *ContractBackend) ContractCall(contract common.Address, data []byte, pen } // Execute the call and convert the output back to Go types out, err := b.bcapi.Call(args, block) - if err == errNoCode { - err = bind.ErrNoCode - } return common.FromHex(out), err } @@ -95,9 +102,6 @@ func (b *ContractBackend) EstimateGasLimit(sender common.Address, contract *comm Value: *rpc.NewHexNumber(value), Data: common.ToHex(data), }) - if err == errNoCode { - err = bind.ErrNoCode - } return out.BigInt(), err }