Commit 971815e2 authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into aj/fpa-docs

parents 574c8c0b 64d1c30c
......@@ -17,8 +17,7 @@ import (
type Responder interface {
CallResolve(ctx context.Context) (gameTypes.GameStatus, error)
Resolve(ctx context.Context) error
Respond(ctx context.Context, response types.Claim) error
Step(ctx context.Context, stepData types.StepCallData) error
PerformAction(ctx context.Context, action solver.Action) error
}
type ClaimLoader interface {
......@@ -27,7 +26,7 @@ type ClaimLoader interface {
type Agent struct {
metrics metrics.Metricer
solver *solver.Solver
solver *solver.GameSolver
loader ClaimLoader
responder Responder
updater types.OracleUpdater
......@@ -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 {
return &Agent{
metrics: m,
solver: solver.NewSolver(maxDepth, trace),
solver: solver.NewGameSolver(maxDepth, trace),
loader: loader,
responder: responder,
updater: updater,
......@@ -58,16 +57,34 @@ func (a *Agent) Act(ctx context.Context) error {
if err != nil {
return fmt.Errorf("create game from contracts: %w", err)
}
// Create counter claims
for _, claim := range game.Claims() {
if err := a.move(ctx, claim, game); err != nil && !errors.Is(err, types.ErrGameDepthReached) {
log.Error("Failed to move", "err", err)
}
// Calculate the actions to take
actions, err := a.solver.CalculateNextActions(ctx, game)
if err != nil {
log.Error("Failed to calculate all required moves", "err", err)
}
// Step on all leaf claims
for _, claim := range game.Claims() {
if err := a.step(ctx, claim, game); err != nil {
log.Error("Failed to step", "err", err)
// Perform the actions
for _, action := range actions {
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
......@@ -118,68 +135,3 @@ func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
}
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 (
"errors"
"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/trace/alphabet"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
......@@ -144,11 +145,7 @@ func (s *stubResponder) Resolve(ctx context.Context) error {
return s.resolveErr
}
func (s *stubResponder) Respond(ctx context.Context, response types.Claim) error {
panic("Not implemented")
}
func (s *stubResponder) Step(ctx context.Context, stepData types.StepCallData) error {
func (s *stubResponder) PerformAction(ctx context.Context, response solver.Action) error {
panic("Not implemented")
}
......
......@@ -5,7 +5,6 @@ import (
"testing"
"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/backends"
"github.com/ethereum/go-ethereum/common"
......@@ -94,12 +93,7 @@ func TestBuildFaultStepData(t *testing.T) {
resp, _ := newTestFaultResponder(t)
data, err := resp.buildStepTxData(types.StepCallData{
ClaimIndex: 2,
IsAttack: false,
StateData: []byte{0x01},
Proof: []byte{0x02},
})
data, err := resp.buildStepTxData(2, false, []byte{0x01}, []byte{0x02})
require.NoError(t, err)
opts.GasLimit = 100_000
......
......@@ -5,7 +5,7 @@ import (
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
......@@ -16,8 +16,8 @@ import (
"github.com/ethereum/go-ethereum/log"
)
// faultResponder implements the [Responder] interface to send onchain transactions.
type faultResponder struct {
// FaultResponder implements the [Responder] interface to send onchain transactions.
type FaultResponder struct {
log log.Logger
txMgr txmgr.TxManager
......@@ -26,13 +26,13 @@ type faultResponder struct {
fdgAbi *abi.ABI
}
// NewFaultResponder returns a new [faultResponder].
func NewFaultResponder(logger log.Logger, txManagr txmgr.TxManager, fdgAddr common.Address) (*faultResponder, error) {
// NewFaultResponder returns a new [FaultResponder].
func NewFaultResponder(logger log.Logger, txManagr txmgr.TxManager, fdgAddr common.Address) (*FaultResponder, error) {
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
if err != nil {
return nil, err
}
return &faultResponder{
return &FaultResponder{
log: logger,
txMgr: txManagr,
fdgAddr: fdgAddr,
......@@ -41,7 +41,7 @@ func NewFaultResponder(logger log.Logger, txManagr txmgr.TxManager, fdgAddr comm
}
// 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(
"defend",
big.NewInt(int64(parentContractIndex)),
......@@ -50,7 +50,7 @@ func (r *faultResponder) buildFaultDefendData(parentContractIndex int, pivot [32
}
// 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(
"attack",
big.NewInt(int64(parentContractIndex)),
......@@ -59,30 +59,13 @@ func (r *faultResponder) buildFaultAttackData(parentContractIndex int, pivot [32
}
// 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")
}
// 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
// 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()
if err != nil {
return gameTypes.GameStatusInProgress, err
......@@ -102,7 +85,7 @@ func (r *faultResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus,
}
// 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()
if err != nil {
return err
......@@ -111,9 +94,19 @@ func (r *faultResponder) Resolve(ctx context.Context) error {
return r.sendTxAndWait(ctx, txData)
}
// Respond takes a [Claim] and executes the response action.
func (r *faultResponder) Respond(ctx context.Context, response types.Claim) error {
txData, err := r.BuildTx(ctx, response)
func (r *FaultResponder) PerformAction(ctx context.Context, action solver.Action) error {
var txData []byte
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 {
return err
}
......@@ -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.
// 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{
To: &r.fdgAddr,
TxData: txData,
......@@ -140,21 +133,12 @@ func (r *faultResponder) sendTxAndWait(ctx context.Context, txData []byte) error
}
// 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(
"step",
big.NewInt(int64(stepData.ClaimIndex)),
stepData.IsAttack,
stepData.StateData,
stepData.Proof,
big.NewInt(int64(claimIdx)),
isAttack,
stateData,
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 (
"testing"
"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"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
......@@ -74,73 +74,98 @@ func TestResolve(t *testing.T) {
}
// TestRespond tests the [Responder.Respond] method.
func TestRespond(t *testing.T) {
func TestPerformAction(t *testing.T) {
t.Run("send fails", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
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.Equal(t, 0, mockTxMgr.sends)
})
t.Run("sends response", func(t *testing.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.Equal(t, 1, mockTxMgr.sends)
})
}
// TestBuildTx tests the [Responder.BuildTx] method.
func TestBuildTx(t *testing.T) {
t.Run("attack", func(t *testing.T) {
responder, _ := newTestFaultResponder(t)
responseClaim := generateMockResponseClaim()
responseClaim.ParentContractIndex = 7
tx, err := responder.BuildTx(context.Background(), responseClaim)
responder, mockTxMgr := newTestFaultResponder(t)
action := solver.Action{
Type: solver.ActionTypeMove,
ParentIdx: 123,
IsAttack: true,
Value: common.Hash{0xaa},
}
err := responder.PerformAction(context.Background(), action)
require.NoError(t, err)
// Pack the tx data manually.
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err)
parent := big.NewInt(int64(7))
claim := responseClaim.ValueBytes()
expected, err := fdgAbi.Pack("attack", parent, claim)
expected, err := fdgAbi.Pack("attack", big.NewInt(int64(action.ParentIdx)), action.Value)
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) {
responder, _ := newTestFaultResponder(t)
responseClaim := types.Claim{
ClaimData: types.ClaimData{
Value: common.Hash{0x01},
Position: types.NewPositionFromGIndex(3),
},
Parent: types.ClaimData{
Value: common.Hash{0x02},
Position: types.NewPositionFromGIndex(6),
},
ContractIndex: 0,
ParentContractIndex: 7,
responder, mockTxMgr := newTestFaultResponder(t)
action := solver.Action{
Type: solver.ActionTypeMove,
ParentIdx: 123,
IsAttack: false,
Value: common.Hash{0xaa},
}
tx, err := responder.BuildTx(context.Background(), responseClaim)
err := responder.PerformAction(context.Background(), action)
require.NoError(t, err)
// Pack the tx data manually.
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err)
parent := big.NewInt(int64(7))
claim := responseClaim.ValueBytes()
expected, err := fdgAbi.Pack("defend", parent, claim)
expected, err := fdgAbi.Pack("defend", big.NewInt(int64(action.ParentIdx)), action.Value)
require.NoError(t, err)
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.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)
mockTxMgr := &mockTxManager{}
responder, err := NewFaultResponder(log, mockTxMgr, mockFdgAddress)
......@@ -151,6 +176,7 @@ func newTestFaultResponder(t *testing.T) (*faultResponder, *mockTxManager) {
type mockTxManager struct {
from common.Address
sends int
sent []txmgr.TxCandidate
calls int
sendFails bool
callFails bool
......@@ -162,6 +188,7 @@ func (m *mockTxManager) Send(ctx context.Context, candidate txmgr.TxCandidate) (
return nil, mockSendError
}
m.sends++
m.sent = append(m.sent, candidate)
return ethtypes.NewReceipt(
[]byte{},
false,
......@@ -189,18 +216,3 @@ func (m *mockTxManager) BlockNumber(ctx context.Context) (uint64, error) {
func (m *mockTxManager) From() common.Address {
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 (
ErrStepAgreedClaim = errors.New("cannot step on claims we agree with")
)
// Solver uses a [TraceProvider] to determine the moves to make in a dispute game.
type Solver struct {
// claimSolver uses a [TraceProvider] to determine the moves to make in a dispute game.
type claimSolver struct {
trace types.TraceProvider
gameDepth int
}
// NewSolver creates a new [Solver] using the provided [TraceProvider].
func NewSolver(gameDepth int, traceProvider types.TraceProvider) *Solver {
return &Solver{
// newClaimSolver creates a new [claimSolver] using the provided [TraceProvider].
func newClaimSolver(gameDepth int, traceProvider types.TraceProvider) *claimSolver {
return &claimSolver{
traceProvider,
gameDepth,
}
}
// 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 {
return nil, nil
}
......@@ -58,7 +58,7 @@ type StepData struct {
// 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.
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 {
return StepData{}, ErrStepNonLeafNode
}
......@@ -100,7 +100,7 @@ func (s *Solver) AttemptStep(ctx context.Context, claim types.Claim, agreeWithCl
}
// 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()
value, err := s.traceAtPosition(ctx, position)
if err != nil {
......@@ -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.
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() {
return nil, nil
}
......@@ -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].
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)
return bytes.Equal(ourValue[:], claim.Value[:]), err
}
// 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)
hash, err := s.trace.Get(ctx, index)
return hash, err
......
package solver_test
package solver
import (
"context"
"errors"
"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/types"
"github.com/stretchr/testify/require"
......@@ -84,7 +83,7 @@ func TestNextMove(t *testing.T) {
for _, test := range tests {
test := test
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)
if test.expectedErr == nil {
require.NoError(t, err)
......@@ -174,19 +173,19 @@ func TestAttemptStep(t *testing.T) {
{
name: "CannotStepNonLeaf",
claim: builder.Seq(false).Attack(false).Get(),
expectedErr: solver.ErrStepNonLeafNode,
expectedErr: ErrStepNonLeafNode,
},
{
name: "CannotStepAgreedNode",
claim: builder.Seq(false).Attack(false).Get(),
agreeWithLevel: true,
expectedErr: solver.ErrStepNonLeafNode,
expectedErr: ErrStepNonLeafNode,
},
{
name: "CannotStepAgreedNode",
claim: builder.Seq(false).Attack(false).Get(),
agreeWithLevel: true,
expectedErr: solver.ErrStepNonLeafNode,
expectedErr: ErrStepNonLeafNode,
},
}
......@@ -198,7 +197,7 @@ func TestAttemptStep(t *testing.T) {
alphabetProvider = test.NewAlphabetWithProofProvider(t, maxDepth, errProvider)
}
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)
if tableTest.expectedErr == nil {
require.NoError(t, err)
......@@ -212,7 +211,7 @@ func TestAttemptStep(t *testing.T) {
require.Equal(t, tableTest.expectedOracleData.OracleOffset, step.OracleData.OracleOffset)
} else {
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 {
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
func (c *ClaimBuilder) CorrectPreState(idx uint64) []byte {
preimage, _, _, err := c.correct.GetStepData(context.Background(), idx)
......@@ -102,7 +109,20 @@ func (c *ClaimBuilder) AttackClaim(claim types.Claim, correct bool) types.Claim
Value: c.claim(pos.TraceIndex(c.maxDepth), correct),
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
Value: c.claim(pos.TraceIndex(c.maxDepth), correct),
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,
}
}
......@@ -62,5 +62,7 @@ done
# Root claim commits to the entire trace.
# Alphabet game claim construction: keccak256(abi.encode(trace_index, trace[trace_index]))
ROOT_CLAIM=$(cast keccak $(cast abi-encode "f(uint256,uint256)" 15 122))
# Replace the first byte of the claim with the invalid vm status indicator
ROOT_CLAIM="0x01${ROOT_CLAIM:4:60}"
GAME_TYPE=255 ${SOURCE_DIR}/../create_game.sh http://localhost:8545 "${DISPUTE_GAME_FACTORY}" "${ROOT_CLAIM}" --private-key "${DEVNET_SPONSOR}"
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