Commit 2c1ccf85 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

Merge pull request #8357 from ethereum-optimism/aj/bisection-game-contract-wrapper

challenger: Begin supporting new OutputBisectionGame contract
parents a0b6d01e f9cf03f3
package contracts
import (
"context"
"fmt"
"math/big"
"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-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
)
const (
methodGameDuration = "GAME_DURATION"
methodMaxGameDepth = "MAX_GAME_DEPTH"
methodAbsolutePrestate = "ABSOLUTE_PRESTATE"
methodStatus = "status"
methodClaimCount = "claimDataLen"
methodClaim = "claimData"
methodL1Head = "l1Head"
methodResolve = "resolve"
methodResolveClaim = "resolveClaim"
methodAttack = "attack"
methodDefend = "defend"
methodStep = "step"
methodAddLocalData = "addLocalData"
methodVM = "VM"
)
type disputeGameContract struct {
multiCaller *batching.MultiCaller
contract *batching.BoundContract
}
// contractProposal matches the structure for output root proposals used by the contracts.
// It must exactly match the contract structure. The exposed API uses Proposal to decouple the contract
// and challenger representations of the proposal data.
type contractProposal struct {
Index *big.Int
L2BlockNumber *big.Int
OutputRoot common.Hash
}
type Proposal struct {
L2BlockNumber *big.Int
OutputRoot common.Hash
}
func asProposal(p contractProposal) Proposal {
return Proposal{
L2BlockNumber: p.L2BlockNumber,
OutputRoot: p.OutputRoot,
}
}
func (f *disputeGameContract) GetGameDuration(ctx context.Context) (uint64, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, 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 *disputeGameContract) GetMaxGameDepth(ctx context.Context) (uint64, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, 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 *disputeGameContract) GetAbsolutePrestateHash(ctx context.Context) (common.Hash, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, 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 *disputeGameContract) GetL1Head(ctx context.Context) (common.Hash, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodL1Head))
if err != nil {
return common.Hash{}, fmt.Errorf("failed to fetch L1 head: %w", err)
}
return result.GetHash(0), nil
}
func (f *disputeGameContract) GetStatus(ctx context.Context) (gameTypes.GameStatus, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, 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 *disputeGameContract) GetClaimCount(ctx context.Context) (uint64, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, 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 *disputeGameContract) GetClaim(ctx context.Context, idx uint64) (types.Claim, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, 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 *disputeGameContract) 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.Call(ctx, batching.BlockLatest, 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 *disputeGameContract) vm(ctx context.Context) (*VMContract, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodVM))
if err != nil {
return nil, fmt.Errorf("failed to fetch VM addr: %w", err)
}
vmAddr := result.GetAddress(0)
return NewVMContract(vmAddr, f.multiCaller)
}
func (f *disputeGameContract) AttackTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) {
call := f.contract.Call(methodAttack, new(big.Int).SetUint64(parentContractIndex), pivot)
return call.ToTxCandidate()
}
func (f *disputeGameContract) DefendTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) {
call := f.contract.Call(methodDefend, new(big.Int).SetUint64(parentContractIndex), pivot)
return call.ToTxCandidate()
}
func (f *disputeGameContract) StepTx(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) (txmgr.TxCandidate, error) {
call := f.contract.Call(methodStep, new(big.Int).SetUint64(claimIdx), isAttack, stateData, proof)
return call.ToTxCandidate()
}
func (f *disputeGameContract) CallResolveClaim(ctx context.Context, claimIdx uint64) error {
call := f.resolveClaimCall(claimIdx)
_, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, call)
if err != nil {
return fmt.Errorf("failed to call resolve claim: %w", err)
}
return nil
}
func (f *disputeGameContract) ResolveClaimTx(claimIdx uint64) (txmgr.TxCandidate, error) {
call := f.resolveClaimCall(claimIdx)
return call.ToTxCandidate()
}
func (f *disputeGameContract) resolveClaimCall(claimIdx uint64) *batching.ContractCall {
return f.contract.Call(methodResolveClaim, new(big.Int).SetUint64(claimIdx))
}
func (f *disputeGameContract) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
call := f.resolveCall()
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, call)
if err != nil {
return gameTypes.GameStatusInProgress, fmt.Errorf("failed to call resolve: %w", err)
}
return gameTypes.GameStatusFromUint8(result.GetUint8(0))
}
func (f *disputeGameContract) ResolveTx() (txmgr.TxCandidate, error) {
call := f.resolveCall()
return call.ToTxCandidate()
}
func (f *disputeGameContract) resolveCall() *batching.ContractCall {
return f.contract.Call(methodResolve)
}
func (f *disputeGameContract) 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"
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"
)
var (
fdgAddr = common.HexToAddress("0x24112842371dFC380576ebb09Ae16Cb6B6caD7CB")
vmAddr = common.HexToAddress("0x33332842371dFC380576ebb09Ae16Cb6B6c3333")
oracleAddr = common.HexToAddress("0x44442842371dFC380576ebb09Ae16Cb6B6ca4444")
)
type disputeGameSetupFunc func(t *testing.T) (*batchingTest.AbiBasedRpc, *disputeGameContract)
func runCommonDisputeGameTests(t *testing.T, setup disputeGameSetupFunc) {
tests := []struct {
name string
method func(t *testing.T, setup disputeGameSetupFunc)
}{
{"SimpleGetters", runSimpleGettersTest},
{"GetClaim", runGetClaimTest},
{"GetAllClaims", runGetAllClaimsTest},
{"CallResolveClaim", runCallResolveClaimTest},
{"ResolveClaimTx", runResolveClaimTxTest},
{"ResolveTx", runResolveTxTest},
{"AttackTx", runAttackTxTest},
{"DefendTx", runDefendTxTest},
{"StepTx", runStepTxTest},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
test.method(t, setup)
})
}
}
func runSimpleGettersTest(t *testing.T, setup disputeGameSetupFunc) {
tests := []struct {
method string
args []interface{}
result interface{}
expected interface{} // Defaults to expecting the same as result
call func(game *disputeGameContract) (any, error)
}{
{
method: methodStatus,
result: types.GameStatusChallengerWon,
call: func(game *disputeGameContract) (any, error) {
return game.GetStatus(context.Background())
},
},
{
method: methodGameDuration,
result: uint64(5566),
call: func(game *disputeGameContract) (any, error) {
return game.GetGameDuration(context.Background())
},
},
{
method: methodMaxGameDepth,
result: big.NewInt(128),
expected: uint64(128),
call: func(game *disputeGameContract) (any, error) {
return game.GetMaxGameDepth(context.Background())
},
},
{
method: methodAbsolutePrestate,
result: common.Hash{0xab},
call: func(game *disputeGameContract) (any, error) {
return game.GetAbsolutePrestateHash(context.Background())
},
},
{
method: methodClaimCount,
result: big.NewInt(9876),
expected: uint64(9876),
call: func(game *disputeGameContract) (any, error) {
return game.GetClaimCount(context.Background())
},
},
{
method: methodL1Head,
result: common.Hash{0xdd, 0xbb},
call: func(game *disputeGameContract) (any, error) {
return game.GetL1Head(context.Background())
},
},
{
method: methodResolve,
result: types.GameStatusInProgress,
call: func(game *disputeGameContract) (any, error) {
return game.CallResolve(context.Background())
},
},
}
for _, test := range tests {
test := test
t.Run(test.method, func(t *testing.T) {
stubRpc, game := setup(t)
stubRpc.SetResponse(fdgAddr, test.method, batching.BlockLatest, 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 runGetClaimTest(t *testing.T, setup disputeGameSetupFunc) {
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(fdgAddr, methodClaim, batching.BlockLatest, []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 runGetAllClaimsTest(t *testing.T, setup disputeGameSetupFunc) {
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(fdgAddr, methodClaimCount, batching.BlockLatest, 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 runCallResolveClaimTest(t *testing.T, setup disputeGameSetupFunc) {
stubRpc, game := setup(t)
stubRpc.SetResponse(fdgAddr, methodResolveClaim, batching.BlockLatest, []interface{}{big.NewInt(123)}, nil)
err := game.CallResolveClaim(context.Background(), 123)
require.NoError(t, err)
}
func runResolveClaimTxTest(t *testing.T, setup disputeGameSetupFunc) {
stubRpc, game := setup(t)
stubRpc.SetResponse(fdgAddr, methodResolveClaim, batching.BlockLatest, []interface{}{big.NewInt(123)}, nil)
tx, err := game.ResolveClaimTx(123)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func runResolveTxTest(t *testing.T, setup disputeGameSetupFunc) {
stubRpc, game := setup(t)
stubRpc.SetResponse(fdgAddr, methodResolve, batching.BlockLatest, nil, nil)
tx, err := game.ResolveTx()
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func runAttackTxTest(t *testing.T, setup disputeGameSetupFunc) {
stubRpc, game := setup(t)
value := common.Hash{0xaa}
stubRpc.SetResponse(fdgAddr, methodAttack, batching.BlockLatest, []interface{}{big.NewInt(111), value}, nil)
tx, err := game.AttackTx(111, value)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func runDefendTxTest(t *testing.T, setup disputeGameSetupFunc) {
stubRpc, game := setup(t)
value := common.Hash{0xaa}
stubRpc.SetResponse(fdgAddr, methodDefend, batching.BlockLatest, []interface{}{big.NewInt(111), value}, nil)
tx, err := game.DefendTx(111, value)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func runStepTxTest(t *testing.T, setup disputeGameSetupFunc) {
stubRpc, game := setup(t)
stateData := []byte{1, 2, 3}
proofData := []byte{4, 5, 6, 7, 8, 9}
stubRpc.SetResponse(fdgAddr, methodStep, batching.BlockLatest, []interface{}{big.NewInt(111), true, stateData, proofData}, nil)
tx, err := game.StepTx(111, true, stateData, proofData)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, claim faultTypes.Claim) {
stubRpc.SetResponse(
fdgAddr,
methodClaim,
batching.BlockLatest,
[]interface{}{big.NewInt(int64(claim.ContractIndex))},
[]interface{}{
uint32(claim.ParentContractIndex),
claim.Countered,
claim.Value,
claim.Position.ToGIndex(),
big.NewInt(int64(claim.Clock)),
})
}
...@@ -7,54 +7,17 @@ import ( ...@@ -7,54 +7,17 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "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-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
) )
const ( const (
methodGameDuration = "GAME_DURATION" methodProposals = "proposals"
methodMaxGameDepth = "MAX_GAME_DEPTH"
methodAbsolutePrestate = "ABSOLUTE_PRESTATE"
methodStatus = "status"
methodClaimCount = "claimDataLen"
methodClaim = "claimData"
methodL1Head = "l1Head"
methodProposals = "proposals"
methodResolve = "resolve"
methodResolveClaim = "resolveClaim"
methodAttack = "attack"
methodDefend = "defend"
methodStep = "step"
methodAddLocalData = "addLocalData"
methodVM = "VM"
) )
type FaultDisputeGameContract struct { type FaultDisputeGameContract struct {
multiCaller *batching.MultiCaller disputeGameContract
contract *batching.BoundContract
}
// contractProposal matches the structure for output root proposals used by the contracts.
// It must exactly match the contract structure. The exposed API uses Proposal to decouple the contract
// and challenger representations of the proposal data.
type contractProposal struct {
Index *big.Int
L2BlockNumber *big.Int
OutputRoot common.Hash
}
type Proposal struct {
L2BlockNumber *big.Int
OutputRoot common.Hash
}
func asProposal(p contractProposal) Proposal {
return Proposal{
L2BlockNumber: p.L2BlockNumber,
OutputRoot: p.OutputRoot,
}
} }
func NewFaultDisputeGameContract(addr common.Address, caller *batching.MultiCaller) (*FaultDisputeGameContract, error) { func NewFaultDisputeGameContract(addr common.Address, caller *batching.MultiCaller) (*FaultDisputeGameContract, error) {
...@@ -64,43 +27,13 @@ func NewFaultDisputeGameContract(addr common.Address, caller *batching.MultiCall ...@@ -64,43 +27,13 @@ func NewFaultDisputeGameContract(addr common.Address, caller *batching.MultiCall
} }
return &FaultDisputeGameContract{ return &FaultDisputeGameContract{
multiCaller: caller, disputeGameContract: disputeGameContract{
contract: batching.NewBoundContract(fdgAbi, addr), multiCaller: caller,
contract: batching.NewBoundContract(fdgAbi, addr),
},
}, nil }, nil
} }
func (f *FaultDisputeGameContract) GetGameDuration(ctx context.Context) (uint64, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, 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.SingleCall(ctx, batching.BlockLatest, 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.SingleCall(ctx, batching.BlockLatest, 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) GetL1Head(ctx context.Context) (common.Hash, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodL1Head))
if err != nil {
return common.Hash{}, fmt.Errorf("failed to fetch L1 head: %w", err)
}
return result.GetHash(0), nil
}
// GetProposals returns the agreed and disputed proposals // GetProposals returns the agreed and disputed proposals
func (f *FaultDisputeGameContract) GetProposals(ctx context.Context) (Proposal, Proposal, error) { func (f *FaultDisputeGameContract) GetProposals(ctx context.Context) (Proposal, Proposal, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodProposals)) result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodProposals))
...@@ -114,114 +47,7 @@ func (f *FaultDisputeGameContract) GetProposals(ctx context.Context) (Proposal, ...@@ -114,114 +47,7 @@ func (f *FaultDisputeGameContract) GetProposals(ctx context.Context) (Proposal,
return asProposal(agreed), asProposal(disputed), nil return asProposal(agreed), asProposal(disputed), nil
} }
func (f *FaultDisputeGameContract) GetStatus(ctx context.Context) (gameTypes.GameStatus, error) { func (f *FaultDisputeGameContract) UpdateOracleTx(ctx context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, 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.SingleCall(ctx, batching.BlockLatest, 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.SingleCall(ctx, batching.BlockLatest, 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.Call(ctx, batching.BlockLatest, 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) vm(ctx context.Context) (*VMContract, error) {
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, f.contract.Call(methodVM))
if err != nil {
return nil, fmt.Errorf("failed to fetch VM addr: %w", err)
}
vmAddr := result.GetAddress(0)
return NewVMContract(vmAddr, f.multiCaller)
}
func (f *FaultDisputeGameContract) AttackTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) {
call := f.contract.Call(methodAttack, new(big.Int).SetUint64(parentContractIndex), pivot)
return call.ToTxCandidate()
}
func (f *FaultDisputeGameContract) DefendTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) {
call := f.contract.Call(methodDefend, new(big.Int).SetUint64(parentContractIndex), pivot)
return call.ToTxCandidate()
}
func (f *FaultDisputeGameContract) StepTx(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) (txmgr.TxCandidate, error) {
call := f.contract.Call(methodStep, new(big.Int).SetUint64(claimIdx), isAttack, stateData, proof)
return call.ToTxCandidate()
}
func (f *FaultDisputeGameContract) CallResolveClaim(ctx context.Context, claimIdx uint64) error {
call := f.resolveClaimCall(claimIdx)
_, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, call)
if err != nil {
return fmt.Errorf("failed to call resolve claim: %w", err)
}
return nil
}
func (f *FaultDisputeGameContract) ResolveClaimTx(claimIdx uint64) (txmgr.TxCandidate, error) {
call := f.resolveClaimCall(claimIdx)
return call.ToTxCandidate()
}
func (f *FaultDisputeGameContract) resolveClaimCall(claimIdx uint64) *batching.ContractCall {
return f.contract.Call(methodResolveClaim, new(big.Int).SetUint64(claimIdx))
}
func (f *FaultDisputeGameContract) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
call := f.resolveCall()
result, err := f.multiCaller.SingleCall(ctx, batching.BlockLatest, call)
if err != nil {
return gameTypes.GameStatusInProgress, fmt.Errorf("failed to call resolve: %w", err)
}
return gameTypes.GameStatusFromUint8(result.GetUint8(0))
}
func (f *FaultDisputeGameContract) ResolveTx() (txmgr.TxCandidate, error) {
call := f.resolveCall()
return call.ToTxCandidate()
}
func (f *FaultDisputeGameContract) resolveCall() *batching.ContractCall {
return f.contract.Call(methodResolve)
}
func (f *FaultDisputeGameContract) UpdateOracleTx(ctx context.Context, data *types.PreimageOracleData) (txmgr.TxCandidate, error) {
if data.IsLocal { if data.IsLocal {
return f.addLocalDataTx(data) return f.addLocalDataTx(data)
} }
...@@ -249,21 +75,3 @@ func (f *FaultDisputeGameContract) addGlobalDataTx(ctx context.Context, data *ty ...@@ -249,21 +75,3 @@ func (f *FaultDisputeGameContract) addGlobalDataTx(ctx context.Context, data *ty
} }
return oracle.AddGlobalDataTx(data) return oracle.AddGlobalDataTx(data)
} }
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),
}
}
...@@ -2,103 +2,26 @@ package contracts ...@@ -2,103 +2,26 @@ package contracts
import ( import (
"context" "context"
"math"
"math/big" "math/big"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" 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" "github.com/ethereum-optimism/optimism/op-service/sources/batching"
batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test" batchingTest "github.com/ethereum-optimism/optimism/op-service/sources/batching/test"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var ( func TestFaultDisputeGameContract_CommonTests(t *testing.T) {
fdgAddr = common.HexToAddress("0x24112842371dFC380576ebb09Ae16Cb6B6caD7CB") runCommonDisputeGameTests(t, func(t *testing.T) (*batchingTest.AbiBasedRpc, *disputeGameContract) {
vmAddr = common.HexToAddress("0x33332842371dFC380576ebb09Ae16Cb6B6c3333") stubRpc, contract := setupFaultDisputeGameTest(t)
oracleAddr = common.HexToAddress("0x44442842371dFC380576ebb09Ae16Cb6B6ca4444") return stubRpc, &contract.disputeGameContract
) })
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())
},
},
{
method: methodL1Head,
result: common.Hash{0xdd, 0xbb},
call: func(game *FaultDisputeGameContract) (any, error) {
return game.GetL1Head(context.Background())
},
},
{
method: methodResolve,
result: types.GameStatusInProgress,
call: func(game *FaultDisputeGameContract) (any, error) {
return game.CallResolve(context.Background())
},
},
}
for _, test := range tests {
test := test
t.Run(test.method, func(t *testing.T) {
stubRpc, game := setup(t)
stubRpc.SetResponse(fdgAddr, test.method, batching.BlockLatest, 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 TestGetProposals(t *testing.T) { func TestGetProposals(t *testing.T) {
stubRpc, game := setup(t) stubRpc, game := setupFaultDisputeGameTest(t)
agreedIndex := big.NewInt(5) agreedIndex := big.NewInt(5)
agreedBlockNum := big.NewInt(6) agreedBlockNum := big.NewInt(6)
agreedRoot := common.Hash{0xaa} agreedRoot := common.Hash{0xaa}
...@@ -132,125 +55,9 @@ func TestGetProposals(t *testing.T) { ...@@ -132,125 +55,9 @@ func TestGetProposals(t *testing.T) {
require.Equal(t, expectedDisputed, actualDisputed) require.Equal(t, expectedDisputed, actualDisputed)
} }
func TestGetClaim(t *testing.T) { func TestFaultDisputeGame_UpdateOracleTx(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(fdgAddr, methodClaim, batching.BlockLatest, []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(fdgAddr, methodClaimCount, batching.BlockLatest, 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 TestCallResolveClaim(t *testing.T) {
stubRpc, game := setup(t)
stubRpc.SetResponse(fdgAddr, methodResolveClaim, batching.BlockLatest, []interface{}{big.NewInt(123)}, nil)
err := game.CallResolveClaim(context.Background(), 123)
require.NoError(t, err)
}
func TestResolveClaimTx(t *testing.T) {
stubRpc, game := setup(t)
stubRpc.SetResponse(fdgAddr, methodResolveClaim, batching.BlockLatest, []interface{}{big.NewInt(123)}, nil)
tx, err := game.ResolveClaimTx(123)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func TestResolveTx(t *testing.T) {
stubRpc, game := setup(t)
stubRpc.SetResponse(fdgAddr, methodResolve, batching.BlockLatest, nil, nil)
tx, err := game.ResolveTx()
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func TestAttackTx(t *testing.T) {
stubRpc, game := setup(t)
value := common.Hash{0xaa}
stubRpc.SetResponse(fdgAddr, methodAttack, batching.BlockLatest, []interface{}{big.NewInt(111), value}, nil)
tx, err := game.AttackTx(111, value)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func TestDefendTx(t *testing.T) {
stubRpc, game := setup(t)
value := common.Hash{0xaa}
stubRpc.SetResponse(fdgAddr, methodDefend, batching.BlockLatest, []interface{}{big.NewInt(111), value}, nil)
tx, err := game.DefendTx(111, value)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func TestStepTx(t *testing.T) {
stubRpc, game := setup(t)
stateData := []byte{1, 2, 3}
proofData := []byte{4, 5, 6, 7, 8, 9}
stubRpc.SetResponse(fdgAddr, methodStep, batching.BlockLatest, []interface{}{big.NewInt(111), true, stateData, proofData}, nil)
tx, err := game.StepTx(111, true, stateData, proofData)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
}
func TestUpdateOracleTx(t *testing.T) {
t.Run("Local", func(t *testing.T) { t.Run("Local", func(t *testing.T) {
stubRpc, game := setup(t) stubRpc, game := setupFaultDisputeGameTest(t)
data := &faultTypes.PreimageOracleData{ data := &faultTypes.PreimageOracleData{
IsLocal: true, IsLocal: true,
LocalContext: common.Hash{0x02}, LocalContext: common.Hash{0x02},
...@@ -258,52 +65,39 @@ func TestUpdateOracleTx(t *testing.T) { ...@@ -258,52 +65,39 @@ func TestUpdateOracleTx(t *testing.T) {
OracleData: []byte{1, 2, 3, 4, 5, 6, 7}, OracleData: []byte{1, 2, 3, 4, 5, 6, 7},
OracleOffset: 16, OracleOffset: 16,
} }
claimIdx := uint64(6)
stubRpc.SetResponse(fdgAddr, methodAddLocalData, batching.BlockLatest, []interface{}{ stubRpc.SetResponse(fdgAddr, methodAddLocalData, batching.BlockLatest, []interface{}{
data.GetIdent(), data.GetIdent(),
data.LocalContext, data.LocalContext,
new(big.Int).SetUint64(uint64(data.OracleOffset)), new(big.Int).SetUint64(uint64(data.OracleOffset)),
}, nil) }, nil)
tx, err := game.UpdateOracleTx(context.Background(), data) tx, err := game.UpdateOracleTx(context.Background(), claimIdx, data)
require.NoError(t, err) require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx) stubRpc.VerifyTxCandidate(tx)
}) })
t.Run("Global", func(t *testing.T) { t.Run("Global", func(t *testing.T) {
stubRpc, game := setup(t) stubRpc, game := setupFaultDisputeGameTest(t)
data := &faultTypes.PreimageOracleData{ data := &faultTypes.PreimageOracleData{
IsLocal: false, IsLocal: false,
OracleKey: common.Hash{0xbc}.Bytes(), OracleKey: common.Hash{0xbc}.Bytes(),
OracleData: []byte{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15}, OracleData: []byte{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15},
OracleOffset: 16, OracleOffset: 16,
} }
claimIdx := uint64(6)
stubRpc.SetResponse(fdgAddr, methodVM, batching.BlockLatest, nil, []interface{}{vmAddr}) stubRpc.SetResponse(fdgAddr, methodVM, batching.BlockLatest, nil, []interface{}{vmAddr})
stubRpc.SetResponse(vmAddr, methodOracle, batching.BlockLatest, nil, []interface{}{oracleAddr}) stubRpc.SetResponse(vmAddr, methodOracle, batching.BlockLatest, nil, []interface{}{oracleAddr})
stubRpc.SetResponse(oracleAddr, methodLoadKeccak256PreimagePart, batching.BlockLatest, []interface{}{ stubRpc.SetResponse(oracleAddr, methodLoadKeccak256PreimagePart, batching.BlockLatest, []interface{}{
new(big.Int).SetUint64(uint64(data.OracleOffset)), new(big.Int).SetUint64(uint64(data.OracleOffset)),
data.GetPreimageWithoutSize(), data.GetPreimageWithoutSize(),
}, nil) }, nil)
tx, err := game.UpdateOracleTx(context.Background(), data) tx, err := game.UpdateOracleTx(context.Background(), claimIdx, data)
require.NoError(t, err) require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx) stubRpc.VerifyTxCandidate(tx)
}) })
} }
func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, claim faultTypes.Claim) { func setupFaultDisputeGameTest(t *testing.T) (*batchingTest.AbiBasedRpc, *FaultDisputeGameContract) {
stubRpc.SetResponse(
fdgAddr,
methodClaim,
batching.BlockLatest,
[]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() fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err) require.NoError(t, err)
......
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"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
)
var (
methodGenesisBlockNumber = "GENESIS_BLOCK_NUMBER"
methodSplitDepth = "SPLIT_DEPTH"
methodL2BlockNumber = "l2BlockNumber"
)
type OutputBisectionGameContract struct {
disputeGameContract
}
func NewOutputBisectionGameContract(addr common.Address, caller *batching.MultiCaller) (*OutputBisectionGameContract, error) {
contractAbi, err := bindings.OutputBisectionGameMetaData.GetAbi()
if err != nil {
return nil, fmt.Errorf("failed to load output bisection game ABI: %w", err)
}
return &OutputBisectionGameContract{
disputeGameContract: disputeGameContract{
multiCaller: caller,
contract: batching.NewBoundContract(contractAbi, addr),
},
}, nil
}
func (c *OutputBisectionGameContract) GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) {
results, err := c.multiCaller.Call(ctx, batching.BlockLatest,
c.contract.Call(methodGenesisBlockNumber),
c.contract.Call(methodL2BlockNumber))
if err != nil {
retErr = fmt.Errorf("failed to retrieve game block range: %w", err)
return
}
if len(results) != 2 {
retErr = fmt.Errorf("expected 2 results but got %v", len(results))
return
}
prestateBlock = results[0].GetBigInt(0).Uint64()
poststateBlock = results[1].GetBigInt(0).Uint64()
return
}
func (c *OutputBisectionGameContract) GetSplitDepth(ctx context.Context) (uint64, error) {
splitDepth, err := c.multiCaller.SingleCall(ctx, batching.BlockLatest, c.contract.Call(methodSplitDepth))
if err != nil {
return 0, fmt.Errorf("failed to retrieve split depth: %w", err)
}
return splitDepth.GetBigInt(0).Uint64(), nil
}
func (f *OutputBisectionGameContract) UpdateOracleTx(ctx context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error) {
if data.IsLocal {
return f.addLocalDataTx(claimIdx, data)
}
return f.addGlobalDataTx(ctx, data)
}
func (f *OutputBisectionGameContract) addLocalDataTx(claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error) {
call := f.contract.Call(
methodAddLocalData,
data.GetIdent(),
new(big.Int).SetUint64(claimIdx),
new(big.Int).SetUint64(uint64(data.OracleOffset)),
)
return call.ToTxCandidate()
}
func (f *OutputBisectionGameContract) addGlobalDataTx(ctx context.Context, data *types.PreimageOracleData) (txmgr.TxCandidate, error) {
vm, err := f.vm(ctx)
if err != nil {
return txmgr.TxCandidate{}, err
}
oracle, err := vm.Oracle(ctx)
if err != nil {
return txmgr.TxCandidate{}, err
}
return oracle.AddGlobalDataTx(data)
}
package contracts
import (
"context"
"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-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 TestOutputBisectionGameContract_CommonTests(t *testing.T) {
runCommonDisputeGameTests(t, func(t *testing.T) (*batchingTest.AbiBasedRpc, *disputeGameContract) {
stubRpc, contract := setupOutputBisectionGameTest(t)
return stubRpc, &contract.disputeGameContract
})
}
func TestGetBlockRange(t *testing.T) {
stubRpc, contract := setupOutputBisectionGameTest(t)
expectedStart := uint64(65)
expectedEnd := uint64(102)
stubRpc.SetResponse(fdgAddr, methodGenesisBlockNumber, batching.BlockLatest, nil, []interface{}{new(big.Int).SetUint64(expectedStart)})
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, batching.BlockLatest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd)})
start, end, err := contract.GetBlockRange(context.Background())
require.NoError(t, err)
require.Equal(t, expectedStart, start)
require.Equal(t, expectedEnd, end)
}
func TestGetSplitDepth(t *testing.T) {
stubRpc, contract := setupOutputBisectionGameTest(t)
expectedSplitDepth := uint64(15)
stubRpc.SetResponse(fdgAddr, methodSplitDepth, batching.BlockLatest, nil, []interface{}{new(big.Int).SetUint64(expectedSplitDepth)})
splitDepth, err := contract.GetSplitDepth(context.Background())
require.NoError(t, err)
require.Equal(t, expectedSplitDepth, splitDepth)
}
func TestOutputBisectionGame_UpdateOracleTx(t *testing.T) {
t.Run("Local", func(t *testing.T) {
stubRpc, game := setupOutputBisectionGameTest(t)
data := &faultTypes.PreimageOracleData{
IsLocal: true,
LocalContext: common.Hash{0x02},
OracleKey: common.Hash{0xbc}.Bytes(),
OracleData: []byte{1, 2, 3, 4, 5, 6, 7},
OracleOffset: 16,
}
claimIdx := uint64(6)
stubRpc.SetResponse(fdgAddr, methodAddLocalData, batching.BlockLatest, []interface{}{
data.GetIdent(),
new(big.Int).SetUint64(claimIdx),
new(big.Int).SetUint64(uint64(data.OracleOffset)),
}, nil)
tx, err := game.UpdateOracleTx(context.Background(), claimIdx, data)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
})
t.Run("Global", func(t *testing.T) {
stubRpc, game := setupOutputBisectionGameTest(t)
data := &faultTypes.PreimageOracleData{
IsLocal: false,
OracleKey: common.Hash{0xbc}.Bytes(),
OracleData: []byte{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15},
OracleOffset: 16,
}
claimIdx := uint64(6)
stubRpc.SetResponse(fdgAddr, methodVM, batching.BlockLatest, nil, []interface{}{vmAddr})
stubRpc.SetResponse(vmAddr, methodOracle, batching.BlockLatest, nil, []interface{}{oracleAddr})
stubRpc.SetResponse(oracleAddr, methodLoadKeccak256PreimagePart, batching.BlockLatest, []interface{}{
new(big.Int).SetUint64(uint64(data.OracleOffset)),
data.GetPreimageWithoutSize(),
}, nil)
tx, err := game.UpdateOracleTx(context.Background(), claimIdx, data)
require.NoError(t, err)
stubRpc.VerifyTxCandidate(tx)
})
}
func setupOutputBisectionGameTest(t *testing.T) (*batchingTest.AbiBasedRpc, *OutputBisectionGameContract) {
fdgAbi, err := bindings.OutputBisectionGameMetaData.GetAbi()
require.NoError(t, err)
vmAbi, err := bindings.MIPSMetaData.GetAbi()
require.NoError(t, err)
oracleAbi, err := bindings.PreimageOracleMetaData.GetAbi()
require.NoError(t, err)
stubRpc := batchingTest.NewAbiBasedRpc(t, fdgAddr, fdgAbi)
stubRpc.AddContract(vmAddr, vmAbi)
stubRpc.AddContract(oracleAddr, oracleAbi)
caller := batching.NewMultiCaller(stubRpc, batching.DefaultBatchSize)
game, err := NewOutputBisectionGameContract(fdgAddr, caller)
require.NoError(t, err)
return stubRpc, game
}
...@@ -5,15 +5,12 @@ import ( ...@@ -5,15 +5,12 @@ import (
"context" "context"
"fmt" "fmt"
"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/responder"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/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-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -24,10 +21,6 @@ type GameInfo interface { ...@@ -24,10 +21,6 @@ type GameInfo interface {
GetClaimCount(context.Context) (uint64, error) GetClaimCount(context.Context) (uint64, error)
} }
// gameValidator checks that the specific game instance is compatible with the configuration.
// Typically, this is done by verifying the absolute prestate of the game matches the local absolute prestate.
type gameValidator func(ctx context.Context, gameContract *contracts.FaultDisputeGameContract) error
type GamePlayer struct { type GamePlayer struct {
act actor act actor
loader GameInfo loader GameInfo
...@@ -35,7 +28,20 @@ type GamePlayer struct { ...@@ -35,7 +28,20 @@ type GamePlayer struct {
status gameTypes.GameStatus status gameTypes.GameStatus
} }
type resourceCreator func(addr common.Address, contract *contracts.FaultDisputeGameContract, gameDepth uint64, dir string) (types.TraceAccessor, gameValidator, error) type GameContract interface {
responder.GameContract
GameInfo
ClaimLoader
GetStatus(ctx context.Context) (gameTypes.GameStatus, error)
GetMaxGameDepth(ctx context.Context) (uint64, error)
}
type gameTypeResources interface {
Contract() GameContract
CreateAccessor(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (types.TraceAccessor, error)
}
type resourceCreator func(addr common.Address) (gameTypeResources, error)
func NewGamePlayer( func NewGamePlayer(
ctx context.Context, ctx context.Context,
...@@ -44,16 +50,17 @@ func NewGamePlayer( ...@@ -44,16 +50,17 @@ func NewGamePlayer(
dir string, dir string,
addr common.Address, addr common.Address,
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
client *ethclient.Client,
creator resourceCreator, creator resourceCreator,
) (*GamePlayer, error) { ) (*GamePlayer, error) {
logger = logger.New("game", addr) logger = logger.New("game", addr)
loader, err := contracts.NewFaultDisputeGameContract(addr, batching.NewMultiCaller(client.Client(), batching.DefaultBatchSize)) resources, err := creator(addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create fault dispute game contract wrapper: %w", err) return nil, fmt.Errorf("failed to create game resources: %w", err)
} }
loader := resources.Contract()
status, err := loader.GetStatus(ctx) status, err := loader.GetStatus(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch game status: %w", err) return nil, fmt.Errorf("failed to fetch game status: %w", err)
...@@ -77,15 +84,11 @@ func NewGamePlayer( ...@@ -77,15 +84,11 @@ func NewGamePlayer(
return nil, fmt.Errorf("failed to fetch the game depth: %w", err) return nil, fmt.Errorf("failed to fetch the game depth: %w", err)
} }
accessor, validator, err := creator(addr, loader, gameDepth, dir) accessor, err := resources.CreateAccessor(ctx, logger, gameDepth, dir)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create trace accessor: %w", err) return nil, fmt.Errorf("failed to create trace accessor: %w", err)
} }
if err := validator(ctx, loader); err != nil {
return nil, fmt.Errorf("failed to validate absolute prestate: %w", err)
}
responder, err := responder.NewFaultResponder(logger, txMgr, loader) responder, err := responder.NewFaultResponder(logger, txMgr, loader)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create the responder: %w", err) return nil, fmt.Errorf("failed to create the responder: %w", err)
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler"
"github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "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-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
...@@ -39,7 +40,7 @@ func RegisterGameTypes( ...@@ -39,7 +40,7 @@ func RegisterGameTypes(
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
client *ethclient.Client, caller *batching.MultiCaller,
) (CloseFunc, error) { ) (CloseFunc, error) {
var closer CloseFunc var closer CloseFunc
var l2Client *ethclient.Client var l2Client *ethclient.Client
...@@ -52,13 +53,13 @@ func RegisterGameTypes( ...@@ -52,13 +53,13 @@ func RegisterGameTypes(
closer = l2Client.Close closer = l2Client.Close
} }
if cfg.TraceTypeEnabled(config.TraceTypeOutputCannon) { if cfg.TraceTypeEnabled(config.TraceTypeOutputCannon) {
registerOutputCannon(registry, ctx, logger, m, cfg, txMgr, client, l2Client) registerOutputCannon(registry, ctx, logger, m, cfg, txMgr, caller, l2Client)
} }
if cfg.TraceTypeEnabled(config.TraceTypeCannon) { if cfg.TraceTypeEnabled(config.TraceTypeCannon) {
registerCannon(registry, ctx, logger, m, cfg, txMgr, client, l2Client) registerCannon(registry, ctx, logger, m, cfg, txMgr, caller, l2Client)
} }
if cfg.TraceTypeEnabled(config.TraceTypeAlphabet) { if cfg.TraceTypeEnabled(config.TraceTypeAlphabet) {
registerAlphabet(registry, ctx, logger, m, cfg, txMgr, client) registerAlphabet(registry, ctx, logger, m, cfg, txMgr, caller)
} }
return closer, nil return closer, nil
} }
...@@ -70,31 +71,53 @@ func registerOutputCannon( ...@@ -70,31 +71,53 @@ func registerOutputCannon(
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
client *ethclient.Client, caller *batching.MultiCaller,
l2Client cannon.L2HeaderSource) { l2Client cannon.L2HeaderSource) {
resourceCreator := func(addr common.Address, contract *contracts.FaultDisputeGameContract, gameDepth uint64, dir string) (faultTypes.TraceAccessor, gameValidator, error) { resourceCreator := func(addr common.Address) (gameTypeResources, error) {
logger := logger.New("game", addr) // Currently still using the old fault dispute game contracts for output_cannon
// TODO(client-pod#43): Updated contracts should expose this as the pre and post state blocks // as the output bisection+cannon contract isn't being deployed.
agreed, disputed, err := contract.GetProposals(ctx) contract, err := contracts.NewFaultDisputeGameContract(addr, caller)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
accessor, err := outputs.NewOutputCannonTraceAccessor(ctx, logger, m, cfg, l2Client, contract, dir, gameDepth, agreed.L2BlockNumber.Uint64(), disputed.L2BlockNumber.Uint64()) return &outputCannonResources{
if err != nil { m: m,
return nil, nil, err cfg: cfg,
} l2Client: l2Client,
// TODO(client-pod#44): Validate absolute pre-state for split games contract: contract,
noopValidator := func(ctx context.Context, gameContract *contracts.FaultDisputeGameContract) error { }, nil
return nil
}
return accessor, noopValidator, nil
} }
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, client, resourceCreator) return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, resourceCreator)
} }
registry.RegisterGameType(outputCannonGameType, playerCreator) registry.RegisterGameType(outputCannonGameType, playerCreator)
} }
type outputCannonResources struct {
m metrics.Metricer
cfg *config.Config
l2Client cannon.L2HeaderSource
contract *contracts.FaultDisputeGameContract
}
func (r *outputCannonResources) Contract() GameContract {
return r.contract
}
func (r *outputCannonResources) CreateAccessor(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
// TODO(client-pod#44): Validate absolute pre-state for split games
// TODO(client-pod#43): Updated contracts should expose this as the pre and post state blocks
agreed, disputed, err := r.contract.GetProposals(ctx)
if err != nil {
return nil, err
}
accessor, err := outputs.NewOutputCannonTraceAccessor(ctx, logger, r.m, r.cfg, r.l2Client, r.contract, dir, gameDepth, agreed.L2BlockNumber.Uint64(), disputed.L2BlockNumber.Uint64())
if err != nil {
return nil, err
}
return accessor, nil
}
func registerCannon( func registerCannon(
registry Registry, registry Registry,
ctx context.Context, ctx context.Context,
...@@ -102,26 +125,49 @@ func registerCannon( ...@@ -102,26 +125,49 @@ func registerCannon(
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
client *ethclient.Client, caller *batching.MultiCaller,
l2Client cannon.L2HeaderSource) { l2Client cannon.L2HeaderSource) {
resourceCreator := func(addr common.Address, contract *contracts.FaultDisputeGameContract, gameDepth uint64, dir string) (faultTypes.TraceAccessor, gameValidator, error) { resourceCreator := func(addr common.Address) (gameTypeResources, error) {
logger := logger.New("game", addr) contract, err := contracts.NewFaultDisputeGameContract(addr, caller)
localInputs, err := cannon.FetchLocalInputs(ctx, contract, l2Client)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err) return nil, err
} }
provider := cannon.NewTraceProvider(logger, m, cfg, faultTypes.NoLocalContext, localInputs, dir, gameDepth) return &cannonResources{
validator := func(ctx context.Context, contract *contracts.FaultDisputeGameContract) error { m: m,
return ValidateAbsolutePrestate(ctx, provider, contract) cfg: cfg,
} l2Client: l2Client,
return trace.NewSimpleTraceAccessor(provider), validator, nil contract: contract,
}, nil
} }
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, client, resourceCreator) return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, resourceCreator)
} }
registry.RegisterGameType(cannonGameType, playerCreator) registry.RegisterGameType(cannonGameType, playerCreator)
} }
type cannonResources struct {
m metrics.Metricer
cfg *config.Config
l2Client cannon.L2HeaderSource
contract *contracts.FaultDisputeGameContract
}
func (r *cannonResources) Contract() GameContract {
return r.contract
}
func (r *cannonResources) CreateAccessor(ctx context.Context, logger log.Logger, gameDepth uint64, dir string) (faultTypes.TraceAccessor, error) {
localInputs, err := cannon.FetchLocalInputs(ctx, r.contract, r.l2Client)
if err != nil {
return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err)
}
provider := cannon.NewTraceProvider(logger, r.m, r.cfg, faultTypes.NoLocalContext, localInputs, dir, gameDepth)
if err := ValidateAbsolutePrestate(ctx, provider, r.contract); err != nil {
return nil, err
}
return trace.NewSimpleTraceAccessor(provider), nil
}
func registerAlphabet( func registerAlphabet(
registry Registry, registry Registry,
ctx context.Context, ctx context.Context,
...@@ -129,16 +175,36 @@ func registerAlphabet( ...@@ -129,16 +175,36 @@ func registerAlphabet(
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
txMgr txmgr.TxManager, txMgr txmgr.TxManager,
client *ethclient.Client) { caller *batching.MultiCaller) {
resourceCreator := func(addr common.Address, contract *contracts.FaultDisputeGameContract, gameDepth uint64, dir string) (faultTypes.TraceAccessor, gameValidator, error) { resourceCreator := func(addr common.Address) (gameTypeResources, error) {
provider := alphabet.NewTraceProvider(cfg.AlphabetTrace, gameDepth) contract, err := contracts.NewFaultDisputeGameContract(addr, caller)
validator := func(ctx context.Context, contract *contracts.FaultDisputeGameContract) error { if err != nil {
return ValidateAbsolutePrestate(ctx, provider, contract) return nil, err
} }
return trace.NewSimpleTraceAccessor(provider), validator, nil return &alphabetResources{
cfg: cfg,
contract: contract,
}, nil
} }
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, client, resourceCreator) return NewGamePlayer(ctx, logger, m, dir, game.Proxy, txMgr, resourceCreator)
} }
registry.RegisterGameType(alphabetGameType, playerCreator) registry.RegisterGameType(alphabetGameType, playerCreator)
} }
type alphabetResources struct {
cfg *config.Config
contract *contracts.FaultDisputeGameContract
}
func (r *alphabetResources) Contract() GameContract {
return r.contract
}
func (r *alphabetResources) CreateAccessor(ctx context.Context, _ log.Logger, gameDepth uint64, _ string) (faultTypes.TraceAccessor, error) {
provider := alphabet.NewTraceProvider(r.cfg.AlphabetTrace, gameDepth)
if err := ValidateAbsolutePrestate(ctx, provider, r.contract); err != nil {
return nil, err
}
return trace.NewSimpleTraceAccessor(provider), nil
}
...@@ -21,7 +21,7 @@ type GameContract interface { ...@@ -21,7 +21,7 @@ type GameContract interface {
AttackTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) AttackTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error)
DefendTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error) DefendTx(parentContractIndex uint64, pivot common.Hash) (txmgr.TxCandidate, error)
StepTx(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) (txmgr.TxCandidate, error) StepTx(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) (txmgr.TxCandidate, error)
UpdateOracleTx(ctx context.Context, data *types.PreimageOracleData) (txmgr.TxCandidate, error) UpdateOracleTx(ctx context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error)
} }
// FaultResponder implements the [Responder] interface to send onchain transactions. // FaultResponder implements the [Responder] interface to send onchain transactions.
...@@ -75,7 +75,7 @@ func (r *FaultResponder) ResolveClaim(ctx context.Context, claimIdx uint64) erro ...@@ -75,7 +75,7 @@ func (r *FaultResponder) ResolveClaim(ctx context.Context, claimIdx uint64) erro
func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error { func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error {
if action.OracleData != nil { if action.OracleData != nil {
r.log.Info("Updating oracle data", "key", action.OracleData.OracleKey) r.log.Info("Updating oracle data", "key", action.OracleData.OracleKey)
candidate, err := r.contract.UpdateOracleTx(ctx, action.OracleData) candidate, err := r.contract.UpdateOracleTx(ctx, uint64(action.ParentIdx), action.OracleData)
if err != nil { if err != nil {
return fmt.Errorf("failed to create pre-image oracle tx: %w", err) return fmt.Errorf("failed to create pre-image oracle tx: %w", err)
} }
......
...@@ -188,6 +188,7 @@ func TestPerformAction(t *testing.T) { ...@@ -188,6 +188,7 @@ func TestPerformAction(t *testing.T) {
require.Len(t, mockTxMgr.sent, 2) require.Len(t, mockTxMgr.sent, 2)
require.EqualValues(t, action.OracleData, contract.updateOracleArgs) require.EqualValues(t, action.OracleData, contract.updateOracleArgs)
require.EqualValues(t, action.ParentIdx, contract.updateOracleClaimIdx)
require.EqualValues(t, []interface{}{uint64(action.ParentIdx), action.IsAttack, action.PreState, action.ProofData}, contract.stepArgs) require.EqualValues(t, []interface{}{uint64(action.ParentIdx), action.IsAttack, action.PreState, action.ProofData}, contract.stepArgs)
// Important that the oracle is updated first // Important that the oracle is updated first
require.Equal(t, ([]byte)("updateOracle"), mockTxMgr.sent[0].TxData) require.Equal(t, ([]byte)("updateOracle"), mockTxMgr.sent[0].TxData)
...@@ -236,12 +237,13 @@ func (m *mockTxManager) Close() { ...@@ -236,12 +237,13 @@ func (m *mockTxManager) Close() {
} }
type mockContract struct { type mockContract struct {
calls int calls int
callFails bool callFails bool
attackArgs []interface{} attackArgs []interface{}
defendArgs []interface{} defendArgs []interface{}
stepArgs []interface{} stepArgs []interface{}
updateOracleArgs *types.PreimageOracleData updateOracleClaimIdx uint64
updateOracleArgs *types.PreimageOracleData
} }
func (m *mockContract) CallResolve(_ context.Context) (gameTypes.GameStatus, error) { func (m *mockContract) CallResolve(_ context.Context) (gameTypes.GameStatus, error) {
...@@ -283,7 +285,8 @@ func (m *mockContract) StepTx(claimIdx uint64, isAttack bool, stateData []byte, ...@@ -283,7 +285,8 @@ func (m *mockContract) StepTx(claimIdx uint64, isAttack bool, stateData []byte,
return txmgr.TxCandidate{TxData: ([]byte)("step")}, nil return txmgr.TxCandidate{TxData: ([]byte)("step")}, nil
} }
func (m *mockContract) UpdateOracleTx(_ context.Context, data *types.PreimageOracleData) (txmgr.TxCandidate, error) { func (m *mockContract) UpdateOracleTx(_ context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error) {
m.updateOracleClaimIdx = claimIdx
m.updateOracleArgs = data m.updateOracleArgs = data
return txmgr.TxCandidate{TxData: ([]byte)("updateOracle")}, nil return txmgr.TxCandidate{TxData: ([]byte)("updateOracle")}, nil
} }
...@@ -22,7 +22,7 @@ func NewOutputCannonTraceAccessor( ...@@ -22,7 +22,7 @@ func NewOutputCannonTraceAccessor(
m metrics.Metricer, m metrics.Metricer,
cfg *config.Config, cfg *config.Config,
l2Client cannon.L2HeaderSource, l2Client cannon.L2HeaderSource,
contract *contracts.FaultDisputeGameContract, contract cannon.L1HeadSource,
dir string, dir string,
gameDepth uint64, gameDepth uint64,
prestateBlock uint64, prestateBlock uint64,
......
...@@ -168,7 +168,8 @@ func (s *Service) initGameLoader(cfg *config.Config) error { ...@@ -168,7 +168,8 @@ func (s *Service) initGameLoader(cfg *config.Config) error {
func (s *Service) initScheduler(ctx context.Context, cfg *config.Config) error { func (s *Service) initScheduler(ctx context.Context, cfg *config.Config) error {
gameTypeRegistry := registry.NewGameTypeRegistry() gameTypeRegistry := registry.NewGameTypeRegistry()
closer, err := fault.RegisterGameTypes(gameTypeRegistry, ctx, s.logger, s.metrics, cfg, s.txMgr, s.l1Client) caller := batching.NewMultiCaller(s.l1Client.Client(), batching.DefaultBatchSize)
closer, err := fault.RegisterGameTypes(gameTypeRegistry, ctx, s.logger, s.metrics, cfg, s.txMgr, caller)
if err != nil { if err != nil {
return err return err
} }
......
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