Commit 23d0635b authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #7080 from ethereum-optimism/refcell/game-gauge

feat(op-challenger): Game GaugeVec
parents 1d861eb5 07cf16db
......@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum/go-ethereum/log"
)
......@@ -14,7 +15,7 @@ import (
// Responder takes a response action & executes.
// For full op-challenger this means executing the transaction on chain.
type Responder interface {
CallResolve(ctx context.Context) (types.GameStatus, error)
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
......@@ -74,10 +75,10 @@ func (a *Agent) Act(ctx context.Context) error {
// shouldResolve returns true if the agent should resolve the game.
// This method will return false if the game is still in progress.
func (a *Agent) shouldResolve(status types.GameStatus) bool {
expected := types.GameStatusDefenderWon
func (a *Agent) shouldResolve(status gameTypes.GameStatus) bool {
expected := gameTypes.GameStatusDefenderWon
if a.agreeWithProposedOutput {
expected = types.GameStatusChallengerWon
expected = gameTypes.GameStatusChallengerWon
}
if expected != status {
a.log.Warn("Game will be lost", "expected", expected, "actual", status)
......@@ -89,7 +90,7 @@ func (a *Agent) shouldResolve(status types.GameStatus) bool {
// Returns true if the game is resolvable (regardless of whether it was actually resolved)
func (a *Agent) tryResolve(ctx context.Context) bool {
status, err := a.responder.CallResolve(ctx)
if err != nil || status == types.GameStatusInProgress {
if err != nil || status == gameTypes.GameStatusInProgress {
return false
}
if !a.shouldResolve(status) {
......
......@@ -8,6 +8,7 @@ import (
"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"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
......@@ -19,16 +20,16 @@ import (
func TestShouldResolve(t *testing.T) {
t.Run("AgreeWithProposedOutput", func(t *testing.T) {
agent, _, _ := setupTestAgent(t, true)
require.False(t, agent.shouldResolve(types.GameStatusDefenderWon))
require.True(t, agent.shouldResolve(types.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(types.GameStatusInProgress))
require.False(t, agent.shouldResolve(gameTypes.GameStatusDefenderWon))
require.True(t, agent.shouldResolve(gameTypes.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(gameTypes.GameStatusInProgress))
})
t.Run("DisagreeWithProposedOutput", func(t *testing.T) {
agent, _, _ := setupTestAgent(t, false)
require.True(t, agent.shouldResolve(types.GameStatusDefenderWon))
require.False(t, agent.shouldResolve(types.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(types.GameStatusInProgress))
require.True(t, agent.shouldResolve(gameTypes.GameStatusDefenderWon))
require.False(t, agent.shouldResolve(gameTypes.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(gameTypes.GameStatusInProgress))
})
}
......@@ -38,31 +39,31 @@ func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) {
tests := []struct {
name string
agreeWithProposedOutput bool
callResolveStatus types.GameStatus
callResolveStatus gameTypes.GameStatus
shouldResolve bool
}{
{
name: "Agree_Losing",
agreeWithProposedOutput: true,
callResolveStatus: types.GameStatusDefenderWon,
callResolveStatus: gameTypes.GameStatusDefenderWon,
shouldResolve: false,
},
{
name: "Agree_Winning",
agreeWithProposedOutput: true,
callResolveStatus: types.GameStatusChallengerWon,
callResolveStatus: gameTypes.GameStatusChallengerWon,
shouldResolve: true,
},
{
name: "Disagree_Losing",
agreeWithProposedOutput: false,
callResolveStatus: types.GameStatusChallengerWon,
callResolveStatus: gameTypes.GameStatusChallengerWon,
shouldResolve: false,
},
{
name: "Disagree_Winning",
agreeWithProposedOutput: false,
callResolveStatus: types.GameStatusDefenderWon,
callResolveStatus: gameTypes.GameStatusDefenderWon,
shouldResolve: true,
},
}
......@@ -126,14 +127,14 @@ func (s *stubClaimLoader) FetchClaims(ctx context.Context) ([]types.Claim, error
type stubResponder struct {
callResolveCount int
callResolveStatus types.GameStatus
callResolveStatus gameTypes.GameStatus
callResolveErr error
resolveCount int
resolveErr error
}
func (s *stubResponder) CallResolve(ctx context.Context) (types.GameStatus, error) {
func (s *stubResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
s.callResolveCount++
return s.callResolveStatus, s.callResolveErr
}
......
......@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
......@@ -49,9 +50,9 @@ func NewLoaderFromBindings(fdgAddr common.Address, client bind.ContractCaller) (
}
// GetGameStatus returns the current game status.
func (l *loader) GetGameStatus(ctx context.Context) (types.GameStatus, error) {
func (l *loader) GetGameStatus(ctx context.Context) (gameTypes.GameStatus, error) {
status, err := l.caller.Status(&bind.CallOpts{Context: ctx})
return types.GameStatus(status), err
return gameTypes.GameStatus(status), err
}
// GetClaimCount returns the number of claims in the game.
......
......@@ -7,6 +7,7 @@ import (
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
......@@ -30,15 +31,15 @@ func TestLoader_GetGameStatus(t *testing.T) {
}{
{
name: "challenger won status",
status: uint8(types.GameStatusChallengerWon),
status: uint8(gameTypes.GameStatusChallengerWon),
},
{
name: "defender won status",
status: uint8(types.GameStatusDefenderWon),
status: uint8(gameTypes.GameStatusDefenderWon),
},
{
name: "in progress status",
status: uint8(types.GameStatusInProgress),
status: uint8(gameTypes.GameStatusInProgress),
},
{
name: "error bubbled up",
......@@ -57,7 +58,7 @@ func TestLoader_GetGameStatus(t *testing.T) {
require.ErrorIs(t, err, mockStatusError)
} else {
require.NoError(t, err)
require.Equal(t, types.GameStatus(test.status), status)
require.Equal(t, gameTypes.GameStatus(test.status), status)
}
})
}
......
......@@ -11,6 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
......@@ -22,7 +23,7 @@ import (
type actor func(ctx context.Context) error
type GameInfo interface {
GetGameStatus(context.Context) (types.GameStatus, error)
GetGameStatus(context.Context) (gameTypes.GameStatus, error)
GetClaimCount(context.Context) (uint64, error)
}
......@@ -31,8 +32,7 @@ type GamePlayer struct {
agreeWithProposedOutput bool
loader GameInfo
logger log.Logger
completed bool
status gameTypes.GameStatus
}
func NewGamePlayer(
......@@ -57,14 +57,14 @@ func NewGamePlayer(
if err != nil {
return nil, fmt.Errorf("failed to fetch game status: %w", err)
}
if status != types.GameStatusInProgress {
if status != gameTypes.GameStatusInProgress {
logger.Info("Game already resolved", "status", status)
// Game is already complete so skip creating the trace provider, loading game inputs etc.
return &GamePlayer{
logger: logger,
loader: loader,
agreeWithProposedOutput: cfg.AgreeWithProposedOutput,
completed: true,
status: status,
// Act function does nothing because the game is already complete
act: func(ctx context.Context) error {
return nil
......@@ -111,32 +111,32 @@ func NewGamePlayer(
agreeWithProposedOutput: cfg.AgreeWithProposedOutput,
loader: loader,
logger: logger,
completed: status != types.GameStatusInProgress,
status: status,
}, nil
}
func (g *GamePlayer) ProgressGame(ctx context.Context) bool {
if g.completed {
func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus {
if g.status != gameTypes.GameStatusInProgress {
// Game is already complete so don't try to perform further actions.
g.logger.Trace("Skipping completed game")
return true
return g.status
}
g.logger.Trace("Checking if actions are required")
if err := g.act(ctx); err != nil {
g.logger.Error("Error when acting on game", "err", err)
}
if status, err := g.loader.GetGameStatus(ctx); err != nil {
status, err := g.loader.GetGameStatus(ctx)
if err != nil {
g.logger.Warn("Unable to retrieve game status", "err", err)
} else {
g.logGameStatus(ctx, status)
g.completed = status != types.GameStatusInProgress
return g.completed
return gameTypes.GameStatusInProgress
}
return false
g.logGameStatus(ctx, status)
g.status = status
return status
}
func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus) {
if status == types.GameStatusInProgress {
func (g *GamePlayer) logGameStatus(ctx context.Context, status gameTypes.GameStatus) {
if status == gameTypes.GameStatusInProgress {
claimCount, err := g.loader.GetClaimCount(ctx)
if err != nil {
g.logger.Error("Failed to get claim count for in progress game", "err", err)
......@@ -145,11 +145,11 @@ func (g *GamePlayer) logGameStatus(ctx context.Context, status types.GameStatus)
g.logger.Info("Game info", "claims", claimCount, "status", status)
return
}
var expectedStatus types.GameStatus
var expectedStatus gameTypes.GameStatus
if g.agreeWithProposedOutput {
expectedStatus = types.GameStatusChallengerWon
expectedStatus = gameTypes.GameStatusChallengerWon
} else {
expectedStatus = types.GameStatusDefenderWon
expectedStatus = gameTypes.GameStatusDefenderWon
}
if expectedStatus == status {
g.logger.Info("Game won", "status", status)
......
......@@ -7,6 +7,7 @@ import (
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
......@@ -22,8 +23,8 @@ var (
func TestProgressGame_LogErrorFromAct(t *testing.T) {
handler, game, actor := setupProgressGameTest(t, true)
actor.actErr = errors.New("boom")
done := game.ProgressGame(context.Background())
require.False(t, done, "should not be done")
status := game.ProgressGame(context.Background())
require.Equal(t, gameTypes.GameStatusInProgress, status)
require.Equal(t, 1, actor.callCount, "should perform next actions")
errLog := handler.FindLog(log.LvlError, "Error when acting on game")
require.NotNil(t, errLog, "should log error")
......@@ -38,42 +39,42 @@ func TestProgressGame_LogErrorFromAct(t *testing.T) {
func TestProgressGame_LogGameStatus(t *testing.T) {
tests := []struct {
name string
status types.GameStatus
status gameTypes.GameStatus
agreeWithOutput bool
logLevel log.Lvl
logMsg string
}{
{
name: "GameLostAsDefender",
status: types.GameStatusChallengerWon,
status: gameTypes.GameStatusChallengerWon,
agreeWithOutput: false,
logLevel: log.LvlError,
logMsg: "Game lost",
},
{
name: "GameLostAsChallenger",
status: types.GameStatusDefenderWon,
status: gameTypes.GameStatusDefenderWon,
agreeWithOutput: true,
logLevel: log.LvlError,
logMsg: "Game lost",
},
{
name: "GameWonAsDefender",
status: types.GameStatusDefenderWon,
status: gameTypes.GameStatusDefenderWon,
agreeWithOutput: false,
logLevel: log.LvlInfo,
logMsg: "Game won",
},
{
name: "GameWonAsChallenger",
status: types.GameStatusChallengerWon,
status: gameTypes.GameStatusChallengerWon,
agreeWithOutput: true,
logLevel: log.LvlInfo,
logMsg: "Game won",
},
{
name: "GameInProgress",
status: types.GameStatusInProgress,
status: gameTypes.GameStatusInProgress,
agreeWithOutput: true,
logLevel: log.LvlInfo,
logMsg: "Game info",
......@@ -85,9 +86,9 @@ func TestProgressGame_LogGameStatus(t *testing.T) {
handler, game, gameState := setupProgressGameTest(t, test.agreeWithOutput)
gameState.status = test.status
done := game.ProgressGame(context.Background())
status := game.ProgressGame(context.Background())
require.Equal(t, 1, gameState.callCount, "should perform next actions")
require.Equal(t, test.status != types.GameStatusInProgress, done, "should be done when not in progress")
require.Equal(t, test.status, status)
errLog := handler.FindLog(test.logLevel, test.logMsg)
require.NotNil(t, errLog, "should log game result")
require.Equal(t, test.status, errLog.GetContextValue("status"))
......@@ -96,19 +97,19 @@ func TestProgressGame_LogGameStatus(t *testing.T) {
}
func TestDoNotActOnCompleteGame(t *testing.T) {
for _, status := range []types.GameStatus{types.GameStatusChallengerWon, types.GameStatusDefenderWon} {
for _, status := range []gameTypes.GameStatus{gameTypes.GameStatusChallengerWon, gameTypes.GameStatusDefenderWon} {
t.Run(status.String(), func(t *testing.T) {
_, game, gameState := setupProgressGameTest(t, true)
gameState.status = status
done := game.ProgressGame(context.Background())
fetched := game.ProgressGame(context.Background())
require.Equal(t, 1, gameState.callCount, "acts the first time")
require.True(t, done, "should be done")
require.Equal(t, status, fetched)
// Should not act when it knows the game is already complete
done = game.ProgressGame(context.Background())
fetched = game.ProgressGame(context.Background())
require.Equal(t, 1, gameState.callCount, "does not act after game is complete")
require.True(t, done, "should still be done")
require.Equal(t, status, fetched)
})
}
}
......@@ -166,7 +167,7 @@ func setupProgressGameTest(t *testing.T, agreeWithProposedRoot bool) (*testlog.C
}
type stubGameState struct {
status types.GameStatus
status gameTypes.GameStatus
claimCount uint64
callCount int
actErr error
......@@ -178,7 +179,7 @@ func (s *stubGameState) Act(ctx context.Context) error {
return s.actErr
}
func (s *stubGameState) GetGameStatus(ctx context.Context) (types.GameStatus, error) {
func (s *stubGameState) GetGameStatus(ctx context.Context) (gameTypes.GameStatus, error) {
return s.status, nil
}
......
......@@ -6,6 +6,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum"
......@@ -81,23 +82,23 @@ func (r *faultResponder) BuildTx(ctx context.Context, response types.Claim) ([]b
// 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) (types.GameStatus, error) {
func (r *faultResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
txData, err := r.buildResolveData()
if err != nil {
return types.GameStatusInProgress, err
return gameTypes.GameStatusInProgress, err
}
res, err := r.txMgr.Call(ctx, ethereum.CallMsg{
To: &r.fdgAddr,
Data: txData,
}, nil)
if err != nil {
return types.GameStatusInProgress, err
return gameTypes.GameStatusInProgress, err
}
var status uint8
if err = r.fdgAbi.UnpackIntoInterface(&status, "resolve", res); err != nil {
return types.GameStatusInProgress, err
return gameTypes.GameStatusInProgress, err
}
return types.GameStatusFromUint8(status)
return gameTypes.GameStatusFromUint8(status)
}
// Resolve executes a resolve transaction to resolve a fault dispute game.
......
......@@ -8,6 +8,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
......@@ -32,7 +33,7 @@ func TestCallResolve(t *testing.T) {
mockTxMgr.callFails = true
status, err := responder.CallResolve(context.Background())
require.ErrorIs(t, err, mockCallError)
require.Equal(t, types.GameStatusInProgress, status)
require.Equal(t, gameTypes.GameStatusInProgress, status)
require.Equal(t, 0, mockTxMgr.calls)
})
......@@ -41,7 +42,7 @@ func TestCallResolve(t *testing.T) {
mockTxMgr.callBytes = []byte{0x00, 0x01}
status, err := responder.CallResolve(context.Background())
require.Error(t, err)
require.Equal(t, types.GameStatusInProgress, status)
require.Equal(t, gameTypes.GameStatusInProgress, status)
require.Equal(t, 1, mockTxMgr.calls)
})
......@@ -49,7 +50,7 @@ func TestCallResolve(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
status, err := responder.CallResolve(context.Background())
require.NoError(t, err)
require.Equal(t, types.GameStatusInProgress, status)
require.Equal(t, gameTypes.GameStatusInProgress, status)
require.Equal(t, 1, mockTxMgr.calls)
})
}
......
......@@ -3,7 +3,6 @@ package types
import (
"context"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
......@@ -13,36 +12,6 @@ var (
ErrGameDepthReached = errors.New("game depth reached")
)
type GameStatus uint8
const (
GameStatusInProgress GameStatus = iota
GameStatusChallengerWon
GameStatusDefenderWon
)
// String returns the string representation of the game status.
func (s GameStatus) String() string {
switch s {
case GameStatusInProgress:
return "In Progress"
case GameStatusChallengerWon:
return "Challenger Won"
case GameStatusDefenderWon:
return "Defender Won"
default:
return "Unknown"
}
}
// GameStatusFromUint8 returns a game status from the uint8 representation.
func GameStatusFromUint8(i uint8) (GameStatus, error) {
if i > 2 {
return GameStatus(i), fmt.Errorf("invalid game status: %d", i)
}
return GameStatus(i), nil
}
// PreimageOracleData encapsulates the preimage oracle data
// to load into the onchain oracle.
type PreimageOracleData struct {
......
package types
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
var validGameStatuses = []GameStatus{
GameStatusInProgress,
GameStatusChallengerWon,
GameStatusDefenderWon,
}
func TestGameStatusFromUint8(t *testing.T) {
for _, status := range validGameStatuses {
t.Run(fmt.Sprintf("Valid Game Status %v", status), func(t *testing.T) {
parsed, err := GameStatusFromUint8(uint8(status))
require.NoError(t, err)
require.Equal(t, status, parsed)
})
}
t.Run("Invalid", func(t *testing.T) {
status, err := GameStatusFromUint8(3)
require.Error(t, err)
require.Equal(t, GameStatus(3), status)
})
}
func TestNewPreimageOracleData(t *testing.T) {
t.Run("LocalData", func(t *testing.T) {
data := NewPreimageOracleData([]byte{1, 2, 3}, []byte{4, 5, 6}, 7)
......
......@@ -8,7 +8,6 @@ import (
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/scheduler"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
......@@ -27,7 +26,6 @@ type gameScheduler interface {
type gameMonitor struct {
logger log.Logger
metrics metrics.Metricer
clock clock.Clock
source gameSource
scheduler gameScheduler
......@@ -38,7 +36,6 @@ type gameMonitor struct {
func newGameMonitor(
logger log.Logger,
m metrics.Metricer,
cl clock.Clock,
source gameSource,
scheduler gameScheduler,
......@@ -48,7 +45,6 @@ func newGameMonitor(
) *gameMonitor {
return &gameMonitor{
logger: logger,
metrics: m,
clock: cl,
scheduler: scheduler,
source: source,
......
......@@ -6,7 +6,6 @@ import (
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum/go-ethereum/common"
......@@ -101,7 +100,7 @@ func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor
return i, nil
}
sched := &stubScheduler{}
monitor := newGameMonitor(logger, metrics.NoopMetrics, clock.SystemClock, source, sched, time.Duration(0), fetchBlockNum, allowedGames)
monitor := newGameMonitor(logger, clock.SystemClock, source, sched, time.Duration(0), fetchBlockNum, allowedGames)
return monitor, source, sched
}
......
......@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"golang.org/x/exp/slices"
......@@ -17,7 +19,7 @@ type PlayerCreator func(address common.Address, dir string) (GamePlayer, error)
type gameState struct {
player GamePlayer
inflight bool
resolved bool
status types.GameStatus
}
// coordinator manages the set of current games, queues games to be played (on separate worker threads) and
......@@ -31,6 +33,7 @@ type coordinator struct {
resultQueue <-chan job
logger log.Logger
m SchedulerMetricer
createPlayer PlayerCreator
states map[common.Address]*gameState
disk DiskManager
......@@ -49,18 +52,35 @@ func (c *coordinator) schedule(ctx context.Context, games []common.Address) erro
}
}
var gamesInProgress int
var gamesChallengerWon int
var gamesDefenderWon int
var errs []error
var jobs []job
// Next collect all the jobs to schedule and ensure all games are recorded in the states map.
// Otherwise, results may start being processed before all games are recorded, resulting in existing
// data directories potentially being deleted for games that are required.
var jobs []job
for _, addr := range games {
if j, err := c.createJob(addr); err != nil {
errs = append(errs, err)
} else if j != nil {
jobs = append(jobs, *j)
}
state, ok := c.states[addr]
if ok {
switch state.status {
case types.GameStatusInProgress:
gamesInProgress++
case types.GameStatusDefenderWon:
gamesDefenderWon++
case types.GameStatusChallengerWon:
gamesChallengerWon++
}
} else {
c.logger.Warn("Game not found in states map", "game", addr)
}
}
c.m.RecordGamesStatus(gamesInProgress, gamesChallengerWon, gamesDefenderWon)
// Finally, enqueue the jobs
for _, j := range jobs {
......@@ -114,7 +134,7 @@ func (c *coordinator) processResult(j job) error {
return fmt.Errorf("game %v received unexpected result: %w", j.addr, errUnknownGame)
}
state.inflight = false
state.resolved = j.resolved
state.status = j.status
c.deleteResolvedGameFiles()
return nil
}
......@@ -122,7 +142,7 @@ func (c *coordinator) processResult(j job) error {
func (c *coordinator) deleteResolvedGameFiles() {
var keepGames []common.Address
for addr, state := range c.states {
if !state.resolved || state.inflight {
if state.status == types.GameStatusInProgress || state.inflight {
keepGames = append(keepGames, addr)
}
}
......@@ -131,9 +151,10 @@ func (c *coordinator) deleteResolvedGameFiles() {
}
}
func newCoordinator(logger log.Logger, jobQueue chan<- job, resultQueue <-chan job, createPlayer PlayerCreator, disk DiskManager) *coordinator {
func newCoordinator(logger log.Logger, m SchedulerMetricer, jobQueue chan<- job, resultQueue <-chan job, createPlayer PlayerCreator, disk DiskManager) *coordinator {
return &coordinator{
logger: logger,
m: m,
jobQueue: jobQueue,
resultQueue: resultQueue,
createPlayer: createPlayer,
......
......@@ -5,6 +5,8 @@ import (
"fmt"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
......@@ -140,7 +142,7 @@ func TestDeleteDataForResolvedGames(t *testing.T) {
require.NoError(t, c.schedule(ctx, []common.Address{gameAddr3}))
require.Len(t, workQueue, 1)
j := <-workQueue
j.resolved = true
j.status = types.GameStatusDefenderWon
require.NoError(t, c.processResult(j))
// But ensure its data directory is marked as existing
disk.DirForGame(gameAddr3)
......@@ -155,7 +157,9 @@ func TestDeleteDataForResolvedGames(t *testing.T) {
// Game 3 hasn't yet progressed (update is still in flight)
for i := 0; i < len(gameAddrs)-1; i++ {
j := <-workQueue
j.resolved = j.addr == gameAddr2
if j.addr == gameAddr2 {
j.status = types.GameStatusDefenderWon
}
require.NoError(t, c.processResult(j))
}
......@@ -229,20 +233,20 @@ func setupCoordinatorTest(t *testing.T, bufferSize int) (*coordinator, <-chan jo
created: make(map[common.Address]*stubGame),
}
disk := &stubDiskManager{gameDirExists: make(map[common.Address]bool)}
c := newCoordinator(logger, workQueue, resultQueue, games.CreateGame, disk)
c := newCoordinator(logger, metrics.NoopMetrics, workQueue, resultQueue, games.CreateGame, disk)
return c, workQueue, resultQueue, games, disk
}
type stubGame struct {
addr common.Address
progressCount int
done bool
status types.GameStatus
dir string
}
func (g *stubGame) ProgressGame(_ context.Context) bool {
func (g *stubGame) ProgressGame(_ context.Context) types.GameStatus {
g.progressCount++
return g.done
return g.status
}
type createdGames struct {
......@@ -259,9 +263,13 @@ func (c *createdGames) CreateGame(addr common.Address, dir string) (GamePlayer,
if _, exists := c.created[addr]; exists {
c.t.Fatalf("game %v already exists", addr)
}
status := types.GameStatusInProgress
if addr == c.createCompleted {
status = types.GameStatusDefenderWon
}
game := &stubGame{
addr: addr,
done: addr == c.createCompleted,
status: status,
dir: dir,
}
c.created[addr] = game
......
......@@ -11,6 +11,10 @@ import (
var ErrBusy = errors.New("busy scheduling previous update")
type SchedulerMetricer interface {
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
}
type Scheduler struct {
logger log.Logger
coordinator *coordinator
......@@ -22,7 +26,7 @@ type Scheduler struct {
cancel func()
}
func NewScheduler(logger log.Logger, disk DiskManager, maxConcurrency uint, createPlayer PlayerCreator) *Scheduler {
func NewScheduler(logger log.Logger, m SchedulerMetricer, disk DiskManager, maxConcurrency uint, createPlayer PlayerCreator) *Scheduler {
// Size job and results queues to be fairly small so backpressure is applied early
// but with enough capacity to keep the workers busy
jobQueue := make(chan job, maxConcurrency*2)
......@@ -34,7 +38,7 @@ func NewScheduler(logger log.Logger, disk DiskManager, maxConcurrency uint, crea
return &Scheduler{
logger: logger,
coordinator: newCoordinator(logger, jobQueue, resultQueue, createPlayer, disk),
coordinator: newCoordinator(logger, m, jobQueue, resultQueue, createPlayer, disk),
maxConcurrency: maxConcurrency,
scheduleQueue: scheduleQueue,
jobQueue: jobQueue,
......
......@@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
......@@ -18,7 +19,7 @@ func TestSchedulerProcessesGames(t *testing.T) {
}
removeExceptCalls := make(chan []common.Address)
disk := &trackingDiskManager{removeExceptCalls: removeExceptCalls}
s := NewScheduler(logger, disk, 2, createPlayer)
s := NewScheduler(logger, metrics.NoopMetrics, disk, 2, createPlayer)
s.Start(ctx)
gameAddr1 := common.Address{0xaa}
......@@ -46,7 +47,7 @@ func TestReturnBusyWhenScheduleQueueFull(t *testing.T) {
}
removeExceptCalls := make(chan []common.Address)
disk := &trackingDiskManager{removeExceptCalls: removeExceptCalls}
s := NewScheduler(logger, disk, 2, createPlayer)
s := NewScheduler(logger, metrics.NoopMetrics, disk, 2, createPlayer)
// Scheduler not started - first call fills the queue
require.NoError(t, s.Schedule([]common.Address{{0xaa}}))
......
......@@ -4,10 +4,12 @@ import (
"context"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
)
type GamePlayer interface {
ProgressGame(ctx context.Context) bool
ProgressGame(ctx context.Context) types.GameStatus
}
type DiskManager interface {
......@@ -18,5 +20,5 @@ type DiskManager interface {
type job struct {
addr common.Address
player GamePlayer
resolved bool
status types.GameStatus
}
......@@ -15,7 +15,7 @@ func progressGames(ctx context.Context, in <-chan job, out chan<- job, wg *sync.
case <-ctx.Done():
return
case j := <-in:
j.resolved = j.player.ProgressGame(ctx)
j.status = j.player.ProgressGame(ctx)
out <- j
}
}
......
......@@ -6,6 +6,8 @@ import (
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/stretchr/testify/require"
)
......@@ -20,17 +22,17 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) {
go progressGames(ctx, in, out, &wg)
in <- job{
player: &stubPlayer{done: false},
player: &stubPlayer{status: types.GameStatusInProgress},
}
in <- job{
player: &stubPlayer{done: true},
player: &stubPlayer{status: types.GameStatusDefenderWon},
}
result1 := readWithTimeout(t, out)
result2 := readWithTimeout(t, out)
require.Equal(t, result1.resolved, false)
require.Equal(t, result2.resolved, true)
require.Equal(t, result1.status, types.GameStatusInProgress)
require.Equal(t, result2.status, types.GameStatusDefenderWon)
// Cancel the context which should exit the worker
cancel()
......@@ -38,11 +40,11 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) {
}
type stubPlayer struct {
done bool
status types.GameStatus
}
func (s *stubPlayer) ProgressGame(ctx context.Context) bool {
return s.done
func (s *stubPlayer) ProgressGame(ctx context.Context) types.GameStatus {
return s.status
}
func readWithTimeout[T any](t *testing.T, ch <-chan T) T {
......
......@@ -69,13 +69,14 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se
disk := newDiskManager(cfg.Datadir)
sched := scheduler.NewScheduler(
logger,
m,
disk,
cfg.MaxConcurrency,
func(addr common.Address, dir string) (scheduler.GamePlayer, error) {
return fault.NewGamePlayer(ctx, logger, m, cfg, dir, addr, txMgr, client)
})
monitor := newGameMonitor(logger, m, cl, loader, sched, cfg.GameWindow, client.BlockNumber, cfg.GameAllowlist)
monitor := newGameMonitor(logger, cl, loader, sched, cfg.GameWindow, client.BlockNumber, cfg.GameAllowlist)
m.RecordInfo(version.SimpleWithMeta)
m.RecordUp()
......
package types
import (
"fmt"
)
type GameStatus uint8
const (
GameStatusInProgress GameStatus = iota
GameStatusChallengerWon
GameStatusDefenderWon
)
// String returns the string representation of the game status.
func (s GameStatus) String() string {
switch s {
case GameStatusInProgress:
return "In Progress"
case GameStatusChallengerWon:
return "Challenger Won"
case GameStatusDefenderWon:
return "Defender Won"
default:
return "Unknown"
}
}
// GameStatusFromUint8 returns a game status from the uint8 representation.
func GameStatusFromUint8(i uint8) (GameStatus, error) {
if i > 2 {
return GameStatus(i), fmt.Errorf("invalid game status: %d", i)
}
return GameStatus(i), nil
}
package types
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
var validGameStatuses = []GameStatus{
GameStatusInProgress,
GameStatusChallengerWon,
GameStatusDefenderWon,
}
func TestGameStatusFromUint8(t *testing.T) {
for _, status := range validGameStatuses {
t.Run(fmt.Sprintf("Valid Game Status %v", status), func(t *testing.T) {
parsed, err := GameStatusFromUint8(uint8(status))
require.NoError(t, err)
require.Equal(t, status, parsed)
})
}
t.Run("Invalid", func(t *testing.T) {
status, err := GameStatusFromUint8(3)
require.Error(t, err)
require.Equal(t, GameStatus(3), status)
})
}
......@@ -24,6 +24,8 @@ type Metricer interface {
RecordGameStep()
RecordGameMove()
RecordCannonExecutionTime(t float64)
RecordGamesStatus(inProgress, defenderWon, challengerWon int)
}
type Metrics struct {
......@@ -38,7 +40,10 @@ type Metrics struct {
moves prometheus.Counter
steps prometheus.Counter
cannonExecutionTime prometheus.Histogram
trackedGames prometheus.GaugeVec
}
var _ Metricer = (*Metrics)(nil)
......@@ -82,6 +87,13 @@ func NewMetrics() *Metrics {
Help: "Time (in seconds) to execute cannon",
Buckets: append([]float64{1.0, 10.0}, prometheus.ExponentialBuckets(30.0, 2.0, 14)...),
}),
trackedGames: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "tracked_games",
Help: "Number of games being tracked by the challenger",
}, []string{
"status",
}),
}
}
......@@ -120,3 +132,9 @@ func (m *Metrics) RecordGameStep() {
func (m *Metrics) RecordCannonExecutionTime(t float64) {
m.cannonExecutionTime.Observe(t)
}
func (m *Metrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {
m.trackedGames.WithLabelValues("in_progress").Set(float64(inProgress))
m.trackedGames.WithLabelValues("defender_won").Set(float64(defenderWon))
m.trackedGames.WithLabelValues("challenger_won").Set(float64(challengerWon))
}
......@@ -12,6 +12,10 @@ var NoopMetrics Metricer = new(noopMetrics)
func (*noopMetrics) RecordInfo(version string) {}
func (*noopMetrics) RecordUp() {}
func (*noopMetrics) RecordGameMove() {}
func (*noopMetrics) RecordGameStep() {}
func (*noopMetrics) RecordCannonExecutionTime(t float64) {}
func (*noopMetrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {}
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