Commit 64d1c30c authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Make solver module work at the entire game level. (#7175)

Introduces GameSolver which evaluates the overall game state and decides all actions to take. Agent now uses GameSolver so that it has no knowledge of the game playing strategy.
parent a472f966
...@@ -17,8 +17,7 @@ import ( ...@@ -17,8 +17,7 @@ import (
type Responder interface { type Responder interface {
CallResolve(ctx context.Context) (gameTypes.GameStatus, error) CallResolve(ctx context.Context) (gameTypes.GameStatus, error)
Resolve(ctx context.Context) error Resolve(ctx context.Context) error
Respond(ctx context.Context, response types.Claim) error PerformAction(ctx context.Context, action solver.Action) error
Step(ctx context.Context, stepData types.StepCallData) error
} }
type ClaimLoader interface { type ClaimLoader interface {
...@@ -27,7 +26,7 @@ type ClaimLoader interface { ...@@ -27,7 +26,7 @@ type ClaimLoader interface {
type Agent struct { type Agent struct {
metrics metrics.Metricer metrics metrics.Metricer
solver *solver.Solver solver *solver.GameSolver
loader ClaimLoader loader ClaimLoader
responder Responder responder Responder
updater types.OracleUpdater updater types.OracleUpdater
...@@ -39,7 +38,7 @@ type Agent struct { ...@@ -39,7 +38,7 @@ type Agent struct {
func NewAgent(m metrics.Metricer, loader ClaimLoader, maxDepth int, trace types.TraceProvider, responder Responder, updater types.OracleUpdater, agreeWithProposedOutput bool, log log.Logger) *Agent { func NewAgent(m metrics.Metricer, loader ClaimLoader, maxDepth int, trace types.TraceProvider, responder Responder, updater types.OracleUpdater, agreeWithProposedOutput bool, log log.Logger) *Agent {
return &Agent{ return &Agent{
metrics: m, metrics: m,
solver: solver.NewSolver(maxDepth, trace), solver: solver.NewGameSolver(maxDepth, trace),
loader: loader, loader: loader,
responder: responder, responder: responder,
updater: updater, updater: updater,
...@@ -58,16 +57,34 @@ func (a *Agent) Act(ctx context.Context) error { ...@@ -58,16 +57,34 @@ func (a *Agent) Act(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("create game from contracts: %w", err) return fmt.Errorf("create game from contracts: %w", err)
} }
// Create counter claims
for _, claim := range game.Claims() { // Calculate the actions to take
if err := a.move(ctx, claim, game); err != nil && !errors.Is(err, types.ErrGameDepthReached) { actions, err := a.solver.CalculateNextActions(ctx, game)
log.Error("Failed to move", "err", err) if err != nil {
} log.Error("Failed to calculate all required moves", "err", err)
} }
// Step on all leaf claims
for _, claim := range game.Claims() { // Perform the actions
if err := a.step(ctx, claim, game); err != nil { for _, action := range actions {
log.Error("Failed to step", "err", err) log := a.log.New("action", action.Type, "is_attack", action.IsAttack, "parent", action.ParentIdx, "value", action.Value)
if action.OracleData != nil {
a.log.Info("Updating oracle data", "oracleKey", action.OracleData.OracleKey, "oracleData", action.OracleData.OracleData)
if err := a.updater.UpdateOracle(ctx, action.OracleData); err != nil {
return fmt.Errorf("failed to load oracle data: %w", err)
}
}
switch action.Type {
case solver.ActionTypeMove:
a.metrics.RecordGameMove()
case solver.ActionTypeStep:
a.metrics.RecordGameStep()
}
log.Info("Performing action")
err := a.responder.PerformAction(ctx, action)
if err != nil {
log.Error("Action failed", "err", err)
} }
} }
return nil return nil
...@@ -118,68 +135,3 @@ func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) { ...@@ -118,68 +135,3 @@ func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
} }
return game, nil return game, nil
} }
// move determines & executes the next move given a claim
func (a *Agent) move(ctx context.Context, claim types.Claim, game types.Game) error {
nextMove, err := a.solver.NextMove(ctx, claim, game.AgreeWithClaimLevel(claim))
if err != nil {
return fmt.Errorf("execute next move: %w", err)
}
if nextMove == nil {
a.log.Debug("No next move")
return nil
}
move := *nextMove
log := a.log.New("is_defend", move.DefendsParent(), "depth", move.Depth(), "index_at_depth", move.IndexAtDepth(),
"value", move.Value, "trace_index", move.TraceIndex(a.maxDepth),
"parent_value", claim.Value, "parent_trace_index", claim.TraceIndex(a.maxDepth))
if game.IsDuplicate(move) {
log.Debug("Skipping duplicate move")
return nil
}
a.metrics.RecordGameMove()
log.Info("Performing move")
return a.responder.Respond(ctx, move)
}
// step determines & executes the next step against a leaf claim through the responder
func (a *Agent) step(ctx context.Context, claim types.Claim, game types.Game) error {
if claim.Depth() != a.maxDepth {
return nil
}
agreeWithClaimLevel := game.AgreeWithClaimLevel(claim)
if agreeWithClaimLevel {
a.log.Debug("Agree with leaf claim, skipping step", "claim_depth", claim.Depth(), "maxDepth", a.maxDepth)
return nil
}
if claim.Countered {
a.log.Debug("Step already executed against claim", "depth", claim.Depth(), "index_at_depth", claim.IndexAtDepth(), "value", claim.Value)
return nil
}
a.log.Info("Attempting step", "claim_depth", claim.Depth(), "maxDepth", a.maxDepth)
step, err := a.solver.AttemptStep(ctx, claim, agreeWithClaimLevel)
if err != nil {
return fmt.Errorf("attempt step: %w", err)
}
if step.OracleData != nil {
a.log.Info("Updating oracle data", "oracleKey", step.OracleData.OracleKey, "oracleData", step.OracleData.OracleData)
if err := a.updater.UpdateOracle(ctx, step.OracleData); err != nil {
return fmt.Errorf("failed to load oracle data: %w", err)
}
}
a.log.Info("Performing step", "is_attack", step.IsAttack,
"depth", step.LeafClaim.Depth(), "index_at_depth", step.LeafClaim.IndexAtDepth(), "value", step.LeafClaim.Value)
a.metrics.RecordGameStep()
callData := types.StepCallData{
ClaimIndex: uint64(step.LeafClaim.ContractIndex),
IsAttack: step.IsAttack,
StateData: step.PreState,
Proof: step.ProofData,
}
return a.responder.Step(ctx, callData)
}
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/test" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/test"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
...@@ -144,11 +145,7 @@ func (s *stubResponder) Resolve(ctx context.Context) error { ...@@ -144,11 +145,7 @@ func (s *stubResponder) Resolve(ctx context.Context) error {
return s.resolveErr return s.resolveErr
} }
func (s *stubResponder) Respond(ctx context.Context, response types.Claim) error { func (s *stubResponder) PerformAction(ctx context.Context, response solver.Action) error {
panic("Not implemented")
}
func (s *stubResponder) Step(ctx context.Context, stepData types.StepCallData) error {
panic("Not implemented") panic("Not implemented")
} }
......
...@@ -5,7 +5,6 @@ import ( ...@@ -5,7 +5,6 @@ import (
"testing" "testing"
"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/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -94,12 +93,7 @@ func TestBuildFaultStepData(t *testing.T) { ...@@ -94,12 +93,7 @@ func TestBuildFaultStepData(t *testing.T) {
resp, _ := newTestFaultResponder(t) resp, _ := newTestFaultResponder(t)
data, err := resp.buildStepTxData(types.StepCallData{ data, err := resp.buildStepTxData(2, false, []byte{0x01}, []byte{0x02})
ClaimIndex: 2,
IsAttack: false,
StateData: []byte{0x01},
Proof: []byte{0x02},
})
require.NoError(t, err) require.NoError(t, err)
opts.GasLimit = 100_000 opts.GasLimit = 100_000
......
...@@ -5,7 +5,7 @@ import ( ...@@ -5,7 +5,7 @@ import (
"math/big" "math/big"
"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/solver"
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-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
...@@ -16,8 +16,8 @@ import ( ...@@ -16,8 +16,8 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// faultResponder implements the [Responder] interface to send onchain transactions. // FaultResponder implements the [Responder] interface to send onchain transactions.
type faultResponder struct { type FaultResponder struct {
log log.Logger log log.Logger
txMgr txmgr.TxManager txMgr txmgr.TxManager
...@@ -26,13 +26,13 @@ type faultResponder struct { ...@@ -26,13 +26,13 @@ type faultResponder struct {
fdgAbi *abi.ABI fdgAbi *abi.ABI
} }
// NewFaultResponder returns a new [faultResponder]. // NewFaultResponder returns a new [FaultResponder].
func NewFaultResponder(logger log.Logger, txManagr txmgr.TxManager, fdgAddr common.Address) (*faultResponder, error) { func NewFaultResponder(logger log.Logger, txManagr txmgr.TxManager, fdgAddr common.Address) (*FaultResponder, error) {
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi() fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &faultResponder{ return &FaultResponder{
log: logger, log: logger,
txMgr: txManagr, txMgr: txManagr,
fdgAddr: fdgAddr, fdgAddr: fdgAddr,
...@@ -41,7 +41,7 @@ func NewFaultResponder(logger log.Logger, txManagr txmgr.TxManager, fdgAddr comm ...@@ -41,7 +41,7 @@ func NewFaultResponder(logger log.Logger, txManagr txmgr.TxManager, fdgAddr comm
} }
// buildFaultDefendData creates the transaction data for the Defend function. // buildFaultDefendData creates the transaction data for the Defend function.
func (r *faultResponder) buildFaultDefendData(parentContractIndex int, pivot [32]byte) ([]byte, error) { func (r *FaultResponder) buildFaultDefendData(parentContractIndex int, pivot [32]byte) ([]byte, error) {
return r.fdgAbi.Pack( return r.fdgAbi.Pack(
"defend", "defend",
big.NewInt(int64(parentContractIndex)), big.NewInt(int64(parentContractIndex)),
...@@ -50,7 +50,7 @@ func (r *faultResponder) buildFaultDefendData(parentContractIndex int, pivot [32 ...@@ -50,7 +50,7 @@ func (r *faultResponder) buildFaultDefendData(parentContractIndex int, pivot [32
} }
// buildFaultAttackData creates the transaction data for the Attack function. // buildFaultAttackData creates the transaction data for the Attack function.
func (r *faultResponder) buildFaultAttackData(parentContractIndex int, pivot [32]byte) ([]byte, error) { func (r *FaultResponder) buildFaultAttackData(parentContractIndex int, pivot [32]byte) ([]byte, error) {
return r.fdgAbi.Pack( return r.fdgAbi.Pack(
"attack", "attack",
big.NewInt(int64(parentContractIndex)), big.NewInt(int64(parentContractIndex)),
...@@ -59,30 +59,13 @@ func (r *faultResponder) buildFaultAttackData(parentContractIndex int, pivot [32 ...@@ -59,30 +59,13 @@ func (r *faultResponder) buildFaultAttackData(parentContractIndex int, pivot [32
} }
// buildResolveData creates the transaction data for the Resolve function. // buildResolveData creates the transaction data for the Resolve function.
func (r *faultResponder) buildResolveData() ([]byte, error) { func (r *FaultResponder) buildResolveData() ([]byte, error) {
return r.fdgAbi.Pack("resolve") return r.fdgAbi.Pack("resolve")
} }
// BuildTx builds the transaction for the [faultResponder].
func (r *faultResponder) BuildTx(ctx context.Context, response types.Claim) ([]byte, error) {
if response.DefendsParent() {
txData, err := r.buildFaultDefendData(response.ParentContractIndex, response.ValueBytes())
if err != nil {
return nil, err
}
return txData, nil
} else {
txData, err := r.buildFaultAttackData(response.ParentContractIndex, response.ValueBytes())
if err != nil {
return nil, err
}
return txData, nil
}
}
// CallResolve determines if the resolve function on the fault dispute game contract // CallResolve determines if the resolve function on the fault dispute game contract
// would succeed. Returns the game status if the call would succeed, errors otherwise. // would succeed. Returns the game status if the call would succeed, errors otherwise.
func (r *faultResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) { func (r *FaultResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
txData, err := r.buildResolveData() txData, err := r.buildResolveData()
if err != nil { if err != nil {
return gameTypes.GameStatusInProgress, err return gameTypes.GameStatusInProgress, err
...@@ -102,7 +85,7 @@ func (r *faultResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, ...@@ -102,7 +85,7 @@ func (r *faultResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus,
} }
// Resolve executes a resolve transaction to resolve a fault dispute game. // Resolve executes a resolve transaction to resolve a fault dispute game.
func (r *faultResponder) Resolve(ctx context.Context) error { func (r *FaultResponder) Resolve(ctx context.Context) error {
txData, err := r.buildResolveData() txData, err := r.buildResolveData()
if err != nil { if err != nil {
return err return err
...@@ -111,9 +94,19 @@ func (r *faultResponder) Resolve(ctx context.Context) error { ...@@ -111,9 +94,19 @@ func (r *faultResponder) Resolve(ctx context.Context) error {
return r.sendTxAndWait(ctx, txData) return r.sendTxAndWait(ctx, txData)
} }
// Respond takes a [Claim] and executes the response action. func (r *FaultResponder) PerformAction(ctx context.Context, action solver.Action) error {
func (r *faultResponder) Respond(ctx context.Context, response types.Claim) error { var txData []byte
txData, err := r.BuildTx(ctx, response) var err error
switch action.Type {
case solver.ActionTypeMove:
if action.IsAttack {
txData, err = r.buildFaultAttackData(action.ParentIdx, action.Value)
} else {
txData, err = r.buildFaultDefendData(action.ParentIdx, action.Value)
}
case solver.ActionTypeStep:
txData, err = r.buildStepTxData(uint64(action.ParentIdx), action.IsAttack, action.PreState, action.ProofData)
}
if err != nil { if err != nil {
return err return err
} }
...@@ -122,7 +115,7 @@ func (r *faultResponder) Respond(ctx context.Context, response types.Claim) erro ...@@ -122,7 +115,7 @@ func (r *faultResponder) Respond(ctx context.Context, response types.Claim) erro
// sendTxAndWait sends a transaction through the [txmgr] and waits for a receipt. // sendTxAndWait sends a transaction through the [txmgr] and waits for a receipt.
// This sets the tx GasLimit to 0, performing gas estimation online through the [txmgr]. // This sets the tx GasLimit to 0, performing gas estimation online through the [txmgr].
func (r *faultResponder) sendTxAndWait(ctx context.Context, txData []byte) error { func (r *FaultResponder) sendTxAndWait(ctx context.Context, txData []byte) error {
receipt, err := r.txMgr.Send(ctx, txmgr.TxCandidate{ receipt, err := r.txMgr.Send(ctx, txmgr.TxCandidate{
To: &r.fdgAddr, To: &r.fdgAddr,
TxData: txData, TxData: txData,
...@@ -140,21 +133,12 @@ func (r *faultResponder) sendTxAndWait(ctx context.Context, txData []byte) error ...@@ -140,21 +133,12 @@ func (r *faultResponder) sendTxAndWait(ctx context.Context, txData []byte) error
} }
// buildStepTxData creates the transaction data for the step function. // buildStepTxData creates the transaction data for the step function.
func (r *faultResponder) buildStepTxData(stepData types.StepCallData) ([]byte, error) { func (r *FaultResponder) buildStepTxData(claimIdx uint64, isAttack bool, stateData []byte, proof []byte) ([]byte, error) {
return r.fdgAbi.Pack( return r.fdgAbi.Pack(
"step", "step",
big.NewInt(int64(stepData.ClaimIndex)), big.NewInt(int64(claimIdx)),
stepData.IsAttack, isAttack,
stepData.StateData, stateData,
stepData.Proof, proof,
) )
} }
// Step accepts step data and executes the step on the fault dispute game contract.
func (r *faultResponder) Step(ctx context.Context, stepData types.StepCallData) error {
txData, err := r.buildStepTxData(stepData)
if err != nil {
return err
}
return r.sendTxAndWait(ctx, txData)
}
...@@ -7,7 +7,7 @@ import ( ...@@ -7,7 +7,7 @@ import (
"testing" "testing"
"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/solver"
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-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
...@@ -74,73 +74,98 @@ func TestResolve(t *testing.T) { ...@@ -74,73 +74,98 @@ func TestResolve(t *testing.T) {
} }
// TestRespond tests the [Responder.Respond] method. // TestRespond tests the [Responder.Respond] method.
func TestRespond(t *testing.T) { func TestPerformAction(t *testing.T) {
t.Run("send fails", func(t *testing.T) { t.Run("send fails", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t) responder, mockTxMgr := newTestFaultResponder(t)
mockTxMgr.sendFails = true mockTxMgr.sendFails = true
err := responder.Respond(context.Background(), generateMockResponseClaim()) err := responder.PerformAction(context.Background(), solver.Action{
Type: solver.ActionTypeMove,
ParentIdx: 123,
IsAttack: true,
Value: common.Hash{0xaa},
})
require.ErrorIs(t, err, mockSendError) require.ErrorIs(t, err, mockSendError)
require.Equal(t, 0, mockTxMgr.sends) require.Equal(t, 0, mockTxMgr.sends)
}) })
t.Run("sends response", func(t *testing.T) { t.Run("sends response", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t) responder, mockTxMgr := newTestFaultResponder(t)
err := responder.Respond(context.Background(), generateMockResponseClaim()) err := responder.PerformAction(context.Background(), solver.Action{
Type: solver.ActionTypeMove,
ParentIdx: 123,
IsAttack: true,
Value: common.Hash{0xaa},
})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends) require.Equal(t, 1, mockTxMgr.sends)
}) })
}
// TestBuildTx tests the [Responder.BuildTx] method.
func TestBuildTx(t *testing.T) {
t.Run("attack", func(t *testing.T) { t.Run("attack", func(t *testing.T) {
responder, _ := newTestFaultResponder(t) responder, mockTxMgr := newTestFaultResponder(t)
responseClaim := generateMockResponseClaim() action := solver.Action{
responseClaim.ParentContractIndex = 7 Type: solver.ActionTypeMove,
tx, err := responder.BuildTx(context.Background(), responseClaim) ParentIdx: 123,
IsAttack: true,
Value: common.Hash{0xaa},
}
err := responder.PerformAction(context.Background(), action)
require.NoError(t, err) require.NoError(t, err)
// Pack the tx data manually. // Pack the tx data manually.
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi() fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err) require.NoError(t, err)
parent := big.NewInt(int64(7)) expected, err := fdgAbi.Pack("attack", big.NewInt(int64(action.ParentIdx)), action.Value)
claim := responseClaim.ValueBytes()
expected, err := fdgAbi.Pack("attack", parent, claim)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, tx)
require.Len(t, mockTxMgr.sent, 1)
require.Equal(t, expected, mockTxMgr.sent[0].TxData)
}) })
t.Run("defend", func(t *testing.T) { t.Run("defend", func(t *testing.T) {
responder, _ := newTestFaultResponder(t) responder, mockTxMgr := newTestFaultResponder(t)
responseClaim := types.Claim{ action := solver.Action{
ClaimData: types.ClaimData{ Type: solver.ActionTypeMove,
Value: common.Hash{0x01}, ParentIdx: 123,
Position: types.NewPositionFromGIndex(3), IsAttack: false,
}, Value: common.Hash{0xaa},
Parent: types.ClaimData{
Value: common.Hash{0x02},
Position: types.NewPositionFromGIndex(6),
},
ContractIndex: 0,
ParentContractIndex: 7,
} }
tx, err := responder.BuildTx(context.Background(), responseClaim) err := responder.PerformAction(context.Background(), action)
require.NoError(t, err) require.NoError(t, err)
// Pack the tx data manually. // Pack the tx data manually.
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi() fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err) require.NoError(t, err)
parent := big.NewInt(int64(7)) expected, err := fdgAbi.Pack("defend", big.NewInt(int64(action.ParentIdx)), action.Value)
claim := responseClaim.ValueBytes() require.NoError(t, err)
expected, err := fdgAbi.Pack("defend", parent, claim)
require.Len(t, mockTxMgr.sent, 1)
require.Equal(t, expected, mockTxMgr.sent[0].TxData)
})
t.Run("step", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
action := solver.Action{
Type: solver.ActionTypeStep,
ParentIdx: 123,
IsAttack: true,
PreState: []byte{1, 2, 3},
ProofData: []byte{4, 5, 6},
}
err := responder.PerformAction(context.Background(), action)
require.NoError(t, err)
// Pack the tx data manually.
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, tx) expected, err := fdgAbi.Pack("step", big.NewInt(int64(action.ParentIdx)), true, action.PreState, action.ProofData)
require.NoError(t, err)
require.Len(t, mockTxMgr.sent, 1)
require.Equal(t, expected, mockTxMgr.sent[0].TxData)
}) })
} }
func newTestFaultResponder(t *testing.T) (*faultResponder, *mockTxManager) { func newTestFaultResponder(t *testing.T) (*FaultResponder, *mockTxManager) {
log := testlog.Logger(t, log.LvlError) log := testlog.Logger(t, log.LvlError)
mockTxMgr := &mockTxManager{} mockTxMgr := &mockTxManager{}
responder, err := NewFaultResponder(log, mockTxMgr, mockFdgAddress) responder, err := NewFaultResponder(log, mockTxMgr, mockFdgAddress)
...@@ -151,6 +176,7 @@ func newTestFaultResponder(t *testing.T) (*faultResponder, *mockTxManager) { ...@@ -151,6 +176,7 @@ func newTestFaultResponder(t *testing.T) (*faultResponder, *mockTxManager) {
type mockTxManager struct { type mockTxManager struct {
from common.Address from common.Address
sends int sends int
sent []txmgr.TxCandidate
calls int calls int
sendFails bool sendFails bool
callFails bool callFails bool
...@@ -162,6 +188,7 @@ func (m *mockTxManager) Send(ctx context.Context, candidate txmgr.TxCandidate) ( ...@@ -162,6 +188,7 @@ func (m *mockTxManager) Send(ctx context.Context, candidate txmgr.TxCandidate) (
return nil, mockSendError return nil, mockSendError
} }
m.sends++ m.sends++
m.sent = append(m.sent, candidate)
return ethtypes.NewReceipt( return ethtypes.NewReceipt(
[]byte{}, []byte{},
false, false,
...@@ -189,18 +216,3 @@ func (m *mockTxManager) BlockNumber(ctx context.Context) (uint64, error) { ...@@ -189,18 +216,3 @@ func (m *mockTxManager) BlockNumber(ctx context.Context) (uint64, error) {
func (m *mockTxManager) From() common.Address { func (m *mockTxManager) From() common.Address {
return m.from return m.from
} }
func generateMockResponseClaim() types.Claim {
return types.Claim{
ClaimData: types.ClaimData{
Value: common.Hash{0x01},
Position: types.NewPositionFromGIndex(2),
},
Parent: types.ClaimData{
Value: common.Hash{0x02},
Position: types.NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 0,
}
}
package solver
import (
"context"
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum/go-ethereum/common"
)
type ActionType string
const (
ActionTypeMove ActionType = "move"
ActionTypeStep ActionType = "step"
)
func (a ActionType) String() string {
return string(a)
}
type Action struct {
Type ActionType
ParentIdx int
IsAttack bool
// Moves
Value common.Hash
// Steps
PreState []byte
ProofData []byte
OracleData *types.PreimageOracleData
}
type GameSolver struct {
claimSolver *claimSolver
gameDepth int
}
func NewGameSolver(gameDepth int, trace types.TraceProvider) *GameSolver {
return &GameSolver{
claimSolver: newClaimSolver(gameDepth, trace),
gameDepth: gameDepth,
}
}
func (s *GameSolver) CalculateNextActions(ctx context.Context, game types.Game) ([]Action, error) {
var errs []error
var actions []Action
for _, claim := range game.Claims() {
var action *Action
var err error
if claim.Depth() == s.gameDepth {
action, err = s.calculateStep(ctx, game, claim)
} else {
action, err = s.calculateMove(ctx, game, claim)
}
if err != nil {
errs = append(errs, err)
continue
}
if action == nil {
continue
}
actions = append(actions, *action)
}
return actions, errors.Join(errs...)
}
func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim types.Claim) (*Action, error) {
if claim.Countered {
return nil, nil
}
if game.AgreeWithClaimLevel(claim) {
return nil, nil
}
step, err := s.claimSolver.AttemptStep(ctx, claim, game.AgreeWithClaimLevel(claim))
if err != nil {
return nil, err
}
return &Action{
Type: ActionTypeStep,
ParentIdx: step.LeafClaim.ContractIndex,
IsAttack: step.IsAttack,
PreState: step.PreState,
ProofData: step.ProofData,
OracleData: step.OracleData,
}, nil
}
func (s *GameSolver) calculateMove(ctx context.Context, game types.Game, claim types.Claim) (*Action, error) {
move, err := s.claimSolver.NextMove(ctx, claim, game.AgreeWithClaimLevel(claim))
if err != nil {
return nil, fmt.Errorf("failed to calculate next move for claim index %v: %w", claim.ContractIndex, err)
}
if move == nil || game.IsDuplicate(*move) {
return nil, nil
}
return &Action{
Type: ActionTypeMove,
IsAttack: !move.DefendsParent(),
ParentIdx: move.ParentContractIndex,
Value: move.Value,
}, nil
}
package solver
import (
"context"
"encoding/hex"
"testing"
faulttest "github.com/ethereum-optimism/optimism/op-challenger/game/fault/test"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
type actionMaker func(game types.Game) Action
func TestCalculateNextActions(t *testing.T) {
maxDepth := 4
claimBuilder := faulttest.NewAlphabetClaimBuilder(t, maxDepth)
attackClaim := func(parentIdx int) actionMaker {
return func(game types.Game) Action {
parentClaim := game.Claims()[parentIdx]
return Action{
Type: ActionTypeMove,
ParentIdx: parentIdx,
IsAttack: true,
Value: claimBuilder.CorrectClaimAtPosition(parentClaim.Position.Attack()),
}
}
}
defendClaim := func(parentIdx int) actionMaker {
return func(game types.Game) Action {
parentClaim := game.Claims()[parentIdx]
return Action{
Type: ActionTypeMove,
ParentIdx: parentIdx,
IsAttack: false,
Value: claimBuilder.CorrectClaimAtPosition(parentClaim.Position.Defend()),
}
}
}
stepAttack := func(parentIdx int) actionMaker {
return func(game types.Game) Action {
parentClaim := game.Claims()[parentIdx]
traceIdx := parentClaim.Position.TraceIndex(maxDepth)
return Action{
Type: ActionTypeStep,
ParentIdx: parentIdx,
IsAttack: true,
PreState: claimBuilder.CorrectPreState(traceIdx),
ProofData: claimBuilder.CorrectProofData(traceIdx),
OracleData: claimBuilder.CorrectOracleData(traceIdx),
}
}
}
stepDefend := func(parentIdx int) actionMaker {
return func(game types.Game) Action {
parentClaim := game.Claims()[parentIdx]
traceIdx := parentClaim.Position.TraceIndex(maxDepth) + 1
return Action{
Type: ActionTypeStep,
ParentIdx: parentIdx,
IsAttack: false,
PreState: claimBuilder.CorrectPreState(traceIdx),
ProofData: claimBuilder.CorrectProofData(traceIdx),
OracleData: claimBuilder.CorrectOracleData(traceIdx),
}
}
}
tests := []struct {
name string
agreeWithOutputRoot bool
rootClaimCorrect bool
setupGame func(builder *faulttest.GameBuilder)
expectedActions []actionMaker
}{
{
name: "AttackRootClaim",
agreeWithOutputRoot: true,
setupGame: func(builder *faulttest.GameBuilder) {},
expectedActions: []actionMaker{
attackClaim(0),
},
},
{
name: "DoNotAttackRootClaimWhenDisagreeWithOutputRoot",
agreeWithOutputRoot: false,
setupGame: func(builder *faulttest.GameBuilder) {},
expectedActions: nil,
},
{
// Note: The fault dispute game contract should prevent a correct root claim from actually being posted
// But for completeness, test we ignore it so we don't get sucked into playing an unwinnable game.
name: "DoNotAttackCorrectRootClaim_AgreeWithOutputRoot",
agreeWithOutputRoot: true,
rootClaimCorrect: true,
setupGame: func(builder *faulttest.GameBuilder) {},
expectedActions: nil,
},
{
// Note: The fault dispute game contract should prevent a correct root claim from actually being posted
// But for completeness, test we ignore it so we don't get sucked into playing an unwinnable game.
name: "DoNotAttackCorrectRootClaim_DisagreeWithOutputRoot",
agreeWithOutputRoot: false,
rootClaimCorrect: true,
setupGame: func(builder *faulttest.GameBuilder) {},
expectedActions: nil,
},
{
name: "DoNotPerformDuplicateMoves",
agreeWithOutputRoot: true,
setupGame: func(builder *faulttest.GameBuilder) {
// Expected move has already been made.
builder.Seq().AttackCorrect()
},
expectedActions: nil,
},
{
name: "RespondToAllClaimsAtDisagreeingLevel",
agreeWithOutputRoot: true,
setupGame: func(builder *faulttest.GameBuilder) {
honestClaim := builder.Seq().AttackCorrect() // 1
honestClaim.AttackCorrect() // 2
honestClaim.DefendCorrect() // 3
honestClaim.Attack(common.Hash{0xaa}) // 4
honestClaim.Attack(common.Hash{0xbb}) // 5
honestClaim.Defend(common.Hash{0xcc}) // 6
honestClaim.Defend(common.Hash{0xdd}) // 7
},
expectedActions: []actionMaker{
// Defend the correct claims
defendClaim(2),
defendClaim(3),
// Attack the incorrect claims
attackClaim(4),
attackClaim(5),
attackClaim(6),
attackClaim(7),
},
},
{
name: "StepAtMaxDepth",
agreeWithOutputRoot: true,
setupGame: func(builder *faulttest.GameBuilder) {
lastHonestClaim := builder.Seq().
AttackCorrect(). // 1 - Honest
AttackCorrect(). // 2 - Dishonest
DefendCorrect() // 3 - Honest
lastHonestClaim.AttackCorrect() // 4 - Dishonest
lastHonestClaim.Attack(common.Hash{0xdd}) // 5 - Dishonest
},
expectedActions: []actionMaker{
stepDefend(4),
stepAttack(5),
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
builder := claimBuilder.GameBuilder(test.agreeWithOutputRoot, test.rootClaimCorrect)
test.setupGame(builder)
game := builder.Game
for i, claim := range game.Claims() {
t.Logf("Claim %v: Pos: %v ParentIdx: %v, Countered: %v, Value: %v", i, claim.Position.ToGIndex(), claim.ParentContractIndex, claim.Countered, claim.Value)
}
solver := NewGameSolver(maxDepth, claimBuilder.CorrectTraceProvider())
actions, err := solver.CalculateNextActions(context.Background(), game)
require.NoError(t, err)
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.ParentIdx, action.IsAttack, action.Value, hex.EncodeToString(action.PreState), hex.EncodeToString(action.ProofData))
}
require.Len(t, actions, len(test.expectedActions))
for i, action := range test.expectedActions {
require.Containsf(t, actions, action(game), "Expected claim %v missing", i)
}
})
}
}
...@@ -15,22 +15,22 @@ var ( ...@@ -15,22 +15,22 @@ var (
ErrStepAgreedClaim = errors.New("cannot step on claims we agree with") ErrStepAgreedClaim = errors.New("cannot step on claims we agree with")
) )
// Solver uses a [TraceProvider] to determine the moves to make in a dispute game. // claimSolver uses a [TraceProvider] to determine the moves to make in a dispute game.
type Solver struct { type claimSolver struct {
trace types.TraceProvider trace types.TraceProvider
gameDepth int gameDepth int
} }
// NewSolver creates a new [Solver] using the provided [TraceProvider]. // newClaimSolver creates a new [claimSolver] using the provided [TraceProvider].
func NewSolver(gameDepth int, traceProvider types.TraceProvider) *Solver { func newClaimSolver(gameDepth int, traceProvider types.TraceProvider) *claimSolver {
return &Solver{ return &claimSolver{
traceProvider, traceProvider,
gameDepth, gameDepth,
} }
} }
// NextMove returns the next move to make given the current state of the game. // NextMove returns the next move to make given the current state of the game.
func (s *Solver) NextMove(ctx context.Context, claim types.Claim, agreeWithClaimLevel bool) (*types.Claim, error) { func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, agreeWithClaimLevel bool) (*types.Claim, error) {
if agreeWithClaimLevel { if agreeWithClaimLevel {
return nil, nil return nil, nil
} }
...@@ -58,7 +58,7 @@ type StepData struct { ...@@ -58,7 +58,7 @@ type StepData struct {
// AttemptStep determines what step should occur for a given leaf claim. // AttemptStep determines what step should occur for a given leaf claim.
// An error will be returned if the claim is not at the max depth. // An error will be returned if the claim is not at the max depth.
func (s *Solver) AttemptStep(ctx context.Context, claim types.Claim, agreeWithClaimLevel bool) (StepData, error) { func (s *claimSolver) AttemptStep(ctx context.Context, claim types.Claim, agreeWithClaimLevel bool) (StepData, error) {
if claim.Depth() != s.gameDepth { if claim.Depth() != s.gameDepth {
return StepData{}, ErrStepNonLeafNode return StepData{}, ErrStepNonLeafNode
} }
...@@ -100,7 +100,7 @@ func (s *Solver) AttemptStep(ctx context.Context, claim types.Claim, agreeWithCl ...@@ -100,7 +100,7 @@ func (s *Solver) AttemptStep(ctx context.Context, claim types.Claim, agreeWithCl
} }
// attack returns a response that attacks the claim. // attack returns a response that attacks the claim.
func (s *Solver) attack(ctx context.Context, claim types.Claim) (*types.Claim, error) { func (s *claimSolver) attack(ctx context.Context, claim types.Claim) (*types.Claim, error) {
position := claim.Attack() position := claim.Attack()
value, err := s.traceAtPosition(ctx, position) value, err := s.traceAtPosition(ctx, position)
if err != nil { if err != nil {
...@@ -114,7 +114,7 @@ func (s *Solver) attack(ctx context.Context, claim types.Claim) (*types.Claim, e ...@@ -114,7 +114,7 @@ func (s *Solver) attack(ctx context.Context, claim types.Claim) (*types.Claim, e
} }
// defend returns a response that defends the claim. // defend returns a response that defends the claim.
func (s *Solver) defend(ctx context.Context, claim types.Claim) (*types.Claim, error) { func (s *claimSolver) defend(ctx context.Context, claim types.Claim) (*types.Claim, error) {
if claim.IsRoot() { if claim.IsRoot() {
return nil, nil return nil, nil
} }
...@@ -131,13 +131,13 @@ func (s *Solver) defend(ctx context.Context, claim types.Claim) (*types.Claim, e ...@@ -131,13 +131,13 @@ func (s *Solver) defend(ctx context.Context, claim types.Claim) (*types.Claim, e
} }
// agreeWithClaim returns true if the claim is correct according to the internal [TraceProvider]. // agreeWithClaim returns true if the claim is correct according to the internal [TraceProvider].
func (s *Solver) agreeWithClaim(ctx context.Context, claim types.ClaimData) (bool, error) { func (s *claimSolver) agreeWithClaim(ctx context.Context, claim types.ClaimData) (bool, error) {
ourValue, err := s.traceAtPosition(ctx, claim.Position) ourValue, err := s.traceAtPosition(ctx, claim.Position)
return bytes.Equal(ourValue[:], claim.Value[:]), err return bytes.Equal(ourValue[:], claim.Value[:]), err
} }
// traceAtPosition returns the [common.Hash] from internal [TraceProvider] at the given [Position]. // traceAtPosition returns the [common.Hash] from internal [TraceProvider] at the given [Position].
func (s *Solver) traceAtPosition(ctx context.Context, p types.Position) (common.Hash, error) { func (s *claimSolver) traceAtPosition(ctx context.Context, p types.Position) (common.Hash, error) {
index := p.TraceIndex(s.gameDepth) index := p.TraceIndex(s.gameDepth)
hash, err := s.trace.Get(ctx, index) hash, err := s.trace.Get(ctx, index)
return hash, err return hash, err
......
package solver_test package solver
import ( import (
"context" "context"
"errors" "errors"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/test" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/test"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -84,7 +83,7 @@ func TestNextMove(t *testing.T) { ...@@ -84,7 +83,7 @@ func TestNextMove(t *testing.T) {
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
solver := solver.NewSolver(maxDepth, builder.CorrectTraceProvider()) solver := newClaimSolver(maxDepth, builder.CorrectTraceProvider())
move, err := solver.NextMove(context.Background(), test.claim, test.agreeWithLevel) move, err := solver.NextMove(context.Background(), test.claim, test.agreeWithLevel)
if test.expectedErr == nil { if test.expectedErr == nil {
require.NoError(t, err) require.NoError(t, err)
...@@ -174,19 +173,19 @@ func TestAttemptStep(t *testing.T) { ...@@ -174,19 +173,19 @@ func TestAttemptStep(t *testing.T) {
{ {
name: "CannotStepNonLeaf", name: "CannotStepNonLeaf",
claim: builder.Seq(false).Attack(false).Get(), claim: builder.Seq(false).Attack(false).Get(),
expectedErr: solver.ErrStepNonLeafNode, expectedErr: ErrStepNonLeafNode,
}, },
{ {
name: "CannotStepAgreedNode", name: "CannotStepAgreedNode",
claim: builder.Seq(false).Attack(false).Get(), claim: builder.Seq(false).Attack(false).Get(),
agreeWithLevel: true, agreeWithLevel: true,
expectedErr: solver.ErrStepNonLeafNode, expectedErr: ErrStepNonLeafNode,
}, },
{ {
name: "CannotStepAgreedNode", name: "CannotStepAgreedNode",
claim: builder.Seq(false).Attack(false).Get(), claim: builder.Seq(false).Attack(false).Get(),
agreeWithLevel: true, agreeWithLevel: true,
expectedErr: solver.ErrStepNonLeafNode, expectedErr: ErrStepNonLeafNode,
}, },
} }
...@@ -198,7 +197,7 @@ func TestAttemptStep(t *testing.T) { ...@@ -198,7 +197,7 @@ func TestAttemptStep(t *testing.T) {
alphabetProvider = test.NewAlphabetWithProofProvider(t, maxDepth, errProvider) alphabetProvider = test.NewAlphabetWithProofProvider(t, maxDepth, errProvider)
} }
builder = test.NewClaimBuilder(t, maxDepth, alphabetProvider) builder = test.NewClaimBuilder(t, maxDepth, alphabetProvider)
alphabetSolver := solver.NewSolver(maxDepth, builder.CorrectTraceProvider()) alphabetSolver := newClaimSolver(maxDepth, builder.CorrectTraceProvider())
step, err := alphabetSolver.AttemptStep(ctx, tableTest.claim, tableTest.agreeWithLevel) step, err := alphabetSolver.AttemptStep(ctx, tableTest.claim, tableTest.agreeWithLevel)
if tableTest.expectedErr == nil { if tableTest.expectedErr == nil {
require.NoError(t, err) require.NoError(t, err)
...@@ -212,7 +211,7 @@ func TestAttemptStep(t *testing.T) { ...@@ -212,7 +211,7 @@ func TestAttemptStep(t *testing.T) {
require.Equal(t, tableTest.expectedOracleData.OracleOffset, step.OracleData.OracleOffset) require.Equal(t, tableTest.expectedOracleData.OracleOffset, step.OracleData.OracleOffset)
} else { } else {
require.ErrorIs(t, err, tableTest.expectedErr) require.ErrorIs(t, err, tableTest.expectedErr)
require.Equal(t, solver.StepData{}, step) require.Equal(t, StepData{}, step)
} }
}) })
} }
......
...@@ -38,6 +38,13 @@ func (c *ClaimBuilder) CorrectClaim(idx uint64) common.Hash { ...@@ -38,6 +38,13 @@ func (c *ClaimBuilder) CorrectClaim(idx uint64) common.Hash {
return value return value
} }
// CorrectClaimAtPosition returns the canonical claim at a specified position
func (c *ClaimBuilder) CorrectClaimAtPosition(pos types.Position) common.Hash {
value, err := c.correct.Get(context.Background(), pos.TraceIndex(c.maxDepth))
c.require.NoError(err)
return value
}
// CorrectPreState returns the pre-state (not hashed) required to execute the valid step at the specified trace index // CorrectPreState returns the pre-state (not hashed) required to execute the valid step at the specified trace index
func (c *ClaimBuilder) CorrectPreState(idx uint64) []byte { func (c *ClaimBuilder) CorrectPreState(idx uint64) []byte {
preimage, _, _, err := c.correct.GetStepData(context.Background(), idx) preimage, _, _, err := c.correct.GetStepData(context.Background(), idx)
...@@ -102,7 +109,20 @@ func (c *ClaimBuilder) AttackClaim(claim types.Claim, correct bool) types.Claim ...@@ -102,7 +109,20 @@ func (c *ClaimBuilder) AttackClaim(claim types.Claim, correct bool) types.Claim
Value: c.claim(pos.TraceIndex(c.maxDepth), correct), Value: c.claim(pos.TraceIndex(c.maxDepth), correct),
Position: pos, Position: pos,
}, },
Parent: claim.ClaimData, Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
}
}
func (c *ClaimBuilder) AttackClaimWithValue(claim types.Claim, value common.Hash) types.Claim {
pos := claim.Position.Attack()
return types.Claim{
ClaimData: types.ClaimData{
Value: value,
Position: pos,
},
Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
} }
} }
...@@ -113,6 +133,19 @@ func (c *ClaimBuilder) DefendClaim(claim types.Claim, correct bool) types.Claim ...@@ -113,6 +133,19 @@ func (c *ClaimBuilder) DefendClaim(claim types.Claim, correct bool) types.Claim
Value: c.claim(pos.TraceIndex(c.maxDepth), correct), Value: c.claim(pos.TraceIndex(c.maxDepth), correct),
Position: pos, Position: pos,
}, },
Parent: claim.ClaimData, Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
}
}
func (c *ClaimBuilder) DefendClaimWithValue(claim types.Claim, value common.Hash) types.Claim {
pos := claim.Position.Defend()
return types.Claim{
ClaimData: types.ClaimData{
Value: value,
Position: pos,
},
Parent: claim.ClaimData,
ParentContractIndex: claim.ContractIndex,
} }
} }
package test
import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum/go-ethereum/common"
)
type GameBuilder struct {
builder *ClaimBuilder
Game types.Game
}
func (c *ClaimBuilder) GameBuilder(agreeWithOutputRoot bool, rootCorrect bool) *GameBuilder {
return &GameBuilder{
builder: c,
Game: types.NewGameState(agreeWithOutputRoot, c.CreateRootClaim(rootCorrect), uint64(c.maxDepth)),
}
}
type GameBuilderSeq struct {
builder *ClaimBuilder
lastClaim types.Claim
game types.Game
}
func (g *GameBuilder) Seq() *GameBuilderSeq {
return &GameBuilderSeq{
builder: g.builder,
game: g.Game,
lastClaim: g.Game.Claims()[0],
}
}
func (s *GameBuilderSeq) AttackCorrect() *GameBuilderSeq {
claim := s.builder.AttackClaim(s.lastClaim, true)
claim.ContractIndex = len(s.game.Claims())
s.builder.require.NoError(s.game.Put(claim))
return &GameBuilderSeq{
builder: s.builder,
game: s.game,
lastClaim: claim,
}
}
func (s *GameBuilderSeq) Attack(value common.Hash) *GameBuilderSeq {
claim := s.builder.AttackClaimWithValue(s.lastClaim, value)
claim.ContractIndex = len(s.game.Claims())
s.builder.require.NoError(s.game.Put(claim))
return &GameBuilderSeq{
builder: s.builder,
game: s.game,
lastClaim: claim,
}
}
func (s *GameBuilderSeq) DefendCorrect() *GameBuilderSeq {
claim := s.builder.DefendClaim(s.lastClaim, true)
claim.ContractIndex = len(s.game.Claims())
s.builder.require.NoError(s.game.Put(claim))
return &GameBuilderSeq{
builder: s.builder,
game: s.game,
lastClaim: claim,
}
}
func (s *GameBuilderSeq) Defend(value common.Hash) *GameBuilderSeq {
claim := s.builder.DefendClaimWithValue(s.lastClaim, value)
claim.ContractIndex = len(s.game.Claims())
s.builder.require.NoError(s.game.Put(claim))
return &GameBuilderSeq{
builder: s.builder,
game: s.game,
lastClaim: claim,
}
}
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