Commit bd80e581 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Read created game address from receipt (#10277)

Extracts the game creation logic into reusable code.
parent b4df5c65
package main
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/flags"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/tools"
opservice "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
......@@ -43,23 +43,12 @@ func CreateGame(ctx *cli.Context) error {
return fmt.Errorf("failed to create dispute game factory bindings: %w", err)
}
txCandidate, err := contract.CreateTx(ctx.Context, uint32(traceType), outputRoot, l2BlockNum)
creator := tools.NewGameCreator(contract, txMgr)
gameAddr, err := creator.CreateGame(ctx.Context, outputRoot, traceType, l2BlockNum)
if err != nil {
return fmt.Errorf("failed to create tx: %w", err)
return fmt.Errorf("failed to create game: %w", err)
}
rct, err := txMgr.Send(context.Background(), txCandidate)
if err != nil {
return fmt.Errorf("failed to send tx: %w", err)
}
fmt.Printf("Sent create transaction with status %v, tx_hash: %s\n", rct.Status, rct.TxHash.String())
fetchedGameAddr, err := contract.GetGameFromParameters(context.Background(), uint32(traceType), outputRoot, l2BlockNum)
if err != nil {
return fmt.Errorf("failed to call games: %w", err)
}
fmt.Printf("Fetched Game Address: %s\n", fetchedGameAddr.String())
fmt.Printf("Fetched Game Address: %s\n", gameAddr.String())
return nil
}
......
......@@ -94,7 +94,7 @@ func listGames(ctx context.Context, caller *batching.MultiCaller, factory *contr
fmt.Printf(lineFormat, "Idx", "Game", "Type", "Created (Local)", "L2 Block", "Output Root", "Claims", "Status")
for idx, game := range infos {
if game.err != nil {
return err
return game.err
}
created := time.Unix(int64(game.Timestamp), 0).Format(time.DateTime)
fmt.Printf(lineFormat,
......
......@@ -2,6 +2,7 @@ package contracts
import (
"context"
"errors"
"fmt"
"math/big"
......@@ -11,7 +12,9 @@ import (
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
)
const (
......@@ -21,12 +24,19 @@ const (
methodInitBonds = "initBonds"
methodCreateGame = "create"
methodGames = "games"
eventDisputeGameCreated = "DisputeGameCreated"
)
var (
ErrEventNotFound = errors.New("event not found")
)
type DisputeGameFactoryContract struct {
metrics metrics.ContractMetricer
multiCaller *batching.MultiCaller
contract *batching.BoundContract
abi *abi.ABI
}
func NewDisputeGameFactoryContract(m metrics.ContractMetricer, addr common.Address, caller *batching.MultiCaller) *DisputeGameFactoryContract {
......@@ -35,6 +45,7 @@ func NewDisputeGameFactoryContract(m metrics.ContractMetricer, addr common.Addre
metrics: m,
multiCaller: caller,
contract: batching.NewBoundContract(factoryAbi, addr),
abi: factoryAbi,
}
}
......@@ -157,6 +168,27 @@ func (f *DisputeGameFactoryContract) CreateTx(ctx context.Context, traceType uin
return candidate, err
}
func (f *DisputeGameFactoryContract) DecodeDisputeGameCreatedLog(rcpt *ethTypes.Receipt) (common.Address, uint32, common.Hash, error) {
for _, log := range rcpt.Logs {
if log.Address != f.contract.Addr() {
// Not from this contract
continue
}
name, result, err := f.contract.DecodeEvent(log)
if err != nil {
// Not a valid event
continue
}
if name != eventDisputeGameCreated {
// Not the event we're looking for
continue
}
return result.GetAddress(0), result.GetUint32(1), result.GetHash(2), nil
}
return common.Address{}, 0, common.Hash{}, fmt.Errorf("%w: %v", ErrEventNotFound, eventDisputeGameCreated)
}
func (f *DisputeGameFactoryContract) decodeGame(result *batching.CallResult) types.GameMetadata {
gameType := result.GetUint32(0)
timestamp := result.GetUint64(1)
......
......@@ -14,6 +14,7 @@ import (
batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/snapshots"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require"
)
......@@ -195,6 +196,73 @@ func TestGetGameImpl(t *testing.T) {
require.Equal(t, gameImplAddr, actual)
}
func TestDecodeDisputeGameCreatedLog(t *testing.T) {
_, factory := setupDisputeGameFactoryTest(t)
fdgAbi := snapshots.LoadDisputeGameFactoryABI()
eventAbi := fdgAbi.Events[eventDisputeGameCreated]
gameAddr := common.Address{0x11}
gameType := uint32(4)
rootClaim := common.Hash{0xaa, 0xbb, 0xcc}
createValidReceipt := func() *ethTypes.Receipt {
return &ethTypes.Receipt{
Status: ethTypes.ReceiptStatusSuccessful,
ContractAddress: fdgAddr,
Logs: []*ethTypes.Log{
{
Address: fdgAddr,
Topics: []common.Hash{
eventAbi.ID,
common.BytesToHash(gameAddr.Bytes()),
common.BytesToHash(big.NewInt(int64(gameType)).Bytes()),
rootClaim,
},
},
},
}
}
t.Run("IgnoreIncorrectContract", func(t *testing.T) {
rcpt := createValidReceipt()
rcpt.Logs[0].Address = common.Address{0xff}
_, _, _, err := factory.DecodeDisputeGameCreatedLog(rcpt)
require.ErrorIs(t, err, ErrEventNotFound)
})
t.Run("IgnoreInvalidEvent", func(t *testing.T) {
rcpt := createValidReceipt()
rcpt.Logs[0].Topics = rcpt.Logs[0].Topics[0:2]
_, _, _, err := factory.DecodeDisputeGameCreatedLog(rcpt)
require.ErrorIs(t, err, ErrEventNotFound)
})
t.Run("IgnoreWrongEvent", func(t *testing.T) {
rcpt := createValidReceipt()
rcpt.Logs[0].Topics = []common.Hash{
fdgAbi.Events["ImplementationSet"].ID,
common.BytesToHash(common.Address{0x11}.Bytes()), // Implementation addr
common.BytesToHash(big.NewInt(4).Bytes()), // Game type
}
// Check the log is a valid ImplementationSet
name, _, err := factory.contract.DecodeEvent(rcpt.Logs[0])
require.NoError(t, err)
require.Equal(t, "ImplementationSet", name)
_, _, _, err = factory.DecodeDisputeGameCreatedLog(rcpt)
require.ErrorIs(t, err, ErrEventNotFound)
})
t.Run("ValidEvent", func(t *testing.T) {
rcpt := createValidReceipt()
actualGameAddr, actualGameType, actualRootClaim, err := factory.DecodeDisputeGameCreatedLog(rcpt)
require.NoError(t, err)
require.Equal(t, gameAddr, actualGameAddr)
require.Equal(t, gameType, actualGameType)
require.Equal(t, rootClaim, actualRootClaim)
})
}
func expectGetGame(stubRpc *batchingTest.AbiBasedRpc, idx int, blockHash common.Hash, game types.GameMetadata) {
stubRpc.SetResponse(
factoryAddr,
......
package tools
import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
type GameCreator struct {
contract *contracts.DisputeGameFactoryContract
txMgr txmgr.TxManager
}
func NewGameCreator(contract *contracts.DisputeGameFactoryContract, txMgr txmgr.TxManager) *GameCreator {
return &GameCreator{
contract: contract,
txMgr: txMgr,
}
}
func (g *GameCreator) CreateGame(ctx context.Context, outputRoot common.Hash, traceType uint64, l2BlockNum uint64) (common.Address, error) {
txCandidate, err := g.contract.CreateTx(ctx, uint32(traceType), outputRoot, l2BlockNum)
if err != nil {
return common.Address{}, fmt.Errorf("failed to create tx: %w", err)
}
rct, err := g.txMgr.Send(ctx, txCandidate)
if err != nil {
return common.Address{}, fmt.Errorf("failed to send tx: %w", err)
}
if rct.Status != types.ReceiptStatusSuccessful {
return common.Address{}, fmt.Errorf("game creation transaction (%v) reverted", rct.TxHash.Hex())
}
gameAddr, _, _, err := g.contract.DecodeDisputeGameCreatedLog(rct)
if err != nil {
return common.Address{}, fmt.Errorf("failed to decode game created: %w", err)
}
return gameAddr, nil
}
......@@ -6,11 +6,14 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
var (
ErrUnknownMethod = errors.New("unknown method")
ErrInvalidCall = errors.New("invalid call")
ErrUnknownEvent = errors.New("unknown event")
ErrInvalidEvent = errors.New("invalid event")
)
type BoundContract struct {
......@@ -48,3 +51,40 @@ func (b *BoundContract) DecodeCall(data []byte) (string, *CallResult, error) {
}
return method.Name, &CallResult{args}, nil
}
func (b *BoundContract) DecodeEvent(log *types.Log) (string, *CallResult, error) {
if len(log.Topics) == 0 {
return "", nil, ErrUnknownEvent
}
event, err := b.abi.EventByID(log.Topics[0])
if err != nil {
return "", nil, fmt.Errorf("%w: %v", ErrUnknownEvent, err.Error())
}
argsMap := make(map[string]interface{})
var indexed abi.Arguments
for _, arg := range event.Inputs {
if arg.Indexed {
indexed = append(indexed, arg)
}
}
if err := abi.ParseTopicsIntoMap(argsMap, indexed, log.Topics[1:]); err != nil {
return "", nil, fmt.Errorf("%w indexed topics: %v", ErrInvalidEvent, err.Error())
}
nonIndexed := event.Inputs.NonIndexed()
if len(nonIndexed) > 0 {
if err := nonIndexed.UnpackIntoMap(argsMap, log.Data); err != nil {
return "", nil, fmt.Errorf("%w non-indexed topics: %v", ErrInvalidEvent, err.Error())
}
}
args := make([]interface{}, 0, len(event.Inputs))
for _, input := range event.Inputs {
val, ok := argsMap[input.Name]
if !ok {
return "", nil, fmt.Errorf("%w missing argument: %v", ErrUnknownEvent, input.Name)
}
args = append(args, val)
}
return event.Name, &CallResult{args}, nil
}
......@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/require"
)
......@@ -51,3 +52,87 @@ func TestDecodeCall(t *testing.T) {
require.Zero(t, amount.Cmp(args.GetBigInt(1)))
})
}
func TestDecodeEvent(t *testing.T) {
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
// event Transfer(address indexed from, address indexed to, uint256 amount);
event := testAbi.Events["Transfer"]
contract := NewBoundContract(testAbi, common.Address{0xaa})
t.Run("NoTopics", func(t *testing.T) {
log := &types.Log{}
_, _, err := contract.DecodeEvent(log)
require.ErrorIs(t, err, ErrUnknownEvent)
})
t.Run("UnknownEvent", func(t *testing.T) {
log := &types.Log{
Topics: []common.Hash{{0xaa}},
}
_, _, err := contract.DecodeEvent(log)
require.ErrorIs(t, err, ErrUnknownEvent)
})
t.Run("InvalidTopics", func(t *testing.T) {
amount := big.NewInt(828274)
data, err := event.Inputs.NonIndexed().Pack(amount)
require.NoError(t, err)
log := &types.Log{
Topics: []common.Hash{
event.ID,
common.BytesToHash(common.Address{0xaa}.Bytes()),
// Missing topic for to indexed value
},
Data: data,
}
_, _, err = contract.DecodeEvent(log)
require.ErrorIs(t, err, ErrInvalidEvent)
})
t.Run("MissingData", func(t *testing.T) {
log := &types.Log{
Topics: []common.Hash{
event.ID,
common.BytesToHash(common.Address{0xaa}.Bytes()),
common.BytesToHash(common.Address{0xbb}.Bytes()),
},
}
_, _, err := contract.DecodeEvent(log)
require.ErrorIs(t, err, ErrInvalidEvent)
})
t.Run("InvalidData", func(t *testing.T) {
log := &types.Log{
Topics: []common.Hash{
event.ID,
common.BytesToHash(common.Address{0xaa}.Bytes()),
common.BytesToHash(common.Address{0xbb}.Bytes()),
},
Data: []byte{0xbb, 0xcc},
}
_, _, err := contract.DecodeEvent(log)
require.ErrorIs(t, err, ErrInvalidEvent)
})
t.Run("ValidEvent", func(t *testing.T) {
amount := big.NewInt(828274)
data, err := event.Inputs.NonIndexed().Pack(amount)
require.NoError(t, err)
log := &types.Log{
Topics: []common.Hash{
event.ID,
common.BytesToHash(common.Address{0xaa}.Bytes()),
common.BytesToHash(common.Address{0xbb}.Bytes()),
},
Data: data,
}
name, result, err := contract.DecodeEvent(log)
require.NoError(t, err)
require.Equal(t, name, event.Name)
require.Equal(t, common.Address{0xaa}, result.GetAddress(0))
require.Equal(t, common.Address{0xbb}, result.GetAddress(1))
require.Zerof(t, amount.Cmp(result.GetBigInt(2)), "expected %v but got %v", amount, result.GetBigInt(2))
})
}
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