diff --git a/op-challenger2/game/fault/agent.go b/op-challenger2/game/fault/agent.go index 9a82250f5921..609e848eb911 100644 --- a/op-challenger2/game/fault/agent.go +++ b/op-challenger2/game/fault/agent.go @@ -123,7 +123,7 @@ func (a *Agent) performAction(ctx context.Context, wg *sync.WaitGroup, action ty containsOracleData := action.OracleData != nil isLocal := containsOracleData && action.OracleData.IsLocal actionLog = actionLog.New( - "is_attack", action.IsAttack, + "attackBranch", action.AttackBranch, "parent", action.ParentClaim.ContractIndex, "prestate", common.Bytes2Hex(action.PreState), "proof", common.Bytes2Hex(action.ProofData), @@ -133,13 +133,15 @@ func (a *Agent) performAction(ctx context.Context, wg *sync.WaitGroup, action ty if action.OracleData != nil { actionLog = actionLog.New("oracleKey", common.Bytes2Hex(action.OracleData.OracleKey)) } - } else if action.Type == types.ActionTypeMove { - actionLog = actionLog.New("is_attack", action.IsAttack, "parent", action.ParentClaim.ContractIndex, "value", action.Value) + } else if action.Type == types.ActionTypeAttackV2 { + actionLog = actionLog.New("attackBranch", action.AttackBranch, "parent", action.ParentClaim.ContractIndex, "value", action.Value) } switch action.Type { case types.ActionTypeMove: a.metrics.RecordGameMove() + case types.ActionTypeAttackV2: + a.metrics.RecordGameAttackV2() case types.ActionTypeStep: a.metrics.RecordGameStep() case types.ActionTypeChallengeL2BlockNumber: diff --git a/op-challenger2/game/fault/responder/responder.go b/op-challenger2/game/fault/responder/responder.go index d12a6b9f6558..5d80bed69b9c 100644 --- a/op-challenger2/game/fault/responder/responder.go +++ b/op-challenger2/game/fault/responder/responder.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math/big" "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/preimages" "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/types" @@ -19,8 +20,10 @@ type GameContract interface { CallResolveClaim(ctx context.Context, claimIdx uint64) error ResolveClaimTx(claimIdx uint64) (txmgr.TxCandidate, error) AttackTx(ctx context.Context, parent types.Claim, pivot common.Hash) (txmgr.TxCandidate, error) + AttackV2Tx(ctx context.Context, parent types.Claim, attackBranch uint64, daType uint64, claims []byte) (txmgr.TxCandidate, error) DefendTx(ctx context.Context, parent types.Claim, pivot common.Hash) (txmgr.TxCandidate, error) StepTx(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) (txmgr.TxCandidate, error) + StepV2Tx(claimIdx uint64, attackBranch uint64, stateData []byte, proof types.StepProof) (txmgr.TxCandidate, error) ChallengeL2BlockNumberTx(challenge *types.InvalidL2BlockNumberChallenge) (txmgr.TxCandidate, error) } @@ -117,8 +120,20 @@ func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) } else { candidate, err = r.contract.DefendTx(ctx, action.ParentClaim, action.Value) } + case types.ActionTypeAttackV2: + subValues := make([]byte, 0, len(*action.SubValues)) + for _, subValue := range *action.SubValues { + subValues = append(subValues, subValue[:]...) + } + daTypeUint64 := (*big.Int)(action.DAType).Uint64() + candidate, err = r.contract.AttackV2Tx(ctx, action.ParentClaim, action.AttackBranch, daTypeUint64, subValues) case types.ActionTypeStep: - candidate, err = r.contract.StepTx(uint64(action.ParentClaim.ContractIndex), action.IsAttack, action.PreState, action.ProofData) + stepProof := types.StepProof{ + PreStateItem: action.OracleData.VMStateDA.PreDA, + PostStateItem: action.OracleData.VMStateDA.PostDA, + VmProof: action.ProofData, + } + candidate, err = r.contract.StepV2Tx(uint64(action.ParentClaim.ContractIndex), action.AttackBranch, action.PreState, stepProof) case types.ActionTypeChallengeL2BlockNumber: candidate, err = r.contract.ChallengeL2BlockNumberTx(action.InvalidL2BlockNumberChallenge) } diff --git a/op-challenger2/game/fault/responder/responder_test.go b/op-challenger2/game/fault/responder/responder_test.go index 65517bc188fd..c9dc5b1f7e35 100644 --- a/op-challenger2/game/fault/responder/responder_test.go +++ b/op-challenger2/game/fault/responder/responder_test.go @@ -146,37 +146,54 @@ func TestPerformAction(t *testing.T) { require.Equal(t, ([]byte)("attack"), mockTxMgr.sent[0].TxData) }) - t.Run("defend", func(t *testing.T) { + t.Run("attackV2", func(t *testing.T) { responder, mockTxMgr, contract, _, _ := newTestFaultResponder(t) action := types.Action{ - Type: types.ActionTypeMove, - ParentClaim: types.Claim{ContractIndex: 123}, - IsAttack: false, - Value: common.Hash{0xaa}, + Type: types.ActionTypeAttackV2, + ParentClaim: types.Claim{ContractIndex: 123}, + IsAttack: false, + AttackBranch: 0, + DAType: types.CallDataType, + SubValues: &[]common.Hash{{0xaa}}, } err := responder.PerformAction(context.Background(), action) require.NoError(t, err) require.Len(t, mockTxMgr.sent, 1) - require.EqualValues(t, []interface{}{action.ParentClaim, action.Value}, contract.defendArgs) - require.Equal(t, ([]byte)("defend"), mockTxMgr.sent[0].TxData) + daTypeUint64 := (*big.Int)(action.DAType).Uint64() + subValues := make([]byte, 0, len(*action.SubValues)) + for _, subValue := range *action.SubValues { + subValues = append(subValues, subValue[:]...) + } + require.EqualValues(t, []interface{}{action.ParentClaim, action.AttackBranch, daTypeUint64, subValues}, contract.attackV2Args) + require.Equal(t, ([]byte)("attackV2"), mockTxMgr.sent[0].TxData) }) t.Run("step", func(t *testing.T) { responder, mockTxMgr, contract, _, _ := newTestFaultResponder(t) action := types.Action{ - Type: types.ActionTypeStep, - ParentClaim: types.Claim{ContractIndex: 123}, - IsAttack: true, - PreState: []byte{1, 2, 3}, - ProofData: []byte{4, 5, 6}, + Type: types.ActionTypeStep, + ParentClaim: types.Claim{ContractIndex: 123}, + IsAttack: true, + AttackBranch: 0, + PreState: []byte{1, 2, 3}, + ProofData: []byte{4, 5, 6}, + OracleData: &types.PreimageOracleData{ + VMStateDA: types.DAData{}, + OutputRootDAItem: types.DAItem{}, + }, + } + stepProof := types.StepProof{ + PreStateItem: action.OracleData.VMStateDA.PreDA, + PostStateItem: action.OracleData.VMStateDA.PostDA, + VmProof: action.ProofData, } err := responder.PerformAction(context.Background(), action) require.NoError(t, err) require.Len(t, mockTxMgr.sent, 1) - require.EqualValues(t, []interface{}{uint64(123), action.IsAttack, action.PreState, action.ProofData}, contract.stepArgs) - require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData) + require.EqualValues(t, []interface{}{uint64(123), action.AttackBranch, action.PreState, stepProof}, contract.stepV2Args) + require.Equal(t, ([]byte)("stepV2"), mockTxMgr.sent[0].TxData) }) t.Run("stepWithLocalOracleData", func(t *testing.T) { @@ -188,7 +205,9 @@ func TestPerformAction(t *testing.T) { PreState: []byte{1, 2, 3}, ProofData: []byte{4, 5, 6}, OracleData: &types.PreimageOracleData{ - IsLocal: true, + IsLocal: true, + VMStateDA: types.DAData{}, + OutputRootDAItem: types.DAItem{}, }, } err := responder.PerformAction(context.Background(), action) @@ -196,7 +215,7 @@ func TestPerformAction(t *testing.T) { require.Len(t, mockTxMgr.sent, 1) require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil - require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData) + require.Equal(t, ([]byte)("stepV2"), mockTxMgr.sent[0].TxData) require.Equal(t, 1, uploader.updates) require.Equal(t, 0, oracle.existCalls) }) @@ -210,7 +229,9 @@ func TestPerformAction(t *testing.T) { PreState: []byte{1, 2, 3}, ProofData: []byte{4, 5, 6}, OracleData: &types.PreimageOracleData{ - IsLocal: false, + IsLocal: false, + VMStateDA: types.DAData{}, + OutputRootDAItem: types.DAItem{}, }, } err := responder.PerformAction(context.Background(), action) @@ -218,7 +239,7 @@ func TestPerformAction(t *testing.T) { require.Len(t, mockTxMgr.sent, 1) require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil - require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData) + require.Equal(t, ([]byte)("stepV2"), mockTxMgr.sent[0].TxData) require.Equal(t, 1, uploader.updates) require.Equal(t, 1, oracle.existCalls) }) @@ -370,8 +391,10 @@ type mockContract struct { calls int callFails bool attackArgs []interface{} + attackV2Args []interface{} defendArgs []interface{} stepArgs []interface{} + stepV2Args []interface{} challengeArgs []interface{} updateOracleClaimIdx uint64 updateOracleArgs *types.PreimageOracleData @@ -411,6 +434,11 @@ func (m *mockContract) AttackTx(_ context.Context, parent types.Claim, claim com return txmgr.TxCandidate{TxData: ([]byte)("attack")}, nil } +func (m *mockContract) AttackV2Tx(ctx context.Context, parent types.Claim, attackBranch uint64, daType uint64, claims []byte) (txmgr.TxCandidate, error) { + m.attackV2Args = []interface{}{parent, attackBranch, daType, claims} + return txmgr.TxCandidate{TxData: ([]byte)("attackV2")}, nil +} + func (m *mockContract) DefendTx(_ context.Context, parent types.Claim, claim common.Hash) (txmgr.TxCandidate, error) { m.defendArgs = []interface{}{parent, claim} return txmgr.TxCandidate{TxData: ([]byte)("defend")}, nil @@ -421,6 +449,11 @@ func (m *mockContract) StepTx(claimIdx uint64, isAttack bool, stateData []byte, return txmgr.TxCandidate{TxData: ([]byte)("step")}, nil } +func (m *mockContract) StepV2Tx(claimIdx uint64, attackBranch uint64, stateData []byte, proof types.StepProof) (txmgr.TxCandidate, error) { + m.stepV2Args = []interface{}{claimIdx, attackBranch, stateData, proof} + return txmgr.TxCandidate{TxData: ([]byte)("stepV2")}, nil +} + func (m *mockContract) UpdateOracleTx(_ context.Context, claimIdx uint64, data *types.PreimageOracleData) (txmgr.TxCandidate, error) { m.updateOracleClaimIdx = claimIdx m.updateOracleArgs = data diff --git a/op-challenger2/game/fault/solver/actors.go b/op-challenger2/game/fault/solver/actors.go index 38ddbbf39b39..aeef491b9493 100644 --- a/op-challenger2/game/fault/solver/actors.go +++ b/op-challenger2/game/fault/solver/actors.go @@ -51,20 +51,28 @@ var correctDefendLastClaim = respondLastClaim(func(seq *test.GameBuilderSeq) { // Must attack the root seq.Attack2(nil, 0) } else { - seq.Attack2(nil, 1) + seq.Attack2(nil, seq.MaxAttackBranch()) } }) var incorrectAttackLastClaim = respondLastClaim(func(seq *test.GameBuilderSeq) { - seq.Attack2(nil, 0, test.WithValue(common.Hash{0xaa})) + incorrectSubValues := []common.Hash{} + for i := uint64(0); i < seq.MaxAttackBranch(); i++ { + incorrectSubValues = append(incorrectSubValues, common.Hash{0xaa}) + } + seq.Attack2(incorrectSubValues, 0) }) var incorrectDefendLastClaim = respondLastClaim(func(seq *test.GameBuilderSeq) { + incorrectSubValues := []common.Hash{} + for i := uint64(0); i < seq.MaxAttackBranch(); i++ { + incorrectSubValues = append(incorrectSubValues, common.Hash{0xdd}) + } if seq.IsRoot() { // Must attack the root - seq.Attack2(nil, 0, test.WithValue(common.Hash{0xdd})) + seq.Attack2(incorrectSubValues, 0) } else { - seq.Attack2(nil, 1, test.WithValue(common.Hash{0xdd})) + seq.Attack2(incorrectSubValues, seq.MaxAttackBranch()) } }) @@ -77,29 +85,51 @@ var defendEverythingCorrect = respondAllClaims(func(seq *test.GameBuilderSeq) { // Must attack root seq.Attack2(nil, 0) } else { - seq.Attack2(nil, 1) + seq.Attack2(nil, seq.MaxAttackBranch()) } }) var attackEverythingIncorrect = respondAllClaims(func(seq *test.GameBuilderSeq) { - seq.Attack2(nil, 0, test.WithValue(common.Hash{0xaa})) + incorrectSubValues := []common.Hash{} + for i := uint64(0); i < seq.MaxAttackBranch(); i++ { + incorrectSubValues = append(incorrectSubValues, common.Hash{0xaa}) + } + seq.Attack2(incorrectSubValues, 0) }) var defendEverythingIncorrect = respondAllClaims(func(seq *test.GameBuilderSeq) { + incorrectSubValues := []common.Hash{} + for i := uint64(0); i < seq.MaxAttackBranch(); i++ { + incorrectSubValues = append(incorrectSubValues, common.Hash{0xbb}) + } if seq.IsRoot() { // Must attack root - seq.Attack2(nil, 0, test.WithValue(common.Hash{0xbb})) + seq.Attack2(incorrectSubValues, 0) } else { - seq.Attack2(nil, 1, test.WithValue(common.Hash{0xbb})) + seq.Attack2(incorrectSubValues, seq.MaxAttackBranch()) } }) var exhaustive = respondAllClaims(func(seq *test.GameBuilderSeq) { seq.Attack2(nil, 0) - seq.Attack2(nil, 0, test.WithValue(common.Hash{0xaa})) - if !seq.IsRoot() { - seq.Attack2(nil, 1) - seq.Attack2(nil, 1, test.WithValue(common.Hash{0xdd})) + incorrectSubValues := []common.Hash{} + for i := uint64(0); i < seq.MaxAttackBranch(); i++ { + incorrectSubValues = append(incorrectSubValues, common.Hash{0xaa}) + if seq.IsSplitDepth() { + // at splitDepth, there is only one subValue + break + } + } + seq.Attack2(incorrectSubValues, 0) + if !seq.IsRoot() && !seq.IsTraceRoot() { + seq.Attack2(nil, seq.MaxAttackBranch()) + for i := uint64(0); i < seq.MaxAttackBranch(); i++ { + incorrectSubValues[i] = common.Hash{0xdd} + if seq.IsSplitDepth() { + break + } + } + seq.Attack2(incorrectSubValues, seq.MaxAttackBranch()) } }) diff --git a/op-challenger2/game/fault/solver/game_rules_test.go b/op-challenger2/game/fault/solver/game_rules_test.go index af6845ccf5eb..0d60476bfb08 100644 --- a/op-challenger2/game/fault/solver/game_rules_test.go +++ b/op-challenger2/game/fault/solver/game_rules_test.go @@ -15,7 +15,6 @@ import ( func verifyGameRules(t *testing.T, game types.Game, rootClaimCorrect bool) { actualResult, claimTree, resolvedGame := gameResult(game) - verifyExpectedGameResult(t, rootClaimCorrect, actualResult) verifyNoChallengerClaimsWereSuccessfullyCountered(t, resolvedGame) diff --git a/op-challenger2/game/fault/solver/game_solver.go b/op-challenger2/game/fault/solver/game_solver.go index 75a9707d6462..7114b1db58f0 100644 --- a/op-challenger2/game/fault/solver/game_solver.go +++ b/op-challenger2/game/fault/solver/game_solver.go @@ -20,7 +20,7 @@ func NewGameSolver(gameDepth types.Depth, trace types.TraceAccessor, daType type } func (s *GameSolver) AgreeWithRootClaim(ctx context.Context, game types.Game) (bool, error) { - return s.claimSolver.agreeWithClaim(ctx, game, game.Claims()[0]) + return s.claimSolver.agreeWithClaimV2(ctx, game, game.Claims()[0], 0) } func (s *GameSolver) CalculateNextActions(ctx context.Context, game types.Game) ([]types.Action, error) { @@ -77,39 +77,54 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t if claim.CounteredBy != (common.Address{}) { return nil, nil } - step, err := s.claimSolver.AttemptStep(ctx, game, claim, agreedClaims) - if err != nil { - return nil, err - } - if step == nil { - return nil, nil + for branch := range *claim.SubValues { + step, err := s.claimSolver.AttemptStep(ctx, game, claim, agreedClaims, uint64(branch)) + if err != nil { + return nil, err + } + if step == nil { + continue + } + return &types.Action{ + Type: types.ActionTypeStep, + ParentClaim: step.LeafClaim, + AttackBranch: step.AttackBranch, + PreState: step.PreState, + ProofData: step.ProofData, + OracleData: step.OracleData, + }, nil } - return &types.Action{ - Type: types.ActionTypeStep, - ParentClaim: step.LeafClaim, - IsAttack: step.IsAttack, - PreState: step.PreState, - ProofData: step.ProofData, - OracleData: step.OracleData, - }, nil + return nil, nil } func (s *GameSolver) calculateMove(ctx context.Context, game types.Game, claim types.Claim, honestClaims *honestClaimTracker) (*types.Action, error) { - move, err := s.claimSolver.NextMove(ctx, claim, game, honestClaims) - if err != nil { - return nil, fmt.Errorf("failed to calculate next move for claim index %v: %w", claim.ContractIndex, err) - } - if move == nil { - return nil, nil - } - honestClaims.AddHonestClaim(claim, *move) - if game.IsDuplicate(*move) { - return nil, nil + for branch := range *claim.SubValues { + // attack branch 0 can be attacked at root or splitDepth+nbits + if claim.Position.Depth() == game.SplitDepth()+types.Depth(game.NBits()) && branch != 0 { + return nil, nil + } + if claim.IsRoot() && branch != 0 { + return nil, fmt.Errorf("cannot attack root claim with branch %v", branch) + } + move, err := s.claimSolver.NextMove(ctx, claim, game, honestClaims, uint64(branch)) + if err != nil { + return nil, fmt.Errorf("failed to calculate next move for claim index %v: %w", claim.ContractIndex, err) + } + if move == nil { + continue + } + honestClaims.AddHonestClaim(claim, *move) + if game.IsDuplicate(*move) { + break + } + return &types.Action{ + Type: types.ActionTypeAttackV2, + ParentClaim: game.Claims()[move.ParentContractIndex], + Value: move.Value, + SubValues: move.SubValues, + AttackBranch: move.AttackBranch, + DAType: s.claimSolver.daType, + }, nil } - return &types.Action{ - Type: types.ActionTypeMove, - IsAttack: !game.DefendsParent(*move), - ParentClaim: game.Claims()[move.ParentContractIndex], - Value: move.Value, - }, nil + return nil, nil } diff --git a/op-challenger2/game/fault/solver/game_solver_test.go b/op-challenger2/game/fault/solver/game_solver_test.go index 9eb9a1a41e50..b63ecdeb13c8 100644 --- a/op-challenger2/game/fault/solver/game_solver_test.go +++ b/op-challenger2/game/fault/solver/game_solver_test.go @@ -17,12 +17,12 @@ import ( func TestCalculateNextActions_ChallengeL2BlockNumber(t *testing.T) { startingBlock := big.NewInt(5) - maxDepth := types.Depth(6) + maxDepth := types.Depth(8) challenge := &types.InvalidL2BlockNumberChallenge{ Output: ð.OutputResponse{OutputRoot: eth.Bytes32{0xbb}}, } - nbits := uint64(1) - splitDepth := types.Depth(3) + nbits := uint64(2) + splitDepth := types.Depth(4) claimBuilder := faulttest.NewAlphabetClaimBuilder2(t, startingBlock, maxDepth, nbits, splitDepth) traceProvider := faulttest.NewAlphabetWithProofProvider(t, startingBlock, maxDepth, nil, 0, faulttest.OracleDefaultKey) solver := NewGameSolver(maxDepth, trace.NewSimpleTraceAccessor(traceProvider), types.CallDataType) @@ -42,11 +42,153 @@ func TestCalculateNextActions_ChallengeL2BlockNumber(t *testing.T) { require.Equal(t, challenge, action.InvalidL2BlockNumberChallenge) } +func runStep(t *testing.T, solver *GameSolver, game types.Game, correctTraceProvider types.TraceProvider) (types.Game, []types.Action) { + actions, err := solver.CalculateNextActions(context.Background(), game) + require.NoError(t, err) + + postState := applyActions(game, challengerAddr, actions) + + for i, action := range actions { + t.Logf("Move %v: Type: %v, ParentIdx: %v, Attack: %v, Value: %v, PreState: %v, ProofData: %v", + i, action.Type, action.ParentClaim.ContractIndex, action.IsAttack, action.Value, hex.EncodeToString(action.PreState), hex.EncodeToString(action.ProofData)) + // Check that every move the solver returns meets the generic validation rules + require.NoError(t, checkRules(game, action, correctTraceProvider), "Attempting to perform invalid action") + } + return postState, actions +} + +func TestMultipleRoundsWithNbits1(t *testing.T) { + t.Parallel() + tests := []struct { + name string + actor actor + }{ + { + name: "SingleRoot", + actor: doNothingActor, + }, + { + name: "LinearAttackCorrect", + actor: correctAttackLastClaim, + }, + { + name: "LinearDefendCorrect", + actor: correctDefendLastClaim, + }, + { + name: "LinearAttackIncorrect", + actor: incorrectAttackLastClaim, + }, + { + name: "LinearDefendInorrect", + actor: incorrectDefendLastClaim, + }, + { + name: "LinearDefendIncorrectDefendCorrect", + actor: combineActors(incorrectDefendLastClaim, correctDefendLastClaim), + }, + { + name: "LinearAttackIncorrectDefendCorrect", + actor: combineActors(incorrectAttackLastClaim, correctDefendLastClaim), + }, + { + name: "LinearDefendIncorrectDefendIncorrect", + actor: combineActors(incorrectDefendLastClaim, incorrectDefendLastClaim), + }, + { + name: "LinearAttackIncorrectDefendIncorrect", + actor: combineActors(incorrectAttackLastClaim, incorrectDefendLastClaim), + }, + { + name: "AttackEverythingCorrect", + actor: attackEverythingCorrect, + }, + { + name: "DefendEverythingCorrect", + actor: defendEverythingCorrect, + }, + { + name: "AttackEverythingIncorrect", + actor: attackEverythingIncorrect, + }, + { + name: "DefendEverythingIncorrect", + actor: defendEverythingIncorrect, + }, + { + name: "Exhaustive", + actor: exhaustive, + }, + } + for _, test := range tests { + test := test + for _, rootClaimCorrect := range []bool{true, false} { + rootClaimCorrect := rootClaimCorrect + t.Run(fmt.Sprintf("%v-%v", test.name, rootClaimCorrect), func(t *testing.T) { + t.Parallel() + + maxDepth := types.Depth(6) + startingL2BlockNumber := big.NewInt(50) + nbits := uint64(1) + splitDepth := types.Depth(3) + claimBuilder := faulttest.NewAlphabetClaimBuilder2(t, startingL2BlockNumber, maxDepth, nbits, splitDepth) + builder := claimBuilder.GameBuilder(faulttest.WithInvalidValue(!rootClaimCorrect)) + game := builder.Game + + correctTrace := claimBuilder.CorrectTraceProvider() + solver := NewGameSolver(maxDepth, trace.NewSimpleTraceAccessor(correctTrace), types.CallDataType) + + roundNum := 0 + done := false + for !done { + t.Logf("------ ROUND %v ------", roundNum) + game, _ = runStep2(t, solver, game, correctTrace) + verifyGameRules(t, game, rootClaimCorrect) + + game, done = test.actor.Apply(t, game, correctTrace) + roundNum++ + } + }) + } + } +} + +func applyActions(game types.Game, claimant common.Address, actions []types.Action) types.Game { + claims := game.Claims() + for _, action := range actions { + switch action.Type { + case types.ActionTypeMove: + newPosition := action.ParentClaim.Position.Attack() + if !action.IsAttack { + newPosition = action.ParentClaim.Position.Defend() + } + claim := types.Claim{ + ClaimData: types.ClaimData{ + Value: action.Value, + Bond: big.NewInt(0), + Position: newPosition, + }, + Claimant: claimant, + ContractIndex: len(claims), + ParentContractIndex: action.ParentClaim.ContractIndex, + } + claims = append(claims, claim) + case types.ActionTypeStep: + counteredClaim := claims[action.ParentClaim.ContractIndex] + counteredClaim.CounteredBy = claimant + claims[action.ParentClaim.ContractIndex] = counteredClaim + default: + panic(fmt.Errorf("unknown move type: %v", action.Type)) + } + } + return types.NewGameState2(claims, game.MaxDepth(), game.NBits(), game.SplitDepth()) +} + func TestCalculateNextActions(t *testing.T) { - maxDepth := types.Depth(6) + maxDepth := types.Depth(8) startingL2BlockNumber := big.NewInt(0) - nbits := uint64(1) - splitDepth := types.Depth(3) + nbits := uint64(2) + splitDepth := types.Depth(4) claimBuilder := faulttest.NewAlphabetClaimBuilder2(t, startingL2BlockNumber, maxDepth, nbits, splitDepth) tests := []struct { @@ -57,7 +199,7 @@ func TestCalculateNextActions(t *testing.T) { { name: "AttackRootClaim", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq().ExpectAttack() + builder.Seq().ExpectAttackV2(0) }, }, { @@ -68,136 +210,230 @@ func TestCalculateNextActions(t *testing.T) { { name: "DoNotPerformDuplicateMoves", setupGame: func(builder *faulttest.GameBuilder) { - // Expected move has already been made. - builder.Seq().Attack() + builder.Seq().Attack2(nil, 0) }, }, { name: "RespondToAllClaimsAtDisagreeingLevel", setupGame: func(builder *faulttest.GameBuilder) { - honestClaim := builder.Seq().Attack() - honestClaim.Attack().ExpectDefend() - honestClaim.Defend().ExpectDefend() - honestClaim.Attack(faulttest.WithValue(common.Hash{0xaa})).ExpectAttack() - honestClaim.Attack(faulttest.WithValue(common.Hash{0xbb})).ExpectAttack() - honestClaim.Defend(faulttest.WithValue(common.Hash{0xcc})).ExpectAttack() - honestClaim.Defend(faulttest.WithValue(common.Hash{0xdd})).ExpectAttack() + honestClaim := builder.Seq().Attack2(nil, 0) + honestClaim.Attack2(nil, 0).ExpectAttackV2(3) }, }, { name: "StepAtMaxDepth", setupGame: func(builder *faulttest.GameBuilder) { + values := []common.Hash{{0x81}, {0x82}, {0x83}} lastHonestClaim := builder.Seq(). - Attack(). - Attack(). - Defend(). - Defend(). - Defend() - lastHonestClaim.Attack().ExpectStepDefend() - lastHonestClaim.Attack(faulttest.WithValue(common.Hash{0xdd})).ExpectStepAttack() + Attack2(nil, 0). // honest + Attack2(values, 3). // dishonest + Attack2(nil, 0) // honest + lastHonestClaim.Attack2([]common.Hash{{0x1}, {0x2}, {0x3}}, 0).ExpectStepV2(0) }, }, { name: "PoisonedPreState", setupGame: func(builder *faulttest.GameBuilder) { - // A claim hash that has no pre-image - maliciousStateHash := common.Hash{0x01, 0xaa} + values := []common.Hash{{0x81}, {0x82}, {0x83}} + honest := builder.Seq().Attack2(nil, 0) + dishonest := honest.Attack2(values, 0) + dishonest.ExpectAttackV2(0) + dishonest.Attack2(values, 0) + dishonest.ExpectAttackV2(0) + }, + }, + { + name: "HonestRoot-OneLevelAttack", + rootClaimCorrect: true, + setupGame: func(builder *faulttest.GameBuilder) { + values := []common.Hash{{0x81}, {0x82}, {0x83}} + honest := builder.Seq() + honest.Attack2(nil, 0).ExpectAttackV2(3) + honest.Attack2(values, 0).ExpectAttackV2(0) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(builder.Game.RootClaim().Position.MoveN(nbits, 0), []uint64{1}) + honest.Attack2(values, 0).ExpectAttackV2(1) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(builder.Game.RootClaim().Position.MoveN(nbits, 0), []uint64{2}) + honest.Attack2(values, 0).ExpectAttackV2(2) + }, + }, + { + name: "DishonestRoot-OneLevelAttack", + setupGame: func(builder *faulttest.GameBuilder) { + values := []common.Hash{{0x81}, {0x82}, {0x83}} + dishonest := builder.Seq().ExpectAttackV2(0) + dishonest.Attack2(values, 0).ExpectAttackV2(0) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(builder.Game.RootClaim().Position.MoveN(nbits, 0), []uint64{1}) + dishonest.Attack2(values, 0).ExpectAttackV2(1) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(builder.Game.RootClaim().Position.MoveN(nbits, 0), []uint64{2}) + dishonest.Attack2(values, 0).ExpectAttackV2(2) + }, + }, + { + name: "HonestRoot-TwoLevelAttack-FirstLevelCorrect-SecondLevelCorrect", + rootClaimCorrect: true, + setupGame: func(builder *faulttest.GameBuilder) { + dishonest := builder.Seq().Attack2(nil, 0) + dishonest.Attack2(nil, 0).ExpectAttackV2(3) + dishonest.Attack2(nil, 1).ExpectAttackV2(3) + dishonest.Attack2(nil, 2).ExpectAttackV2(3) + dishonest.Attack2(nil, 3) + }, + }, + { + name: "HonestRoot-TwoLevelAttack-FirstLevelCorrect-SecondLevelIncorrect", + rootClaimCorrect: true, + setupGame: func(builder *faulttest.GameBuilder) { + values := []common.Hash{{0x81}, {0x82}, {0x83}} + dishonest := builder.Seq().Attack2(nil, 0).ExpectAttackV2(3) + lastPosition := builder.Game.Claims()[len(builder.Game.Claims())-1].Position + dishonest.Attack2(values, 0).ExpectAttackV2(0) + dishonest.Attack2(values, 1).ExpectAttackV2(0) + dishonest.Attack2(values, 2).ExpectAttackV2(0) + dishonest.Attack2(values, 3).ExpectAttackV2(0) + + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{0}) + dishonest.Attack2(values, 0).ExpectAttackV2(0) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{1}) + dishonest.Attack2(values, 0).ExpectAttackV2(1) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{2}) + dishonest.Attack2(values, 0).ExpectAttackV2(2) + + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 1), []uint64{0}) + dishonest.Attack2(values, 1).ExpectAttackV2(0) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 1), []uint64{1}) + dishonest.Attack2(values, 1).ExpectAttackV2(1) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 1), []uint64{2}) + dishonest.Attack2(values, 1).ExpectAttackV2(2) - // Dishonest actor counters their own claims to set up a situation with an invalid prestate - // The honest actor should ignore path created by the dishonest actor, only supporting its own attack on the root claim - honestMove := builder.Seq().Attack() // This expected action is the winning move. - dishonestMove := honestMove.Attack(faulttest.WithValue(maliciousStateHash)) - // The expected action by the honest actor - dishonestMove.ExpectAttack() - // The honest actor will ignore this poisoned path - dishonestMove. - Defend(faulttest.WithValue(maliciousStateHash)). - Attack(faulttest.WithValue(maliciousStateHash)) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 2), []uint64{0}) + dishonest.Attack2(values, 2).ExpectAttackV2(0) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 2), []uint64{1}) + dishonest.Attack2(values, 2).ExpectAttackV2(1) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 2), []uint64{2}) + dishonest.Attack2(values, 2).ExpectAttackV2(2) + + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 3), []uint64{0}) + dishonest.Attack2(values, 3).ExpectAttackV2(0) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 3), []uint64{1}) + dishonest.Attack2(values, 3).ExpectAttackV2(1) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 3), []uint64{2}) + dishonest.Attack2(values, 3).ExpectAttackV2(2) }, }, { - name: "Freeloader-ValidClaimAtInvalidAttackPosition", + name: "HonestRoot-TwoLevelAttack-FirstLevelIncorrect-SecondLevelCorrect", + rootClaimCorrect: true, setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). - Attack(). // Honest response to invalid root - Defend().ExpectDefend(). // Defender agrees at this point, we should defend - Attack().ExpectDefend() // Freeloader attacks instead of defends + values := []common.Hash{{0x81}, {0x82}, {0x83}} + honest := builder.Seq() + lastPosition := builder.Game.Claims()[len(builder.Game.Claims())-1].Position + honest.Attack2(values, 0).Attack2(nil, 0) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{1}) + honest.Attack2(values, 0).ExpectAttackV2(1).Attack2(nil, 0).ExpectAttackV2(3) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{2}) + honest.Attack2(values, 0).ExpectAttackV2(2).Attack2(nil, 0).ExpectAttackV2(3) + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{3}) + honest.Attack2(values, 0).ExpectAttackV2(3).Attack2(nil, 0).ExpectAttackV2(3) }, }, { - name: "Freeloader-InvalidClaimAtInvalidAttackPosition", + name: "StepAtMaxDepth-LeftBranch-StepBranch0", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). - Attack(). // Honest response to invalid root - Defend().ExpectDefend(). // Defender agrees at this point, we should defend - Attack(faulttest.WithValue(common.Hash{0xbb})).ExpectAttack() // Freeloader attacks with wrong claim instead of defends + values := []common.Hash{{0x81}, {0x82}, {0x83}} + lastHonestClaim := builder.Seq(). + Attack2(nil, 0). // honest + Attack2(values, 0). // dishonest + Attack2(nil, 0) // honest + lastHonestClaim.Attack2([]common.Hash{{0x1}, {0x2}, {0x3}}, 0).ExpectStepV2(0) }, }, { - name: "Freeloader-InvalidClaimAtValidDefensePosition", + name: "StepAtMaxDepth-LeftBranch-StepBranch1", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). - Attack(). // Honest response to invalid root - Defend().ExpectDefend(). // Defender agrees at this point, we should defend - Defend(faulttest.WithValue(common.Hash{0xbb})).ExpectAttack() // Freeloader defends with wrong claim, we should attack + values := []common.Hash{{0x81}, {0x82}, {0x83}} + lastHonestClaim := builder.Seq(). + Attack2(nil, 0). // honest + Attack2(values, 0). // dishonest + Attack2(nil, 0) // honest + lastPosition := builder.Game.Claims()[len(builder.Game.Claims())-1].Position + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{1}) + lastHonestClaim.Attack2(values, 0).ExpectStepV2(1) }, }, { - name: "Freeloader-InvalidClaimAtValidAttackPosition", + name: "StepAtMaxDepth-LeftBranch-StepBranch2", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). - Attack(). // Honest response to invalid root - Defend(faulttest.WithValue(common.Hash{0xaa})).ExpectAttack(). // Defender disagrees at this point, we should attack - Attack(faulttest.WithValue(common.Hash{0xbb})).ExpectAttack() // Freeloader attacks with wrong claim instead of defends + values := []common.Hash{{0x81}, {0x82}, {0x83}} + lastHonestClaim := builder.Seq(). + Attack2(nil, 0). // honest + Attack2(values, 0). // dishonest + Attack2(nil, 0) // honest + lastPosition := builder.Game.Claims()[len(builder.Game.Claims())-1].Position + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{2}) + lastHonestClaim.Attack2(values, 0).ExpectStepV2(2) }, }, { - name: "Freeloader-InvalidClaimAtInvalidDefensePosition", + name: "StepAtMaxDepth-LeftBranch-StepBranch3", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). - Attack(). // Honest response to invalid root - Defend(faulttest.WithValue(common.Hash{0xaa})).ExpectAttack(). // Defender disagrees at this point, we should attack - Defend(faulttest.WithValue(common.Hash{0xbb})) // Freeloader defends with wrong claim but we must not respond to avoid poisoning + values := []common.Hash{{0x81}, {0x82}, {0x83}} + lastHonestClaim := builder.Seq(). + Attack2(nil, 0). // honest + Attack2(values, 0). // dishonest + Attack2(nil, 0) // honest + lastPosition := builder.Game.Claims()[len(builder.Game.Claims())-1].Position + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{}) + lastHonestClaim.Attack2(values, 0).ExpectStepV2(3) }, }, { - name: "Freeloader-ValidClaimAtInvalidAttackPosition-RespondingToDishonestButCorrectAttack", + name: "StepAtMaxDepth-MiddleBranch-StepBranch0", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). - Attack(). // Honest response to invalid root - Attack().ExpectDefend(). // Defender attacks with correct value, we should defend - Attack().ExpectDefend() // Freeloader attacks with wrong claim, we should defend + values := []common.Hash{{0x81}, {0x82}, {0x83}} + lastHonestClaim := builder.Seq(). + Attack2(nil, 0). // honest + Attack2(values, 1). // dishonest + Attack2(nil, 0) // honest + lastHonestClaim.Attack2([]common.Hash{{0x1}, {0x2}, {0x3}}, 0).ExpectStepV2(0) }, }, { - name: "Freeloader-DoNotCounterOwnClaim", + name: "StepAtMaxDepth-MiddleBranch-StepBranch1", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). - Attack(). // Honest response to invalid root - Attack().ExpectDefend(). // Defender attacks with correct value, we should defend - Attack(). // Freeloader attacks instead, we should defend - Defend() // We do defend and we shouldn't counter our own claim + values := []common.Hash{{0x81}, {0x82}, {0x83}} + lastHonestClaim := builder.Seq(). + Attack2(nil, 0). // honest + Attack2(values, 1). // dishonest + Attack2(nil, 0) // honest + lastPosition := builder.Game.Claims()[len(builder.Game.Claims())-1].Position + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{1}) + lastHonestClaim.Attack2(values, 0).ExpectStepV2(1) }, }, { - name: "Freeloader-ContinueDefendingAgainstFreeloader", + name: "StepAtMaxDepth-MiddleBranch-StepBranch2", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). // invalid root - Attack(). // Honest response to invalid root - Attack().ExpectDefend(). // Defender attacks with correct value, we should defend - Attack(). // Freeloader attacks instead, we should defend - Defend(). // We do defend - Attack(faulttest.WithValue(common.Hash{0xaa})). // freeloader attacks our defense, we should attack - ExpectAttack() + values := []common.Hash{{0x81}, {0x82}, {0x83}} + lastHonestClaim := builder.Seq(). + Attack2(nil, 0). // honest + Attack2(values, 1). // dishonest + Attack2(nil, 0) // honest + lastPosition := builder.Game.Claims()[len(builder.Game.Claims())-1].Position + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{2}) + lastHonestClaim.Attack2(values, 0).ExpectStepV2(2) }, }, { - name: "Freeloader-FreeloaderCountersRootClaim", + name: "StepAtMaxDepth-MiddleBranch-StepBranch3", setupGame: func(builder *faulttest.GameBuilder) { - builder.Seq(). - ExpectAttack(). // Honest response to invalid root - Attack(faulttest.WithValue(common.Hash{0xaa})). // freeloader - ExpectAttack() // Honest response to freeloader + values := []common.Hash{{0x81}, {0x82}, {0x83}} + lastHonestClaim := builder.Seq(). + Attack2(nil, 0). // honest + Attack2(values, 1). // dishonest + Attack2(nil, 0) // honest + lastPosition := builder.Game.Claims()[len(builder.Game.Claims())-1].Position + values = claimBuilder.GetCorrectClaimsAndInvalidClaimAtIndex(lastPosition.MoveN(nbits, 0), []uint64{}) + lastHonestClaim.Attack2(values, 0).ExpectStepV2(3) }, }, } @@ -208,37 +444,88 @@ func TestCalculateNextActions(t *testing.T) { builder := claimBuilder.GameBuilder(faulttest.WithInvalidValue(!test.rootClaimCorrect)) test.setupGame(builder) game := builder.Game - - solver := NewGameSolver(maxDepth, trace.NewSimpleTraceAccessor(claimBuilder.CorrectTraceProvider()), types.CallDataType) - postState, actions := runStep(t, solver, game, claimBuilder.CorrectTraceProvider()) + accessor := trace.NewSimpleTraceAccessor(claimBuilder.CorrectTraceProvider()) + solver := NewGameSolver(maxDepth, accessor, types.CallDataType) + postState, actions := runStep2(t, solver, game, claimBuilder.CorrectTraceProvider()) + preimage := getStepPreimage(t, builder, game, accessor) for i, action := range builder.ExpectedActions { - t.Logf("Expect %v: Type: %v, ParentIdx: %v, Attack: %v, Value: %v, PreState: %v, ProofData: %v", - i, action.Type, action.ParentClaim.ContractIndex, action.IsAttack, action.Value, hex.EncodeToString(action.PreState), hex.EncodeToString(action.ProofData)) + if action.Type == types.ActionTypeStep { + action.OracleData = preimage + } + t.Logf("Expect %v: Type: %v, Position: %v, ParentIdx: %v, Branch: %v, Value: %v, SubValues: %v, PreState: %v, ProofData: %v", + i, action.Type, action.ParentClaim.Position, action.ParentClaim.ContractIndex, action.AttackBranch, action.Value, action.SubValues, hex.EncodeToString(action.PreState), hex.EncodeToString(action.ProofData)) require.Containsf(t, actions, action, "Expected claim %v missing", i) } require.Len(t, actions, len(builder.ExpectedActions), "Incorrect number of actions") - verifyGameRules(t, postState, test.rootClaimCorrect) }) } } -func runStep(t *testing.T, solver *GameSolver, game types.Game, correctTraceProvider types.TraceProvider) (types.Game, []types.Action) { +func runStep2(t *testing.T, solver *GameSolver, game types.Game, correctTraceProvider types.TraceProvider) (types.Game, []types.Action) { actions, err := solver.CalculateNextActions(context.Background(), game) require.NoError(t, err) - postState := applyActions(game, challengerAddr, actions) + postState := applyActions2(game, challengerAddr, actions) for i, action := range actions { - t.Logf("Move %v: Type: %v, ParentIdx: %v, Attack: %v, Value: %v, PreState: %v, ProofData: %v", - i, action.Type, action.ParentClaim.ContractIndex, action.IsAttack, action.Value, hex.EncodeToString(action.PreState), hex.EncodeToString(action.ProofData)) + t.Logf("Real %v: Type: %v, Position: %v, ParentIdx: %v, Branch: %v, Value: %v, SubValues: %v, PreState: %v, ProofData: %v", + i, action.Type, + action.ParentClaim.Position, + action.ParentClaim.ContractIndex, + action.AttackBranch, action.Value, action.SubValues, hex.EncodeToString(action.PreState), hex.EncodeToString(action.ProofData)) // Check that every move the solver returns meets the generic validation rules require.NoError(t, checkRules(game, action, correctTraceProvider), "Attempting to perform invalid action") } return postState, actions } -func TestMultipleRounds(t *testing.T) { +func applyActions2(game types.Game, claimant common.Address, actions []types.Action) types.Game { + claims := game.Claims() + for _, action := range actions { + switch action.Type { + case types.ActionTypeAttackV2: + newPosition := action.ParentClaim.Position.MoveN(game.NBits(), action.AttackBranch) + claim := types.Claim{ + ClaimData: types.ClaimData{ + Value: action.Value, + Bond: big.NewInt(0), + Position: newPosition, + }, + Claimant: claimant, + ContractIndex: len(claims), + ParentContractIndex: action.ParentClaim.ContractIndex, + SubValues: action.SubValues, + AttackBranch: action.AttackBranch, + } + claims = append(claims, claim) + case types.ActionTypeStep: + counteredClaim := claims[action.ParentClaim.ContractIndex] + counteredClaim.CounteredBy = claimant + claims[action.ParentClaim.ContractIndex] = counteredClaim + default: + panic(fmt.Errorf("unknown move type: %v", action.Type)) + } + } + return types.NewGameState2(claims, game.MaxDepth(), game.NBits(), game.SplitDepth()) +} + +func getStepPreimage(t *testing.T, builder *faulttest.GameBuilder, game types.Game, accessor types.TraceAccessor) *types.PreimageOracleData { + if len(builder.ExpectedActions) == 0 { + return nil + } + claims := game.Claims() + lastClaim := claims[len(claims)-1] + lastExpectedAction := builder.ExpectedActions[len(builder.ExpectedActions)-1] + if lastExpectedAction.Type != types.ActionTypeStep { + return nil + } + _, _, preimage, err := accessor.GetStepData2(context.Background(), game, lastClaim, lastClaim.MoveRightN(lastExpectedAction.AttackBranch)) + require.NoError(t, err) + return preimage +} + +func TestMultipleRoundsWithNbits2(t *testing.T) { t.Parallel() tests := []struct { name string @@ -303,15 +590,16 @@ func TestMultipleRounds(t *testing.T) { } for _, test := range tests { test := test - for _, rootClaimCorrect := range []bool{true, false} { + for _, rootClaimCorrect := range []bool{false} { rootClaimCorrect := rootClaimCorrect t.Run(fmt.Sprintf("%v-%v", test.name, rootClaimCorrect), func(t *testing.T) { t.Parallel() - maxDepth := types.Depth(6) + maxDepth := types.Depth(10) startingL2BlockNumber := big.NewInt(50) - nbits := uint64(1) - splitDepth := types.Depth(3) + nbits := uint64(2) + // splitDepth can't be 4 when maxDepth=8, because we can only attack branch 0 at claim with depth of splitDepth+nbits + splitDepth := types.Depth(4) claimBuilder := faulttest.NewAlphabetClaimBuilder2(t, startingL2BlockNumber, maxDepth, nbits, splitDepth) builder := claimBuilder.GameBuilder(faulttest.WithInvalidValue(!rootClaimCorrect)) game := builder.Game @@ -323,7 +611,7 @@ func TestMultipleRounds(t *testing.T) { done := false for !done { t.Logf("------ ROUND %v ------", roundNum) - game, _ = runStep(t, solver, game, correctTrace) + game, _ = runStep2(t, solver, game, correctTrace) verifyGameRules(t, game, rootClaimCorrect) game, done = test.actor.Apply(t, game, correctTrace) @@ -333,34 +621,3 @@ func TestMultipleRounds(t *testing.T) { } } } - -func applyActions(game types.Game, claimant common.Address, actions []types.Action) types.Game { - claims := game.Claims() - for _, action := range actions { - switch action.Type { - case types.ActionTypeMove: - newPosition := action.ParentClaim.Position.Attack() - if !action.IsAttack { - newPosition = action.ParentClaim.Position.Defend() - } - claim := types.Claim{ - ClaimData: types.ClaimData{ - Value: action.Value, - Bond: big.NewInt(0), - Position: newPosition, - }, - Claimant: claimant, - ContractIndex: len(claims), - ParentContractIndex: action.ParentClaim.ContractIndex, - } - claims = append(claims, claim) - case types.ActionTypeStep: - counteredClaim := claims[action.ParentClaim.ContractIndex] - counteredClaim.CounteredBy = claimant - claims[action.ParentClaim.ContractIndex] = counteredClaim - default: - panic(fmt.Errorf("unknown move type: %v", action.Type)) - } - } - return types.NewGameState2(claims, game.MaxDepth(), game.NBits(), game.SplitDepth()) -} diff --git a/op-challenger2/game/fault/solver/rules.go b/op-challenger2/game/fault/solver/rules.go index 88d790296a78..45ce9af7c5c4 100644 --- a/op-challenger2/game/fault/solver/rules.go +++ b/op-challenger2/game/fault/solver/rules.go @@ -20,13 +20,13 @@ type actionRule func(game types.Game, action types.Action, correctTrace types.Tr var rules = []actionRule{ parentMustExist, onlyStepAtMaxDepth, - onlyMoveBeforeMaxDepth, + onlyAttackBeforeMaxDepth, doNotDuplicateExistingMoves, doNotStepAlreadyCounteredClaims, - doNotDefendRootClaim, + onlyAttackRootClaimZeroBranch, avoidPoisonedPrestate, - detectPoisonedStepPrestate, - detectFailedStep, + //detectPoisonedStepPrestate, + //detectFailedStep, doNotCounterSelf, } @@ -66,15 +66,13 @@ func onlyStepAtMaxDepth(game types.Game, action types.Action, _ types.TraceProvi return nil } -// onlyMoveBeforeMaxDepth verifies that move actions are not performed against leaf claims -// Rationale: The action would be rejected by the contracts -func onlyMoveBeforeMaxDepth(game types.Game, action types.Action, _ types.TraceProvider) error { - if action.Type == types.ActionTypeMove { +func onlyAttackBeforeMaxDepth(game types.Game, action types.Action, _ types.TraceProvider) error { + if action.Type == types.ActionTypeAttackV2 { return nil } parentDepth := game.Claims()[action.ParentClaim.ContractIndex].Position.Depth() if parentDepth < game.MaxDepth() { - return fmt.Errorf("parent (%v) not at max depth (%v) but attempting to perform %v action instead of move", + return fmt.Errorf("parent (%v) not at max depth (%v) but attempting to perform %v action instead of attackV2", parentDepth, game.MaxDepth(), action.Type) } return nil @@ -103,11 +101,9 @@ func doNotStepAlreadyCounteredClaims(game types.Game, action types.Action, _ typ return nil } -// doNotDefendRootClaim checks the challenger doesn't attempt to defend the root claim -// Rationale: The action would be rejected by the contracts -func doNotDefendRootClaim(game types.Game, action types.Action, _ types.TraceProvider) error { - if game.Claims()[action.ParentClaim.ContractIndex].IsRootPosition() && !action.IsAttack { - return fmt.Errorf("defending the root claim at idx %v", action.ParentClaim.ContractIndex) +func onlyAttackRootClaimZeroBranch(game types.Game, action types.Action, _ types.TraceProvider) error { + if game.Claims()[action.ParentClaim.ContractIndex].IsRootPosition() && action.AttackBranch != 0 { + return fmt.Errorf("attacking the root claim at idx %v with branch %v", action.ParentClaim.ContractIndex, action.AttackBranch) } return nil } @@ -163,7 +159,8 @@ func avoidPoisonedPrestate(game types.Game, action types.Action, correctTrace ty if err != nil { return fmt.Errorf("failed to get correct trace at position %v: %w", preStateClaim.Position, err) } - if correctValue != preStateClaim.Value { + preStateClaimValue := (*preStateClaim.SubValues)[0] + if correctValue != preStateClaimValue { err = fmt.Errorf("prestate poisoned claim %v has invalid prestate and is left of honest claim countering %v at trace index %v", preStateClaim.ContractIndex, action.ParentClaim.ContractIndex, honestTraceIndex) return err } @@ -194,10 +191,7 @@ func detectFailedStep(game types.Game, action types.Action, correctTrace types.T return nil } honestTraceIndex := position.TraceIndex(game.MaxDepth()) - poststateIndex := honestTraceIndex - if !action.IsAttack { - poststateIndex = new(big.Int).Add(honestTraceIndex, big.NewInt(1)) - } + poststateIndex := new(big.Int).Add(honestTraceIndex, big.NewInt(int64(action.AttackBranch))) // Walk back up the claims and find the claim required post state index claim := game.Claims()[action.ParentClaim.ContractIndex] poststateClaim, ok := game.AncestorWithTraceIndex(claim, poststateIndex) @@ -208,7 +202,7 @@ func detectFailedStep(game types.Game, action types.Action, correctTrace types.T if err != nil { return fmt.Errorf("failed to get correct trace at position %v: %w", poststateClaim.Position, err) } - validStep := correctValue == poststateClaim.Value + validStep := correctValue == (*poststateClaim.SubValues)[0] parentPostAgree := (claim.Depth()-poststateClaim.Depth())%2 == 0 if parentPostAgree == validStep { return fmt.Errorf("failed step against claim at %v using poststate from claim %v post state is correct? %v parentPostAgree? %v", @@ -272,8 +266,5 @@ func resultingPosition(game types.Game, action types.Action) types.Position { if action.Type == types.ActionTypeStep { return parentPos } - if action.IsAttack { - return parentPos.Attack() - } - return parentPos.Defend() + return parentPos.MoveN(uint64(game.NBits()), action.AttackBranch) } diff --git a/op-challenger2/game/fault/solver/solver.go b/op-challenger2/game/fault/solver/solver.go index a82cc5bb88a0..b59ee357d13b 100644 --- a/op-challenger2/game/fault/solver/solver.go +++ b/op-challenger2/game/fault/solver/solver.go @@ -6,7 +6,9 @@ import ( "errors" "fmt" + "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/types" + "github.com/ethereum/go-ethereum/common" ) var ( @@ -63,7 +65,7 @@ func (s *claimSolver) shouldCounter(game types.Game, claim types.Claim, honestCl } // NextMove returns the next move to make given the current state of the game. -func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, game types.Game, honestClaims *honestClaimTracker) (*types.Claim, error) { +func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, game types.Game, honestClaims *honestClaimTracker, branch uint64) (*types.Claim, error) { if claim.Depth() == s.gameDepth { return nil, types.ErrGameDepthReached } @@ -74,27 +76,40 @@ func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, game type return nil, nil } - if agree, err := s.agreeWithClaim(ctx, game, claim); err != nil { + agree, err := s.agreeWithClaimV2(ctx, game, claim, branch) + if err != nil { return nil, err - } else if agree { - return s.defend(ctx, game, claim) + } + if agree { + if claim.Depth() == game.TraceRootDepth() && branch == 0 { + // Only useful in alphabet game, because the alphabet game has a constant status byte, and is not safe from someone being dishonest in + // output bisection and then posting a correct execution trace bisection root claim. + // when root claim of output bisection is dishonest, + // root claim of execution trace bisection is made by the dishonest actor but is honest + // we should counter it. + return s.attackV2(ctx, game, claim, branch) + } + if branch < game.MaxAttackBranch()-1 { + return nil, nil + } + return s.attackV2(ctx, game, claim, branch+1) } else { - return s.attack(ctx, game, claim) + return s.attackV2(ctx, game, claim, branch) } } type StepData struct { - LeafClaim types.Claim - IsAttack bool - PreState []byte - ProofData []byte - OracleData *types.PreimageOracleData + LeafClaim types.Claim + AttackBranch uint64 + PreState []byte + ProofData []byte + OracleData *types.PreimageOracleData } // AttemptStep determines what step, if any, should occur for a given leaf claim. // An error will be returned if the claim is not at the max depth. // Returns nil, nil if no step should be performed. -func (s *claimSolver) AttemptStep(ctx context.Context, game types.Game, claim types.Claim, honestClaims *honestClaimTracker) (*StepData, error) { +func (s *claimSolver) AttemptStep(ctx context.Context, game types.Game, claim types.Claim, honestClaims *honestClaimTracker, branch uint64) (*StepData, error) { if claim.Depth() != s.gameDepth { return nil, ErrStepNonLeafNode } @@ -105,66 +120,70 @@ func (s *claimSolver) AttemptStep(ctx context.Context, game types.Game, claim ty return nil, nil } - claimCorrect, err := s.agreeWithClaim(ctx, game, claim) + claimCorrect, err := s.agreeWithClaimV2(ctx, game, claim, branch) if err != nil { return nil, err } var position types.Position + attackBranch := branch if !claimCorrect { // Attack the claim by executing step index, so we need to get the pre-state of that index - position = claim.Position + position = claim.Position.MoveRightN(branch) } else { - // Defend and use this claim as the starting point to execute the step after. - // Thus, we need the pre-state of the next step. - position = claim.Position.MoveRight() - } - - preState, proofData, oracleData, err := s.trace.GetStepData(ctx, game, claim, position) + if branch == game.MaxAttackBranch()-1 { + // If we are at the max attack branch, we need to step on the next branch + position = claim.Position.MoveRightN(branch + 1) + attackBranch = branch + 1 + } else { + return nil, nil + } + } + preState, proofData, oracleData, err := s.trace.GetStepData2(ctx, game, claim, position) if err != nil { return nil, err } - return &StepData{ - LeafClaim: claim, - IsAttack: !claimCorrect, - PreState: preState, - ProofData: proofData, - OracleData: oracleData, + LeafClaim: claim, + AttackBranch: attackBranch, + PreState: preState, + ProofData: proofData, + OracleData: oracleData, }, nil } -// attack returns a response that attacks the claim. -func (s *claimSolver) attack(ctx context.Context, game types.Game, claim types.Claim) (*types.Claim, error) { - position := claim.Attack() - value, err := s.trace.Get(ctx, game, claim, position) - if err != nil { - return nil, fmt.Errorf("attack claim: %w", err) +// agreeWithClaim returns true if the claim is correct according to the internal [TraceProvider]. +func (s *claimSolver) agreeWithClaimV2(ctx context.Context, game types.Game, claim types.Claim, branch uint64) (bool, error) { + if branch >= uint64(len(*claim.SubValues)) { + return true, fmt.Errorf("branch must be less than maxAttachBranch") } - return &types.Claim{ - ClaimData: types.ClaimData{Value: value, Position: position}, - ParentContractIndex: claim.ContractIndex, - }, nil + ourValue, err := s.trace.Get(ctx, game, claim, claim.Position.MoveRightN(branch)) + return bytes.Equal(ourValue[:], (*claim.SubValues)[branch][:]), err } -// defend returns a response that defends the claim. -func (s *claimSolver) defend(ctx context.Context, game types.Game, claim types.Claim) (*types.Claim, error) { - if claim.IsRoot() { - return nil, nil - } - position := claim.Defend() - value, err := s.trace.Get(ctx, game, claim, position) - if err != nil { - return nil, fmt.Errorf("defend claim: %w", err) - } +func (s *claimSolver) attackV2(ctx context.Context, game types.Game, claim types.Claim, branch uint64) (*types.Claim, error) { + var err error + var value common.Hash + var values []common.Hash + maxAttackBranch := game.MaxAttackBranch() + position := claim.MoveN(game.NBits(), branch) + for i := uint64(0); i < maxAttackBranch; i++ { + tmpPosition := position.MoveRightN(i) + if tmpPosition.Depth() == (game.SplitDepth()+types.Depth(game.NBits())) && i != 0 { + value = common.Hash{} + } else { + value, err = s.trace.Get(ctx, game, claim, tmpPosition) + if err != nil { + return nil, fmt.Errorf("attack claim: %w", err) + } + } + values = append(values, value) + } + hash := contracts.SubValuesHash(values) return &types.Claim{ - ClaimData: types.ClaimData{Value: value, Position: position}, + ClaimData: types.ClaimData{Value: hash, Position: position}, ParentContractIndex: claim.ContractIndex, + SubValues: &values, + AttackBranch: branch, }, nil } - -// agreeWithClaim returns true if the claim is correct according to the internal [TraceProvider]. -func (s *claimSolver) agreeWithClaim(ctx context.Context, game types.Game, claim types.Claim) (bool, error) { - ourValue, err := s.trace.Get(ctx, game, claim, claim.Position) - return bytes.Equal(ourValue[:], claim.Value[:]), err -} diff --git a/op-challenger2/game/fault/solver/solver_test.go b/op-challenger2/game/fault/solver/solver_test.go index 4fb5723fde8e..5a0e1f595cc1 100644 --- a/op-challenger2/game/fault/solver/solver_test.go +++ b/op-challenger2/game/fault/solver/solver_test.go @@ -5,84 +5,124 @@ import ( "math/big" "testing" + "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/contracts" faulttest "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/test" "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/trace" + "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/trace/alphabet" + "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/trace/split" "github.com/ethereum-optimism/optimism/op-challenger2/game/fault/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" ) -func TestAttemptStep(t *testing.T) { - maxDepth := types.Depth(3) +func TestAttemptStepNary2(t *testing.T) { + maxDepth := types.Depth(7) startingL2BlockNumber := big.NewInt(0) nbits := uint64(1) splitDepth := types.Depth(3) claimBuilder := faulttest.NewAlphabetClaimBuilder2(t, startingL2BlockNumber, maxDepth, nbits, splitDepth) + traceDepth := maxDepth - splitDepth - types.Depth(nbits) // Last accessible leaf is the second last trace index // The root node is used for the last trace index and can only be attacked. - lastLeafTraceIndex := big.NewInt(1<