Commit 3c10d0c7 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

Merge pull request #7712 from ethereum-optimism/aj/multicaller

op-challenger: Move away from generated bindings and batch load claims.
parents fe32ae5c e5f8d680
......@@ -25,7 +25,7 @@ type Responder interface {
}
type ClaimLoader interface {
FetchClaims(ctx context.Context) ([]types.Claim, error)
GetAllClaims(ctx context.Context) ([]types.Claim, error)
}
type Agent struct {
......@@ -136,7 +136,7 @@ func (a *Agent) tryResolve(ctx context.Context) bool {
var errNoResolvableClaims = errors.New("no resolvable claims")
func (a *Agent) tryResolveClaims(ctx context.Context) error {
claims, err := a.loader.FetchClaims(ctx)
claims, err := a.loader.GetAllClaims(ctx)
if err != nil {
return fmt.Errorf("failed to fetch claims: %w", err)
}
......@@ -189,7 +189,7 @@ func (a *Agent) resolveClaims(ctx context.Context) error {
// newGameFromContracts initializes a new game state from the state in the contract
func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
claims, err := a.loader.FetchClaims(ctx)
claims, err := a.loader.GetAllClaims(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch claims: %w", err)
}
......
......@@ -124,7 +124,7 @@ type stubClaimLoader struct {
claims []types.Claim
}
func (s *stubClaimLoader) FetchClaims(ctx context.Context) ([]types.Claim, error) {
func (s *stubClaimLoader) GetAllClaims(ctx context.Context) ([]types.Claim, error) {
s.callCount++
return s.claims, nil
}
......
package contracts
import (
"context"
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common"
)
const (
methodGameDuration = "GAME_DURATION"
methodMaxGameDepth = "MAX_GAME_DEPTH"
methodAbsolutePrestate = "ABSOLUTE_PRESTATE"
methodStatus = "status"
methodClaimCount = "claimDataLen"
methodClaim = "claimData"
)
type FaultDisputeGameContract struct {
multiCaller *batching.MultiCaller
contract *batching.BoundContract
}
func NewFaultDisputeGameContract(addr common.Address, caller *batching.MultiCaller) (*FaultDisputeGameContract, error) {
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
if err != nil {
return nil, fmt.Errorf("failed to load fault dispute game ABI: %w", err)
}
return &FaultDisputeGameContract{
multiCaller: caller,
contract: batching.NewBoundContract(fdgAbi, addr),
}, nil
}
func (f *FaultDisputeGameContract) GetGameDuration(ctx context.Context) (uint64, error) {
result, err := f.multiCaller.SingleCallLatest(ctx, f.contract.Call(methodGameDuration))
if err != nil {
return 0, fmt.Errorf("failed to fetch game duration: %w", err)
}
return result.GetUint64(0), nil
}
func (f *FaultDisputeGameContract) GetMaxGameDepth(ctx context.Context) (uint64, error) {
result, err := f.multiCaller.SingleCallLatest(ctx, f.contract.Call(methodMaxGameDepth))
if err != nil {
return 0, fmt.Errorf("failed to fetch max game depth: %w", err)
}
return result.GetBigInt(0).Uint64(), nil
}
func (f *FaultDisputeGameContract) GetAbsolutePrestateHash(ctx context.Context) (common.Hash, error) {
result, err := f.multiCaller.SingleCallLatest(ctx, f.contract.Call(methodAbsolutePrestate))
if err != nil {
return common.Hash{}, fmt.Errorf("failed to fetch absolute prestate hash: %w", err)
}
return result.GetHash(0), nil
}
func (f *FaultDisputeGameContract) GetStatus(ctx context.Context) (gameTypes.GameStatus, error) {
result, err := f.multiCaller.SingleCallLatest(ctx, f.contract.Call(methodStatus))
if err != nil {
return 0, fmt.Errorf("failed to fetch status: %w", err)
}
return gameTypes.GameStatusFromUint8(result.GetUint8(0))
}
func (f *FaultDisputeGameContract) GetClaimCount(ctx context.Context) (uint64, error) {
result, err := f.multiCaller.SingleCallLatest(ctx, f.contract.Call(methodClaimCount))
if err != nil {
return 0, fmt.Errorf("failed to fetch claim count: %w", err)
}
return result.GetBigInt(0).Uint64(), nil
}
func (f *FaultDisputeGameContract) GetClaim(ctx context.Context, idx uint64) (types.Claim, error) {
result, err := f.multiCaller.SingleCallLatest(ctx, f.contract.Call(methodClaim, new(big.Int).SetUint64(idx)))
if err != nil {
return types.Claim{}, fmt.Errorf("failed to fetch claim %v: %w", idx, err)
}
return f.decodeClaim(result, int(idx)), nil
}
func (f *FaultDisputeGameContract) GetAllClaims(ctx context.Context) ([]types.Claim, error) {
count, err := f.GetClaimCount(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load claim count: %w", err)
}
calls := make([]*batching.ContractCall, count)
for i := uint64(0); i < count; i++ {
calls[i] = f.contract.Call(methodClaim, new(big.Int).SetUint64(i))
}
results, err := f.multiCaller.CallLatest(ctx, calls...)
if err != nil {
return nil, fmt.Errorf("failed to fetch claim data: %w", err)
}
var claims []types.Claim
for idx, result := range results {
claims = append(claims, f.decodeClaim(result, idx))
}
return claims, nil
}
func (f *FaultDisputeGameContract) decodeClaim(result *batching.CallResult, contractIndex int) types.Claim {
parentIndex := result.GetUint32(0)
countered := result.GetBool(1)
claim := result.GetHash(2)
position := result.GetBigInt(3)
clock := result.GetBigInt(4)
return types.Claim{
ClaimData: types.ClaimData{
Value: claim,
Position: types.NewPositionFromGIndex(position),
},
Countered: countered,
Clock: clock.Uint64(),
ContractIndex: contractIndex,
ParentContractIndex: int(parentIndex),
}
}
package contracts
import (
"context"
"math"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestSimpleGetters(t *testing.T) {
tests := []struct {
method string
args []interface{}
result interface{}
expected interface{} // Defaults to expecting the same as result
call func(game *FaultDisputeGameContract) (any, error)
}{
{
method: methodStatus,
result: types.GameStatusChallengerWon,
call: func(game *FaultDisputeGameContract) (any, error) {
return game.GetStatus(context.Background())
},
},
{
method: methodGameDuration,
result: uint64(5566),
call: func(game *FaultDisputeGameContract) (any, error) {
return game.GetGameDuration(context.Background())
},
},
{
method: methodMaxGameDepth,
result: big.NewInt(128),
expected: uint64(128),
call: func(game *FaultDisputeGameContract) (any, error) {
return game.GetMaxGameDepth(context.Background())
},
},
{
method: methodAbsolutePrestate,
result: common.Hash{0xab},
call: func(game *FaultDisputeGameContract) (any, error) {
return game.GetAbsolutePrestateHash(context.Background())
},
},
{
method: methodClaimCount,
result: big.NewInt(9876),
expected: uint64(9876),
call: func(game *FaultDisputeGameContract) (any, error) {
return game.GetClaimCount(context.Background())
},
},
}
for _, test := range tests {
test := test
t.Run(test.method, func(t *testing.T) {
stubRpc, game := setup(t)
stubRpc.SetResponse(test.method, nil, []interface{}{test.result})
status, err := test.call(game)
require.NoError(t, err)
expected := test.expected
if expected == nil {
expected = test.result
}
require.Equal(t, expected, status)
})
}
}
func TestGetClaim(t *testing.T) {
stubRpc, game := setup(t)
idx := big.NewInt(2)
parentIndex := uint32(1)
countered := true
value := common.Hash{0xab}
position := big.NewInt(2)
clock := big.NewInt(1234)
stubRpc.SetResponse(methodClaim, []interface{}{idx}, []interface{}{parentIndex, countered, value, position, clock})
status, err := game.GetClaim(context.Background(), idx.Uint64())
require.NoError(t, err)
require.Equal(t, faultTypes.Claim{
ClaimData: faultTypes.ClaimData{
Value: value,
Position: faultTypes.NewPositionFromGIndex(position),
},
Countered: true,
Clock: 1234,
ContractIndex: int(idx.Uint64()),
ParentContractIndex: 1,
}, status)
}
func TestGetAllClaims(t *testing.T) {
stubRpc, game := setup(t)
claim0 := faultTypes.Claim{
ClaimData: faultTypes.ClaimData{
Value: common.Hash{0xaa},
Position: faultTypes.NewPositionFromGIndex(big.NewInt(1)),
},
Countered: true,
Clock: 1234,
ContractIndex: 0,
ParentContractIndex: math.MaxUint32,
}
claim1 := faultTypes.Claim{
ClaimData: faultTypes.ClaimData{
Value: common.Hash{0xab},
Position: faultTypes.NewPositionFromGIndex(big.NewInt(2)),
},
Countered: true,
Clock: 4455,
ContractIndex: 1,
ParentContractIndex: 0,
}
claim2 := faultTypes.Claim{
ClaimData: faultTypes.ClaimData{
Value: common.Hash{0xbb},
Position: faultTypes.NewPositionFromGIndex(big.NewInt(6)),
},
Countered: false,
Clock: 7777,
ContractIndex: 2,
ParentContractIndex: 1,
}
expectedClaims := []faultTypes.Claim{claim0, claim1, claim2}
stubRpc.SetResponse(methodClaimCount, nil, []interface{}{big.NewInt(int64(len(expectedClaims)))})
for _, claim := range expectedClaims {
expectGetClaim(stubRpc, claim)
}
claims, err := game.GetAllClaims(context.Background())
require.NoError(t, err)
require.Equal(t, expectedClaims, claims)
}
func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, claim faultTypes.Claim) {
stubRpc.SetResponse(
methodClaim,
[]interface{}{big.NewInt(int64(claim.ContractIndex))},
[]interface{}{
uint32(claim.ParentContractIndex),
claim.Countered,
claim.Value,
claim.Position.ToGIndex(),
big.NewInt(int64(claim.Clock)),
})
}
func setup(t *testing.T) (*batchingTest.AbiBasedRpc, *FaultDisputeGameContract) {
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err)
address := common.HexToAddress("0x24112842371dFC380576ebb09Ae16Cb6B6caD7CB")
stubRpc := batchingTest.NewAbiBasedRpc(t, fdgAbi, address)
caller := batching.NewMultiCaller(stubRpc, 100)
game, err := NewFaultDisputeGameContract(address, caller)
require.NoError(t, err)
return stubRpc, game
}
......@@ -5,22 +5,23 @@ import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/responder"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
type actor func(ctx context.Context) error
type GameInfo interface {
GetGameStatus(context.Context) (gameTypes.GameStatus, error)
GetStatus(context.Context) (gameTypes.GameStatus, error)
GetClaimCount(context.Context) (uint64, error)
}
......@@ -42,18 +43,16 @@ func NewGamePlayer(
dir string,
addr common.Address,
txMgr txmgr.TxManager,
client bind.ContractCaller,
client *ethclient.Client,
creator resourceCreator,
) (*GamePlayer, error) {
logger = logger.New("game", addr)
contract, err := bindings.NewFaultDisputeGameCaller(addr, client)
loader, err := contracts.NewFaultDisputeGameContract(addr, batching.NewMultiCaller(client.Client(), 100))
if err != nil {
return nil, fmt.Errorf("failed to bind the fault dispute game contract: %w", err)
return nil, fmt.Errorf("failed to create fault dispute game contract wrapper: %w", err)
}
loader := NewLoader(contract)
status, err := loader.GetGameStatus(ctx)
status, err := loader.GetStatus(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch game status: %w", err)
}
......@@ -72,7 +71,7 @@ func NewGamePlayer(
}, nil
}
gameDepth, err := loader.FetchGameDepth(ctx)
gameDepth, err := loader.GetMaxGameDepth(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch the game depth: %w", err)
}
......@@ -114,7 +113,7 @@ func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus {
if err := g.act(ctx); err != nil {
g.logger.Error("Error when acting on game", "err", err)
}
status, err := g.loader.GetGameStatus(ctx)
status, err := g.loader.GetStatus(ctx)
if err != nil {
g.logger.Warn("Unable to retrieve game status", "err", err)
return gameTypes.GameStatusInProgress
......@@ -148,7 +147,7 @@ func (g *GamePlayer) logGameStatus(ctx context.Context, status gameTypes.GameSta
}
type PrestateLoader interface {
FetchAbsolutePrestateHash(ctx context.Context) (common.Hash, error)
GetAbsolutePrestateHash(ctx context.Context) (common.Hash, error)
}
// ValidateAbsolutePrestate validates the absolute prestate of the fault game.
......@@ -157,7 +156,7 @@ func ValidateAbsolutePrestate(ctx context.Context, trace types.TraceProvider, lo
if err != nil {
return fmt.Errorf("failed to get the trace provider's absolute prestate: %w", err)
}
onchainPrestate, err := loader.FetchAbsolutePrestateHash(ctx)
onchainPrestate, err := loader.GetAbsolutePrestateHash(ctx)
if err != nil {
return fmt.Errorf("failed to get the onchain absolute prestate: %w", err)
}
......
......@@ -181,7 +181,7 @@ func (s *stubGameState) Act(ctx context.Context) error {
return s.actErr
}
func (s *stubGameState) GetGameStatus(ctx context.Context) (gameTypes.GameStatus, error) {
func (s *stubGameState) GetStatus(ctx context.Context) (gameTypes.GameStatus, error) {
return s.status, nil
}
......@@ -234,7 +234,7 @@ func newMockPrestateLoader(prestateError bool, prestate common.Hash) *mockLoader
prestate: prestate,
}
}
func (m *mockLoader) FetchAbsolutePrestateHash(ctx context.Context) (common.Hash, error) {
func (m *mockLoader) GetAbsolutePrestateHash(ctx context.Context) (common.Hash, error) {
if m.prestateError {
return common.Hash{}, mockLoaderError
}
......
......@@ -12,8 +12,8 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
......@@ -33,7 +33,7 @@ func RegisterGameTypes(
m metrics.Metricer,
cfg *config.Config,
txMgr txmgr.TxManager,
client bind.ContractCaller,
client *ethclient.Client,
) {
if cfg.TraceTypeEnabled(config.TraceTypeCannon) {
resourceCreator := func(addr common.Address, gameDepth uint64, dir string) (faultTypes.TraceProvider, faultTypes.OracleUpdater, error) {
......
package batching
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
type BoundContract struct {
abi *abi.ABI
addr common.Address
}
func NewBoundContract(abi *abi.ABI, addr common.Address) *BoundContract {
return &BoundContract{
abi: abi,
addr: addr,
}
}
func (b *BoundContract) Call(method string, args ...interface{}) *ContractCall {
return NewContractCall(b.abi, b.addr, method, args...)
}
type ContractCall struct {
Abi *abi.ABI
Addr common.Address
Method string
Args []interface{}
}
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)
}
msg := ethereum.CallMsg{
To: &c.Addr,
Data: data,
}
return toCallArg(msg), 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 toCallArg(msg ethereum.CallMsg) interface{} {
arg := map[string]interface{}{
"from": msg.From,
"to": msg.To,
}
if len(msg.Data) > 0 {
arg["input"] = hexutil.Bytes(msg.Data)
}
if msg.Value != nil {
arg["value"] = (*hexutil.Big)(msg.Value)
}
if msg.Gas != 0 {
arg["gas"] = hexutil.Uint64(msg.Gas)
}
if msg.GasPrice != nil {
arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice)
}
return arg
}
type CallResult struct {
out []interface{}
}
func (c *CallResult) GetUint8(i int) uint8 {
return *abi.ConvertType(c.out[i], new(uint8)).(*uint8)
}
func (c *CallResult) GetUint32(i int) uint32 {
return *abi.ConvertType(c.out[i], new(uint32)).(*uint32)
}
func (c *CallResult) GetUint64(i int) uint64 {
return *abi.ConvertType(c.out[i], new(uint64)).(*uint64)
}
func (c *CallResult) GetBool(i int) bool {
return *abi.ConvertType(c.out[i], new(bool)).(*bool)
}
func (c *CallResult) GetHash(i int) common.Hash {
return *abi.ConvertType(c.out[i], new([32]byte)).(*[32]byte)
}
func (c *CallResult) GetBigInt(i int) *big.Int {
return *abi.ConvertType(c.out[i], new(*big.Int)).(**big.Int)
}
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))
args, err := call.ToCallArgs()
require.NoError(t, err)
argMap, ok := args.(map[string]interface{})
require.True(t, ok)
require.Equal(t, argMap["from"], common.Address{})
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_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) {
tests := []struct {
name string
getter func(result *CallResult, i int) interface{}
expected interface{}
}{
{
name: "GetUint8",
getter: func(result *CallResult, i int) interface{} {
return result.GetUint8(i)
},
expected: uint8(12),
},
{
name: "GetUint32",
getter: func(result *CallResult, i int) interface{} {
return result.GetUint32(i)
},
expected: uint32(12346),
},
{
name: "GetUint64",
getter: func(result *CallResult, i int) interface{} {
return result.GetUint64(i)
},
expected: uint64(12346),
},
{
name: "GetBool",
getter: func(result *CallResult, i int) interface{} {
return result.GetBool(i)
},
expected: true,
},
{
name: "GetHash",
getter: func(result *CallResult, i int) interface{} {
return result.GetHash(i)
},
expected: ([32]byte)(common.Hash{0xaa, 0xbb, 0xcc}),
},
{
name: "GetBigInt",
getter: func(result *CallResult, i int) interface{} {
return result.GetBigInt(i)
},
expected: big.NewInt(2398423),
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
callResult := &CallResult{[]interface{}{nil, 0, "abc", test.expected, "xyz", 3, nil}}
actual := test.getter(callResult, 3)
require.EqualValues(t, test.expected, actual)
})
}
}
package batching
import (
"context"
"fmt"
"io"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)
type EthRpc interface {
CallContext(ctx context.Context, out interface{}, method string, args ...interface{}) error
BatchCallContext(ctx context.Context, b []rpc.BatchElem) error
}
type MultiCaller struct {
rpc EthRpc
batchSize int
}
func NewMultiCaller(rpc EthRpc, batchSize int) *MultiCaller {
return &MultiCaller{
rpc: rpc,
batchSize: batchSize,
}
}
func (m *MultiCaller) SingleCallLatest(ctx context.Context, call *ContractCall) (*CallResult, error) {
results, err := m.CallLatest(ctx, call)
if err != nil {
return nil, err
}
return results[0], nil
}
func (m *MultiCaller) CallLatest(ctx context.Context, calls ...*ContractCall) ([]*CallResult, error) {
keys := make([]interface{}, len(calls))
for i := 0; i < len(calls); i++ {
args, err := calls[i].ToCallArgs()
if err != nil {
return nil, err
}
keys[i] = args
}
fetcher := NewIterativeBatchCall[interface{}, *hexutil.Bytes](
keys,
func(args interface{}) (*hexutil.Bytes, rpc.BatchElem) {
out := new(hexutil.Bytes)
return out, rpc.BatchElem{
Method: "eth_call",
Args: []interface{}{args, "latest"},
Result: &out,
}
},
m.rpc.BatchCallContext,
m.rpc.CallContext,
m.batchSize)
for {
if err := fetcher.Fetch(ctx); err == io.EOF {
break
} else if err != nil {
return nil, fmt.Errorf("failed to fetch claims: %w", err)
}
}
results, err := fetcher.Result()
if err != nil {
return nil, fmt.Errorf("failed to get batch call results: %w", err)
}
callResults := make([]*CallResult, len(results))
for i, result := range results {
call := calls[i]
out, err := call.Unpack(*result)
if err != nil {
return nil, fmt.Errorf("failed to unpack result: %w", err)
}
callResults[i] = out
}
return callResults, nil
}
package test
import (
"context"
"encoding/json"
"errors"
"fmt"
"testing"
"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"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
)
type expectedCall struct {
args []interface{}
packedArgs []byte
outputs []interface{}
}
func (e *expectedCall) String() string {
return fmt.Sprintf("{args: %v, outputs: %v}", e.args, e.outputs)
}
type AbiBasedRpc struct {
t *testing.T
abi *abi.ABI
addr common.Address
expectedCalls map[string][]*expectedCall
}
func NewAbiBasedRpc(t *testing.T, contractAbi *abi.ABI, addr common.Address) *AbiBasedRpc {
return &AbiBasedRpc{
t: t,
abi: contractAbi,
addr: addr,
expectedCalls: make(map[string][]*expectedCall),
}
}
func (l *AbiBasedRpc) SetResponse(method string, expected []interface{}, output []interface{}) {
if expected == nil {
expected = []interface{}{}
}
if output == nil {
output = []interface{}{}
}
abiMethod, ok := l.abi.Methods[method]
require.Truef(l.t, ok, "No method: %v", method)
packedArgs, err := abiMethod.Inputs.Pack(expected...)
require.NoErrorf(l.t, err, "Invalid expected arguments for method %v: %v", method, expected)
l.expectedCalls[method] = append(l.expectedCalls[method], &expectedCall{
args: expected,
packedArgs: packedArgs,
outputs: output,
})
}
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) CallContext(_ context.Context, out interface{}, method string, args ...interface{}) error {
require.Equal(l.t, "eth_call", method)
require.Len(l.t, args, 2)
require.Equal(l.t, "latest", args[1])
callOpts, ok := args[0].(map[string]any)
require.True(l.t, ok)
require.Equal(l.t, &l.addr, callOpts["to"])
data, ok := callOpts["input"].(hexutil.Bytes)
require.True(l.t, ok)
abiMethod, err := l.abi.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 slices.Equal(candidate.packedArgs, argData) {
call = candidate
break
}
}
require.NotNilf(l.t, call, "No expected calls to %v with arguments: %v\nExpected calls: %v", abiMethod.Name, args, expectedCalls)
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
}
package batching
import (
"context"
"github.com/ethereum/go-ethereum/rpc"
)
type BatchCallContextFn func(ctx context.Context, b []rpc.BatchElem) error
type CallContextFn func(ctx context.Context, result any, method string, args ...any) error
......@@ -4,16 +4,17 @@ import (
"context"
"fmt"
"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/core/rawdb"
)
type DebugClient struct {
callContext CallContextFn
callContext batching.CallContextFn
}
func NewDebugClient(callContext CallContextFn) *DebugClient {
func NewDebugClient(callContext batching.CallContextFn) *DebugClient {
return &DebugClient{callContext}
}
......
......@@ -6,6 +6,7 @@ import (
"io"
"sync"
"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/core/types"
......@@ -391,7 +392,7 @@ type receiptsFetchingJob struct {
receiptHash common.Hash
txHashes []common.Hash
fetcher *IterativeBatchCall[common.Hash, *types.Receipt]
fetcher *batching.IterativeBatchCall[common.Hash, *types.Receipt]
// [OPTIONAL] RethDB path to fetch receipts from
rethDbPath string
......@@ -424,7 +425,7 @@ type ReceiptsRequester interface {
func (job *receiptsFetchingJob) runFetcher(ctx context.Context) error {
if job.fetcher == nil {
// start new work
job.fetcher = NewIterativeBatchCall[common.Hash, *types.Receipt](
job.fetcher = batching.NewIterativeBatchCall[common.Hash, *types.Receipt](
job.txHashes,
makeReceiptRequest,
job.client.BatchCallContext,
......
package sources
import (
"context"
"fmt"
"math/big"
"strings"
......@@ -18,10 +17,6 @@ import (
"github.com/ethereum-optimism/optimism/op-service/eth"
)
type BatchCallContextFn func(ctx context.Context, b []rpc.BatchElem) error
type CallContextFn func(ctx context.Context, result any, method string, args ...any) error
// Note: these types are used, instead of the geth types, to enable:
// - batched calls of many block requests (standard bindings do extra uncle-header fetches, cannot be batched nicely)
// - ignore uncle data (does not even exist anymore post-Merge)
......
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