Commit 53573e0e authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-service: Make Multicaller more generic (#9767)

* multicaller: Support generic calls, not just eth_call

* multicaller: Support generic calls, not just eth_call

* multicaller: Implement balance_call

* multicaller: Split out a generic rpc stub for testing RPC requests other than eth_call

Test balance call
parent 1dacba5a
...@@ -138,7 +138,7 @@ func (c *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient comm ...@@ -138,7 +138,7 @@ func (c *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient comm
} }
func (c *FaultDisputeGameContract) GetCredits(ctx context.Context, block batching.Block, recipients ...common.Address) ([]*big.Int, error) { func (c *FaultDisputeGameContract) GetCredits(ctx context.Context, block batching.Block, recipients ...common.Address) ([]*big.Int, error) {
calls := make([]*batching.ContractCall, 0, len(recipients)) calls := make([]batching.Call, 0, len(recipients))
for _, recipient := range recipients { for _, recipient := range recipients {
calls = append(calls, c.contract.Call(methodCredit, recipient)) calls = append(calls, c.contract.Call(methodCredit, recipient))
} }
......
...@@ -86,7 +86,7 @@ func (f *DisputeGameFactoryContract) GetGamesAtOrAfter(ctx context.Context, bloc ...@@ -86,7 +86,7 @@ func (f *DisputeGameFactoryContract) GetGamesAtOrAfter(ctx context.Context, bloc
if rangeEnd > batchSize { if rangeEnd > batchSize {
rangeStart = rangeEnd - batchSize rangeStart = rangeEnd - batchSize
} }
calls := make([]*batching.ContractCall, 0, rangeEnd-rangeStart) calls := make([]batching.Call, 0, rangeEnd-rangeStart)
for i := rangeEnd - 1; ; i-- { for i := rangeEnd - 1; ; i-- {
calls = append(calls, f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(i))) calls = append(calls, f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(i)))
// Break once we've added the last call to avoid underflow when rangeStart == 0 // Break once we've added the last call to avoid underflow when rangeStart == 0
...@@ -117,7 +117,7 @@ func (f *DisputeGameFactoryContract) GetAllGames(ctx context.Context, blockHash ...@@ -117,7 +117,7 @@ func (f *DisputeGameFactoryContract) GetAllGames(ctx context.Context, blockHash
return nil, err return nil, err
} }
calls := make([]*batching.ContractCall, count) calls := make([]batching.Call, count)
for i := uint64(0); i < count; i++ { for i := uint64(0); i < count; i++ {
calls[i] = f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(i)) calls[i] = f.contract.Call(methodGameAtIndex, new(big.Int).SetUint64(i))
} }
......
...@@ -213,7 +213,7 @@ func (c *PreimageOracleContract) GetActivePreimages(ctx context.Context, blockHa ...@@ -213,7 +213,7 @@ func (c *PreimageOracleContract) GetActivePreimages(ctx context.Context, blockHa
} }
func (c *PreimageOracleContract) GetProposalMetadata(ctx context.Context, block batching.Block, idents ...keccakTypes.LargePreimageIdent) ([]keccakTypes.LargePreimageMetaData, error) { func (c *PreimageOracleContract) GetProposalMetadata(ctx context.Context, block batching.Block, idents ...keccakTypes.LargePreimageIdent) ([]keccakTypes.LargePreimageMetaData, error) {
var calls []*batching.ContractCall var calls []batching.Call
for _, ident := range idents { for _, ident := range idents {
calls = append(calls, c.contract.Call(methodProposalMetadata, ident.Claimant, ident.UUID)) calls = append(calls, c.contract.Call(methodProposalMetadata, ident.Claimant, ident.UUID))
} }
......
...@@ -98,7 +98,7 @@ func TestPreimageOracleContract_ChallengePeriod(t *testing.T) { ...@@ -98,7 +98,7 @@ func TestPreimageOracleContract_ChallengePeriod(t *testing.T) {
require.Equal(t, uint64(123), challengePeriod) require.Equal(t, uint64(123), challengePeriod)
// Should cache responses // Should cache responses
stubRpc.ClearResponses(methodChallengePeriod) stubRpc.ClearResponses()
challengePeriod, err = oracle.ChallengePeriod(context.Background()) challengePeriod, err = oracle.ChallengePeriod(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(123), challengePeriod) require.Equal(t, uint64(123), challengePeriod)
...@@ -126,7 +126,7 @@ func TestPreimageOracleContract_MinBondSizeLPP(t *testing.T) { ...@@ -126,7 +126,7 @@ func TestPreimageOracleContract_MinBondSizeLPP(t *testing.T) {
require.Equal(t, big.NewInt(123), minBond) require.Equal(t, big.NewInt(123), minBond)
// Should cache responses // Should cache responses
stubRpc.ClearResponses(methodMinBondSizeLPP) stubRpc.ClearResponses()
minBond, err = oracle.GetMinBondLPP(context.Background()) minBond, err = oracle.GetMinBondLPP(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, big.NewInt(123), minBond) require.Equal(t, big.NewInt(123), minBond)
......
...@@ -16,7 +16,7 @@ func ReadArray(ctx context.Context, caller *MultiCaller, block Block, countCall ...@@ -16,7 +16,7 @@ func ReadArray(ctx context.Context, caller *MultiCaller, block Block, countCall
return nil, fmt.Errorf("failed to load array length: %w", err) return nil, fmt.Errorf("failed to load array length: %w", err)
} }
count := result.GetBigInt(0).Uint64() count := result.GetBigInt(0).Uint64()
calls := make([]*ContractCall, count) calls := make([]Call, count)
for i := uint64(0); i < count; i++ { for i := uint64(0); i < count; i++ {
calls[i] = getCall(new(big.Int).SetUint64(i)) calls[i] = getCall(new(big.Int).SetUint64(i))
} }
......
package batching
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)
type BalanceCall struct {
addr common.Address
}
var _ Call = (*BalanceCall)(nil)
func NewBalanceCall(addr common.Address) *BalanceCall {
return &BalanceCall{addr}
}
func (b *BalanceCall) ToBatchElemCreator() (BatchElementCreator, error) {
return func(block Block) (any, rpc.BatchElem) {
out := new(hexutil.Big)
return out, rpc.BatchElem{
Method: "eth_getBalance",
Args: []interface{}{b.addr, block.value},
Result: &out,
}
}, nil
}
func (b *BalanceCall) HandleResult(result interface{}) (*CallResult, error) {
val, ok := result.(*hexutil.Big)
if !ok {
return nil, fmt.Errorf("response %v was not a *big.Int", result)
}
return &CallResult{out: []interface{}{(*big.Int)(val)}}, nil
}
package batching package batching
import ( import (
"fmt"
"math/big" "math/big"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc"
) )
type ContractCall struct { type BatchElementCreator func(block Block) (any, rpc.BatchElem)
Abi *abi.ABI
Addr common.Address
Method string
Args []interface{}
From common.Address
}
func NewContractCall(abi *abi.ABI, addr common.Address, method string, args ...interface{}) *ContractCall {
return &ContractCall{
Abi: abi,
Addr: addr,
Method: method,
Args: args,
}
}
func (c *ContractCall) Pack() ([]byte, error) {
return c.Abi.Pack(c.Method, c.Args...)
}
func (c *ContractCall) ToCallArgs() (interface{}, error) {
data, err := c.Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack arguments: %w", err)
}
arg := map[string]interface{}{
"from": c.From,
"to": &c.Addr,
"input": hexutil.Bytes(data),
}
return arg, nil
}
func (c *ContractCall) Unpack(hex hexutil.Bytes) (*CallResult, error) {
out, err := c.Abi.Unpack(c.Method, hex)
if err != nil {
return nil, fmt.Errorf("failed to unpack data: %w", err)
}
return &CallResult{out: out}, nil
}
func (c *ContractCall) ToTxCandidate() (txmgr.TxCandidate, error) { type Call interface {
data, err := c.Pack() ToBatchElemCreator() (BatchElementCreator, error)
if err != nil { HandleResult(interface{}) (*CallResult, error)
return txmgr.TxCandidate{}, fmt.Errorf("failed to pack arguments: %w", err)
}
return txmgr.TxCandidate{
TxData: data,
To: &c.Addr,
}, nil
} }
type CallResult struct { type CallResult struct {
......
...@@ -4,103 +4,10 @@ import ( ...@@ -4,103 +4,10 @@ import (
"math/big" "math/big"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestContractCall_ToCallArgs(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, big.NewInt(1234444))
call.From = common.Address{0xab}
args, err := call.ToCallArgs()
require.NoError(t, err)
argMap, ok := args.(map[string]interface{})
require.True(t, ok)
require.Equal(t, argMap["from"], call.From)
require.Equal(t, argMap["to"], &addr)
expectedData, err := call.Pack()
require.NoError(t, err)
require.Equal(t, argMap["input"], hexutil.Bytes(expectedData))
require.NotContains(t, argMap, "value")
require.NotContains(t, argMap, "gas")
require.NotContains(t, argMap, "gasPrice")
}
func TestContractCall_ToTxCandidate(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, big.NewInt(1234444))
candidate, err := call.ToTxCandidate()
require.NoError(t, err)
require.Equal(t, candidate.To, &addr)
expectedData, err := call.Pack()
require.NoError(t, err)
require.Equal(t, candidate.TxData, expectedData)
require.Nil(t, candidate.Value)
require.Zero(t, candidate.GasLimit)
}
func TestContractCall_Pack(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
sender := common.Address{0xcc}
amount := big.NewInt(1234444)
call := NewContractCall(testAbi, addr, "approve", sender, amount)
actual, err := call.Pack()
require.NoError(t, err)
expected, err := testAbi.Pack("approve", sender, amount)
require.NoError(t, err)
require.Equal(t, actual, expected)
}
func TestContractCall_PackInvalid(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
// Second arg should be a *big.Int so packing should fail
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, uint32(123))
_, err = call.Pack()
require.Error(t, err)
}
func TestContractCall_Unpack(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "balanceOf", common.Address{0xcc})
outputs := testAbi.Methods["balanceOf"].Outputs
expected := big.NewInt(1234)
packed, err := outputs.Pack(expected)
require.NoError(t, err)
unpacked, err := call.Unpack(packed)
require.NoError(t, err)
require.Equal(t, unpacked.GetBigInt(0), expected)
}
func TestContractCall_UnpackInvalid(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "balanceOf", common.Address{0xcc})
// Input data is the wrong format and won't unpack successfully
inputPacked, err := call.Pack()
require.NoError(t, err)
_, err = call.Unpack(inputPacked)
require.Error(t, err)
}
func TestCallResult_GetValues(t *testing.T) { func TestCallResult_GetValues(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
......
package batching
import (
"fmt"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)
type ContractCall struct {
Abi *abi.ABI
Addr common.Address
Method string
Args []interface{}
From common.Address
}
func NewContractCall(abi *abi.ABI, addr common.Address, method string, args ...interface{}) *ContractCall {
return &ContractCall{
Abi: abi,
Addr: addr,
Method: method,
Args: args,
}
}
func (c *ContractCall) Pack() ([]byte, error) {
return c.Abi.Pack(c.Method, c.Args...)
}
func (c *ContractCall) CallMethod() string {
return "eth_call"
}
func (c *ContractCall) ToBatchElemCreator() (BatchElementCreator, error) {
args, err := c.ToCallArgs()
if err != nil {
return nil, err
}
f := func(block Block) (any, rpc.BatchElem) {
out := new(hexutil.Bytes)
return out, rpc.BatchElem{
Method: "eth_call",
Args: []interface{}{args, block.value},
Result: &out,
}
}
return f, nil
}
func (c *ContractCall) ToCallArgs() (interface{}, error) {
data, err := c.Pack()
if err != nil {
return nil, fmt.Errorf("failed to pack arguments: %w", err)
}
arg := map[string]interface{}{
"from": c.From,
"to": &c.Addr,
"input": hexutil.Bytes(data),
}
return arg, nil
}
func (c *ContractCall) CreateResult() interface{} {
return new(hexutil.Bytes)
}
func (c *ContractCall) HandleResult(result interface{}) (*CallResult, error) {
out, err := c.Unpack(*result.(*hexutil.Bytes))
return out, err
}
func (c *ContractCall) Unpack(hex hexutil.Bytes) (*CallResult, error) {
out, err := c.Abi.Unpack(c.Method, hex)
if err != nil {
return nil, fmt.Errorf("failed to unpack data: %w", err)
}
return &CallResult{out: out}, nil
}
func (c *ContractCall) ToTxCandidate() (txmgr.TxCandidate, error) {
data, err := c.Pack()
if err != nil {
return txmgr.TxCandidate{}, fmt.Errorf("failed to pack arguments: %w", err)
}
return txmgr.TxCandidate{
TxData: data,
To: &c.Addr,
}, nil
}
package batching
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/require"
)
func TestContractCall_ToCallArgs(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, big.NewInt(1234444))
call.From = common.Address{0xab}
args, err := call.ToCallArgs()
require.NoError(t, err)
argMap, ok := args.(map[string]interface{})
require.True(t, ok)
require.Equal(t, argMap["from"], call.From)
require.Equal(t, argMap["to"], &addr)
expectedData, err := call.Pack()
require.NoError(t, err)
require.Equal(t, argMap["input"], hexutil.Bytes(expectedData))
require.NotContains(t, argMap, "value")
require.NotContains(t, argMap, "gas")
require.NotContains(t, argMap, "gasPrice")
}
func TestContractCall_ToTxCandidate(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, big.NewInt(1234444))
candidate, err := call.ToTxCandidate()
require.NoError(t, err)
require.Equal(t, candidate.To, &addr)
expectedData, err := call.Pack()
require.NoError(t, err)
require.Equal(t, candidate.TxData, expectedData)
require.Nil(t, candidate.Value)
require.Zero(t, candidate.GasLimit)
}
func TestContractCall_Pack(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
sender := common.Address{0xcc}
amount := big.NewInt(1234444)
call := NewContractCall(testAbi, addr, "approve", sender, amount)
actual, err := call.Pack()
require.NoError(t, err)
expected, err := testAbi.Pack("approve", sender, amount)
require.NoError(t, err)
require.Equal(t, actual, expected)
}
func TestContractCall_PackInvalid(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
// Second arg should be a *big.Int so packing should fail
call := NewContractCall(testAbi, addr, "approve", common.Address{0xcc}, uint32(123))
_, err = call.Pack()
require.Error(t, err)
}
func TestContractCall_Unpack(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "balanceOf", common.Address{0xcc})
outputs := testAbi.Methods["balanceOf"].Outputs
expected := big.NewInt(1234)
packed, err := outputs.Pack(expected)
require.NoError(t, err)
unpacked, err := call.Unpack(packed)
require.NoError(t, err)
require.Equal(t, unpacked.GetBigInt(0), expected)
}
func TestContractCall_UnpackInvalid(t *testing.T) {
addr := common.Address{0xbd}
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
call := NewContractCall(testAbi, addr, "balanceOf", common.Address{0xcc})
// Input data is the wrong format and won't unpack successfully
inputPacked, err := call.Pack()
require.NoError(t, err)
_, err = call.Unpack(inputPacked)
require.Error(t, err)
}
...@@ -6,7 +6,6 @@ import ( ...@@ -6,7 +6,6 @@ import (
"io" "io"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
...@@ -33,7 +32,7 @@ func (m *MultiCaller) BatchSize() int { ...@@ -33,7 +32,7 @@ func (m *MultiCaller) BatchSize() int {
return m.batchSize return m.batchSize
} }
func (m *MultiCaller) SingleCall(ctx context.Context, block Block, call *ContractCall) (*CallResult, error) { func (m *MultiCaller) SingleCall(ctx context.Context, block Block, call Call) (*CallResult, error) {
results, err := m.Call(ctx, block, call) results, err := m.Call(ctx, block, call)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -41,24 +40,19 @@ func (m *MultiCaller) SingleCall(ctx context.Context, block Block, call *Contrac ...@@ -41,24 +40,19 @@ func (m *MultiCaller) SingleCall(ctx context.Context, block Block, call *Contrac
return results[0], nil return results[0], nil
} }
func (m *MultiCaller) Call(ctx context.Context, block Block, calls ...*ContractCall) ([]*CallResult, error) { func (m *MultiCaller) Call(ctx context.Context, block Block, calls ...Call) ([]*CallResult, error) {
keys := make([]interface{}, len(calls)) keys := make([]BatchElementCreator, len(calls))
for i := 0; i < len(calls); i++ { for i := 0; i < len(calls); i++ {
args, err := calls[i].ToCallArgs() creator, err := calls[i].ToBatchElemCreator()
if err != nil { if err != nil {
return nil, err return nil, err
} }
keys[i] = args keys[i] = creator
} }
fetcher := NewIterativeBatchCall[interface{}, *hexutil.Bytes]( fetcher := NewIterativeBatchCall[BatchElementCreator, any](
keys, keys,
func(args interface{}) (*hexutil.Bytes, rpc.BatchElem) { func(key BatchElementCreator) (any, rpc.BatchElem) {
out := new(hexutil.Bytes) return key(block)
return out, rpc.BatchElem{
Method: "eth_call",
Args: []interface{}{args, block.value},
Result: &out,
}
}, },
m.rpc.BatchCallContext, m.rpc.BatchCallContext,
m.rpc.CallContext, m.rpc.CallContext,
...@@ -78,7 +72,7 @@ func (m *MultiCaller) Call(ctx context.Context, block Block, calls ...*ContractC ...@@ -78,7 +72,7 @@ func (m *MultiCaller) Call(ctx context.Context, block Block, calls ...*ContractC
callResults := make([]*CallResult, len(results)) callResults := make([]*CallResult, len(results))
for i, result := range results { for i, result := range results {
call := calls[i] call := calls[i]
out, err := call.Unpack(*result) out, err := call.HandleResult(result)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unpack result: %w", err) return nil, fmt.Errorf("failed to unpack result: %w", err)
} }
......
package test package test
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
...@@ -12,13 +11,13 @@ import ( ...@@ -12,13 +11,13 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
type expectedCall struct { type expectedCall struct {
abiMethod abi.Method
to common.Address to common.Address
block batching.Block block batching.Block
args []interface{} args []interface{}
...@@ -26,24 +25,75 @@ type expectedCall struct { ...@@ -26,24 +25,75 @@ type expectedCall struct {
outputs []interface{} outputs []interface{}
} }
func (e *expectedCall) String() string { func (c *expectedCall) Matches(rpcMethod string, args ...interface{}) error {
return fmt.Sprintf("{to: %v, block: %v, args: %v, outputs: %v}", e.to, e.block, e.args, e.outputs) if rpcMethod != "eth_call" {
return fmt.Errorf("expected rpcMethod eth_call but was %v", rpcMethod)
}
if len(args) != 2 {
return fmt.Errorf("expected arg count 2 but was %v", len(args))
}
callOpts, ok := args[0].(map[string]any)
if !ok {
return fmt.Errorf("arg 0 is not a map[string]any")
}
actualBlockRef := args[1]
to, ok := callOpts["to"].(*common.Address)
if !ok {
return errors.New("to is not an address")
}
if to == nil {
return errors.New("to is nil")
}
if *to != c.to {
return fmt.Errorf("expected to %v but was %v", c.to, *to)
}
data, ok := callOpts["input"].(hexutil.Bytes)
if !ok {
return errors.New("input is not hexutil.Bytes")
}
if len(data) < 4 {
return fmt.Errorf("expected input to have at least 4 bytes but was %v", len(data))
}
if !slices.Equal(c.abiMethod.ID, data[:4]) {
return fmt.Errorf("expected abi method ID %x but was %x", c.abiMethod.ID, data[:4])
}
if !slices.Equal(c.packedArgs, data[4:]) {
return fmt.Errorf("expected args %x but was %x", c.packedArgs, data[4:])
}
if !assert.ObjectsAreEqualValues(c.block.ArgValue(), actualBlockRef) {
return fmt.Errorf("expected block ref %v but was %v", c.block.ArgValue(), actualBlockRef)
}
return nil
}
func (c *expectedCall) Execute(t *testing.T, out interface{}) {
output, err := c.abiMethod.Outputs.Pack(c.outputs...)
require.NoErrorf(t, err, "Invalid outputs for method %v: %v", c.abiMethod.Name, c.outputs)
// I admit I do not understand Go reflection.
// So leverage json.Unmarshal to set the out value correctly.
j, err := json.Marshal(hexutil.Bytes(output))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(j, out))
}
func (c *expectedCall) String() string {
return fmt.Sprintf("{to: %v, block: %v, args: %v, outputs: %v}", c.to, c.block, c.args, c.outputs)
} }
type AbiBasedRpc struct { type AbiBasedRpc struct {
t *testing.T RpcStub
abis map[common.Address]*abi.ABI abis map[common.Address]*abi.ABI
expectedCalls map[string][]*expectedCall
} }
func NewAbiBasedRpc(t *testing.T, to common.Address, contractAbi *abi.ABI) *AbiBasedRpc { func NewAbiBasedRpc(t *testing.T, to common.Address, contractAbi *abi.ABI) *AbiBasedRpc {
abis := make(map[common.Address]*abi.ABI) abis := make(map[common.Address]*abi.ABI)
abis[to] = contractAbi abis[to] = contractAbi
return &AbiBasedRpc{ return &AbiBasedRpc{
t: t, RpcStub: RpcStub{
abis: abis, t: t,
expectedCalls: make(map[string][]*expectedCall), },
abis: abis,
} }
} }
...@@ -68,7 +118,8 @@ func (l *AbiBasedRpc) SetResponse(to common.Address, method string, block batchi ...@@ -68,7 +118,8 @@ func (l *AbiBasedRpc) SetResponse(to common.Address, method string, block batchi
require.Truef(l.t, ok, "No method: %v", method) require.Truef(l.t, ok, "No method: %v", method)
packedArgs, err := abiMethod.Inputs.Pack(expected...) packedArgs, err := abiMethod.Inputs.Pack(expected...)
require.NoErrorf(l.t, err, "Invalid expected arguments for method %v: %v", method, expected) require.NoErrorf(l.t, err, "Invalid expected arguments for method %v: %v", method, expected)
l.expectedCalls[method] = append(l.expectedCalls[method], &expectedCall{ l.AddExpectedCall(&expectedCall{
abiMethod: abiMethod,
to: to, to: to,
block: block, block: block,
args: expected, args: expected,
...@@ -77,71 +128,11 @@ func (l *AbiBasedRpc) SetResponse(to common.Address, method string, block batchi ...@@ -77,71 +128,11 @@ func (l *AbiBasedRpc) SetResponse(to common.Address, method string, block batchi
}) })
} }
func (l *AbiBasedRpc) ClearResponses(method string) {
delete(l.expectedCalls, method)
}
func (l *AbiBasedRpc) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
var errs []error
for _, elem := range b {
elem.Error = l.CallContext(ctx, elem.Result, elem.Method, elem.Args...)
errs = append(errs, elem.Error)
}
return errors.Join(errs...)
}
func (l *AbiBasedRpc) VerifyTxCandidate(candidate txmgr.TxCandidate) { func (l *AbiBasedRpc) VerifyTxCandidate(candidate txmgr.TxCandidate) {
require.NotNil(l.t, candidate.To) require.NotNil(l.t, candidate.To)
l.findExpectedCall(*candidate.To, candidate.TxData, batching.BlockLatest.ArgValue()) l.findExpectedCall("eth_call", map[string]any{
} "to": candidate.To,
"input": hexutil.Bytes(candidate.TxData),
func (l *AbiBasedRpc) CallContext(_ context.Context, out interface{}, method string, args ...interface{}) error { "value": candidate.Value,
require.Equal(l.t, "eth_call", method) }, batching.BlockLatest.ArgValue())
require.Len(l.t, args, 2)
actualBlockRef := args[1]
callOpts, ok := args[0].(map[string]any)
require.True(l.t, ok)
to, ok := callOpts["to"].(*common.Address)
require.True(l.t, ok)
require.NotNil(l.t, to)
data, ok := callOpts["input"].(hexutil.Bytes)
require.True(l.t, ok)
call, abiMethod := l.findExpectedCall(*to, data, actualBlockRef)
output, err := abiMethod.Outputs.Pack(call.outputs...)
require.NoErrorf(l.t, err, "Invalid outputs for method %v: %v", abiMethod.Name, call.outputs)
// I admit I do not understand Go reflection.
// So leverage json.Unmarshal to set the out value correctly.
j, err := json.Marshal(hexutil.Bytes(output))
require.NoError(l.t, err)
require.NoError(l.t, json.Unmarshal(j, out))
return nil
}
func (l *AbiBasedRpc) findExpectedCall(to common.Address, data []byte, actualBlockRef interface{}) (*expectedCall, *abi.Method) {
abiMethod, err := l.abi(to).MethodById(data[0:4])
require.NoError(l.t, err)
argData := data[4:]
args, err := abiMethod.Inputs.Unpack(argData)
require.NoError(l.t, err)
require.Len(l.t, args, len(abiMethod.Inputs))
expectedCalls, ok := l.expectedCalls[abiMethod.Name]
require.Truef(l.t, ok, "Unexpected call to %v", abiMethod.Name)
var call *expectedCall
for _, candidate := range expectedCalls {
if to == candidate.to &&
slices.Equal(candidate.packedArgs, argData) &&
assert.ObjectsAreEqualValues(candidate.block.ArgValue(), actualBlockRef) {
call = candidate
break
}
}
require.NotNilf(l.t, call, "No expected calls to %v at block %v with to: %v, arguments: %v\nExpected calls: %v",
to, abiMethod.Name, actualBlockRef, args, expectedCalls)
return call, abiMethod
} }
package test
import (
"context"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
// Note: These tests are in the test subpackage to avoid dependency cycles since they need to use the stubs
func TestGetBalance(t *testing.T) {
addr := common.Address{0xab, 0xcd}
expectedBalance := big.NewInt(248924)
stub := NewRpcStub(t)
stub.AddExpectedCall(NewGetBalanceCall(addr, batching.BlockLatest, expectedBalance))
caller := batching.NewMultiCaller(stub, batching.DefaultBatchSize)
result, err := caller.SingleCall(context.Background(), batching.BlockLatest, batching.NewBalanceCall(addr))
require.NoError(t, err)
require.Equal(t, expectedBalance, result.GetBigInt(0))
}
package test
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type ExpectedRpcCall interface {
fmt.Stringer
Matches(rpcMethod string, args ...interface{}) error
Execute(t *testing.T, out interface{})
}
type RpcStub struct {
t *testing.T
expectedCalls []ExpectedRpcCall
}
func NewRpcStub(t *testing.T) *RpcStub {
return &RpcStub{t: t}
}
func (r *RpcStub) ClearResponses() {
r.expectedCalls = nil
}
func (r *RpcStub) AddExpectedCall(call ExpectedRpcCall) {
r.expectedCalls = append(r.expectedCalls, call)
}
func (r *RpcStub) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
var errs []error
for _, elem := range b {
elem.Error = r.CallContext(ctx, elem.Result, elem.Method, elem.Args...)
errs = append(errs, elem.Error)
}
return errors.Join(errs...)
}
func (r *RpcStub) CallContext(_ context.Context, out interface{}, method string, args ...interface{}) error {
call := r.findExpectedCall(method, args...)
call.Execute(r.t, out)
return nil
}
func (r *RpcStub) findExpectedCall(rpcMethod string, args ...interface{}) ExpectedRpcCall {
var matchResults string
for _, call := range r.expectedCalls {
if err := call.Matches(rpcMethod, args...); err == nil {
return call
} else {
matchResults += fmt.Sprintf("%v: %v", call, err)
}
}
require.Failf(r.t, "No matching expected calls.", matchResults)
return nil
}
type GenericExpectedCall struct {
method string
args []interface{}
result interface{}
}
func NewGetBalanceCall(addr common.Address, block batching.Block, balance *big.Int) ExpectedRpcCall {
return &GenericExpectedCall{
method: "eth_getBalance",
args: []interface{}{addr, block.ArgValue()},
result: (*hexutil.Big)(balance),
}
}
func (c *GenericExpectedCall) Matches(rpcMethod string, args ...interface{}) error {
if rpcMethod != c.method {
return fmt.Errorf("expected method %v but was %v", c.method, rpcMethod)
}
if !assert.ObjectsAreEqualValues(c.args, args) {
return fmt.Errorf("expected args %v but was %v", c.args, args)
}
return nil
}
func (c *GenericExpectedCall) Execute(t *testing.T, out interface{}) {
// I admit I do not understand Go reflection.
// So leverage json.Unmarshal to set the out value correctly.
j, err := json.Marshal(c.result)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(j, out))
}
func (c *GenericExpectedCall) String() string {
return fmt.Sprintf("%v(%v)->%v", c.method, c.args, c.result)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment