Skip to content

Commit

Permalink
Update Simulated Backend Client (#10403)
Browse files Browse the repository at this point in the history
* Update Simulated Backend Client

This commit aims to improve the simulated backend client by removing the block
limit where only the latest block can be provided to `CallContext` and handling
input args more gracefully.

- `CallContext` will now only validate the block number passed in args
- `CallContext` will NOT restrict calls to current block, but will pass nil to backend
- `from` in call args is no longer required and will default to `0x`
- `from` and `to` accept `common.Address`, `*big.Int` and `string` types

Included is a move to Go standard error handling consistent with v1.20+ and
error wrapping.

* remove commented code

* add comments at top of file and panic on unrecognized value type

* updated error handling and comments

* address comments
  • Loading branch information
EasterTheBunny authored Sep 6, 2023
1 parent 55be162 commit 3dfb527
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 42 deletions.
10 changes: 10 additions & 0 deletions core/chains/evm/client/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
The simulated backend cannot access old blocks and will return an error if
anything other than `latest`, `nil`, or the latest block are passed to
`CallContract`.
The simulated client avoids the old block error from the simulated backend by
passing `nil` to `CallContract` when calling `CallContext` or `BatchCallContext`
and will not return an error when an old block is used.
*/
package client
134 changes: 92 additions & 42 deletions core/chains/evm/client/simulated_backend_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client
import (
"bytes"
"context"
"errors"
"fmt"
"math/big"
"strings"
Expand All @@ -16,7 +17,6 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
"github.com/pkg/errors"

clienttypes "github.com/smartcontractkit/chainlink/v2/common/chains/client"
"github.com/smartcontractkit/chainlink/v2/core/assets"
Expand Down Expand Up @@ -64,37 +64,85 @@ func (c *SimulatedBackendClient) checkEthCallArgs(
"must be an eth.CallArgs, got %+#v", args[0])
}
blockNumber, err := c.blockNumber(args[1])
if err != nil || blockNumber.Cmp(c.currentBlockNumber()) != 0 {
if err != nil {
return nil, nil, fmt.Errorf("fourth arg to SimulatedBackendClient.Call "+
"must be the string \"latest\", or a *big.Int equal to current "+
"blocknumber, got %#+v", args[1])
"must be the string \"latest\", or a *big.Int, got %#+v", args[1])
}

// to and from need to map to a common.Address but could come in as a string
var (
toAddr common.Address
frmAddr common.Address
)

toAddr, err = interfaceToAddress(callArgs["to"])
if err != nil {
return nil, nil, err
}

// from is optional in the standard client; default to 0x when missing
if value, ok := callArgs["from"]; ok {
addr, err := interfaceToAddress(value)
if err != nil {
return nil, nil, err
}

frmAddr = addr
} else {
frmAddr = common.HexToAddress("0x")
}

ca := CallArgs{
From: callArgs["from"].(common.Address),
To: *callArgs["to"].(*common.Address),
To: toAddr,
From: frmAddr,
Data: callArgs["data"].(hexutil.Bytes),
}

return &ca, blockNumber, nil
}

func interfaceToAddress(value interface{}) (common.Address, error) {
switch v := value.(type) {
case common.Address:
return v, nil
case string:
return common.HexToAddress(v), nil
case *big.Int:
return common.BigToAddress(v), nil
default:
return common.HexToAddress("0x"), fmt.Errorf("unrecognized value type for converting value to common.Address; try string, *big.Int, or common.Address")
}
}

// CallContext mocks the ethereum client RPC calls used by chainlink, copying the
// return value into result.
// The simulated client avoids the old block error from the simulated backend by
// passing `nil` to `CallContract` when calling `CallContext` or `BatchCallContext`
// and will not return an error when an old block is used.
func (c *SimulatedBackendClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
switch method {
case "eth_call":
callArgs, _, err := c.checkEthCallArgs(args)
if err != nil {
var (
callArgs *CallArgs
b []byte
err error
)

if callArgs, _, err = c.checkEthCallArgs(args); err != nil {
return err
}

callMsg := ethereum.CallMsg{From: callArgs.From, To: &callArgs.To, Data: callArgs.Data}
b, err := c.b.CallContract(ctx, callMsg, nil /* always latest block */)
if err != nil {
return errors.Wrapf(err, "while calling contract at address %x with "+
"data %x", callArgs.To, callArgs.Data)

if b, err = c.b.CallContract(ctx, callMsg, nil /* always latest block */); err != nil {
return fmt.Errorf("%w: while calling contract at address %x with "+
"data %x", err, callArgs.To, callArgs.Data)
}

switch r := result.(type) {
case *hexutil.Bytes:
*r = append(*r, b...)

if !bytes.Equal(*r, b) {
return fmt.Errorf("was passed a non-empty array, or failed to copy "+
"answer. Expected %x = %x", *r, b)
Expand Down Expand Up @@ -155,26 +203,26 @@ func init() {
var err error
balanceOfABI, err = abi.JSON(strings.NewReader(balanceOfABIString))
if err != nil {
panic(errors.Wrapf(err, "while parsing erc20ABI"))
panic(fmt.Errorf("%w: while parsing erc20ABI", err))
}
}

func (c *SimulatedBackendClient) TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (balance *big.Int, err error) {
callData, err := balanceOfABI.Pack("balanceOf", address)
if err != nil {
return nil, errors.Wrapf(err, "while seeking the ERC20 balance of %s on %s",
return nil, fmt.Errorf("%w: while seeking the ERC20 balance of %s on %s", err,
address, contractAddress)
}
b, err := c.b.CallContract(ctx, ethereum.CallMsg{
To: &contractAddress, Data: callData},
c.currentBlockNumber())
if err != nil {
return nil, errors.Wrapf(err, "while calling ERC20 balanceOf method on %s "+
"for balance of %s", contractAddress, address)
return nil, fmt.Errorf("%w: while calling ERC20 balanceOf method on %s "+
"for balance of %s", err, contractAddress, address)
}
err = balanceOfABI.UnpackIntoInterface(balance, "balanceOf", b)
if err != nil {
return nil, errors.New("unable to unpack balance")
return nil, fmt.Errorf("unable to unpack balance")
}
return balance, nil
}
Expand Down Expand Up @@ -208,8 +256,8 @@ func (c *SimulatedBackendClient) blockNumber(number interface{}) (blockNumber *b
default:
blockNumber, err = utils.HexToUint256(n)
if err != nil {
return nil, errors.Wrapf(err, "while parsing '%s' as hex-encoded"+
"block number", n)
return nil, fmt.Errorf("%w: while parsing '%s' as hex-encoded"+
"block number", err, n)
}
return blockNumber, nil
}
Expand Down Expand Up @@ -328,8 +376,8 @@ func (c *SimulatedBackendClient) SubscribeNewHead(
var err error
subscription.subscription, err = c.b.SubscribeNewHead(ctx, ch)
if err != nil {
return nil, errors.Wrapf(err, "could not subscribe to new heads on "+
"simulated backend")
return nil, fmt.Errorf("%w: could not subscribe to new heads on "+
"simulated backend", err)
}
go func() {
var lastHead *evmtypes.Head
Expand Down Expand Up @@ -428,8 +476,7 @@ func (c *SimulatedBackendClient) CallContract(ctx context.Context, msg ethereum.
res, err := c.b.CallContract(ctx, msg, blockNumber)
if err != nil {
dataErr := revertError{}
isCustomRevert := errors.As(err, &dataErr)
if isCustomRevert {
if errors.Is(err, &dataErr) {
return nil, &JsonError{Data: dataErr.ErrorData(), Message: dataErr.Error(), Code: 3}
}
// Generic revert, no data
Expand Down Expand Up @@ -459,6 +506,9 @@ func (c *SimulatedBackendClient) SuggestGasPrice(ctx context.Context) (*big.Int,
}

// BatchCallContext makes a batch rpc call.
// The simulated client avoids the old block error from the simulated backend by
// passing `nil` to `CallContract` when calling `CallContext` or `BatchCallContext`
// and will not return an error when an old block is used.
func (c *SimulatedBackendClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
select {
case <-ctx.Done():
Expand All @@ -471,14 +521,14 @@ func (c *SimulatedBackendClient) BatchCallContext(ctx context.Context, b []rpc.B
switch elem.Method {
case "eth_getTransactionReceipt":
if _, ok := elem.Result.(*evmtypes.Receipt); !ok {
return errors.Errorf("SimulatedBackendClient expected return type of *evmtypes.Receipt for eth_getTransactionReceipt, got type %T", elem.Result)
return fmt.Errorf("SimulatedBackendClient expected return type of *evmtypes.Receipt for eth_getTransactionReceipt, got type %T", elem.Result)
}
if len(elem.Args) != 1 {
return errors.Errorf("SimulatedBackendClient expected 1 arg, got %d for eth_getTransactionReceipt", len(elem.Args))
return fmt.Errorf("SimulatedBackendClient expected 1 arg, got %d for eth_getTransactionReceipt", len(elem.Args))
}
hash, is := elem.Args[0].(common.Hash)
if !is {
return errors.Errorf("SimulatedBackendClient expected arg to be a hash, got: %T", elem.Args[0])
return fmt.Errorf("SimulatedBackendClient expected arg to be a hash, got: %T", elem.Args[0])
}
receipt, err := c.b.TransactionReceipt(ctx, hash)
if receipt != nil {
Expand All @@ -490,22 +540,22 @@ func (c *SimulatedBackendClient) BatchCallContext(ctx context.Context, b []rpc.B
case *evmtypes.Head:
case *evmtypes.Block:
default:
return errors.Errorf("SimulatedBackendClient expected return type of [*evmtypes.Head] or [*evmtypes.Block] for eth_getBlockByNumber, got type %T", v)
return fmt.Errorf("SimulatedBackendClient expected return type of [*evmtypes.Head] or [*evmtypes.Block] for eth_getBlockByNumber, got type %T", v)
}
if len(elem.Args) != 2 {
return errors.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_getBlockByNumber", len(elem.Args))
return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_getBlockByNumber", len(elem.Args))
}
blockNum, is := elem.Args[0].(string)
if !is {
return errors.Errorf("SimulatedBackendClient expected first arg to be a string for eth_getBlockByNumber, got: %T", elem.Args[0])
return fmt.Errorf("SimulatedBackendClient expected first arg to be a string for eth_getBlockByNumber, got: %T", elem.Args[0])
}
_, is = elem.Args[1].(bool)
if !is {
return errors.Errorf("SimulatedBackendClient expected second arg to be a boolean for eth_getBlockByNumber, got: %T", elem.Args[1])
return fmt.Errorf("SimulatedBackendClient expected second arg to be a boolean for eth_getBlockByNumber, got: %T", elem.Args[1])
}
n, ok := new(big.Int).SetString(blockNum, 0)
if !ok {
return errors.Errorf("error while converting block number string: %s to big.Int ", blockNum)
return fmt.Errorf("error while converting block number string: %s to big.Int ", blockNum)
}
header, err := c.b.HeaderByNumber(ctx, n)
if err != nil {
Expand All @@ -525,33 +575,33 @@ func (c *SimulatedBackendClient) BatchCallContext(ctx context.Context, b []rpc.B
Timestamp: time.Unix(int64(header.Time), 0),
}
default:
return errors.Errorf("SimulatedBackendClient Unexpected Type %T", v)
return fmt.Errorf("SimulatedBackendClient Unexpected Type %T", v)
}

b[i].Error = err
case "eth_call":
if len(elem.Args) != 2 {
return errors.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_call", len(elem.Args))
return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_call", len(elem.Args))
}

_, ok := elem.Result.(*string)
if !ok {
return errors.Errorf("SimulatedBackendClient expected result to be *string for eth_call, got: %T", elem.Result)
return fmt.Errorf("SimulatedBackendClient expected result to be *string for eth_call, got: %T", elem.Result)
}

params, ok := elem.Args[0].(map[string]interface{})
if !ok {
return errors.Errorf("SimulatedBackendClient expected first arg to be map[string]interface{} for eth_call, got: %T", elem.Args[0])
return fmt.Errorf("SimulatedBackendClient expected first arg to be map[string]interface{} for eth_call, got: %T", elem.Args[0])
}

blockNum, ok := elem.Args[1].(string)
if !ok {
return errors.Errorf("SimulatedBackendClient expected second arg to be a string for eth_call, got: %T", elem.Args[1])
return fmt.Errorf("SimulatedBackendClient expected second arg to be a string for eth_call, got: %T", elem.Args[1])
}

if blockNum != "" {
if _, ok = new(big.Int).SetString(blockNum, 0); !ok {
return errors.Errorf("error while converting block number string: %s to big.Int ", blockNum)
return fmt.Errorf("error while converting block number string: %s to big.Int ", blockNum)
}
}

Expand All @@ -561,15 +611,15 @@ func (c *SimulatedBackendClient) BatchCallContext(ctx context.Context, b []rpc.B
b[i].Error = err
case "eth_getHeaderByNumber":
if len(elem.Args) != 1 {
return errors.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_getHeaderByNumber", len(elem.Args))
return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_getHeaderByNumber", len(elem.Args))
}
blockNum, is := elem.Args[0].(string)
if !is {
return errors.Errorf("SimulatedBackendClient expected first arg to be a string for eth_getHeaderByNumber, got: %T", elem.Args[0])
return fmt.Errorf("SimulatedBackendClient expected first arg to be a string for eth_getHeaderByNumber, got: %T", elem.Args[0])
}
n, err := hexutil.DecodeBig(blockNum)
if err != nil {
return errors.Errorf("error while converting hex block number %s to big.Int ", blockNum)
return fmt.Errorf("error while converting hex block number %s to big.Int ", blockNum)
}
header, err := c.b.HeaderByNumber(ctx, n)
if err != nil {
Expand All @@ -579,10 +629,10 @@ func (c *SimulatedBackendClient) BatchCallContext(ctx context.Context, b []rpc.B
case *types.Header:
b[i].Result = header
default:
return errors.Errorf("SimulatedBackendClient Unexpected Type %T", v)
return fmt.Errorf("SimulatedBackendClient Unexpected Type %T", v)
}
default:
return errors.Errorf("SimulatedBackendClient got unsupported method %s", elem.Method)
return fmt.Errorf("SimulatedBackendClient got unsupported method %s", elem.Method)
}
}
return nil
Expand Down

0 comments on commit 3dfb527

Please sign in to comment.