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

Merge branch 'develop' into aj/delete-trace-files

parents d47df31b ffcff575
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/config" "github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-node/chaincfg" "github.com/ethereum-optimism/optimism/op-node/chaincfg"
...@@ -233,6 +234,23 @@ func TestCannonSnapshotFreq(t *testing.T) { ...@@ -233,6 +234,23 @@ func TestCannonSnapshotFreq(t *testing.T) {
}) })
} }
func TestGameWindow(t *testing.T) {
t.Run("UsesDefault", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet))
require.Equal(t, config.DefaultGameWindow, cfg.GameWindow)
})
t.Run("Valid", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet, "--game-window=1m"))
require.Equal(t, time.Duration(time.Minute), cfg.GameWindow)
})
t.Run("ParsesDefault", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet, "--game-window=264h"))
require.Equal(t, config.DefaultGameWindow, cfg.GameWindow)
})
}
func TestRequireEitherCannonNetworkOrRollupAndGenesis(t *testing.T) { func TestRequireEitherCannonNetworkOrRollupAndGenesis(t *testing.T) {
verifyArgsInvalid( verifyArgsInvalid(
t, t,
......
...@@ -3,6 +3,7 @@ package config ...@@ -3,6 +3,7 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -73,7 +74,14 @@ func ValidTraceType(value TraceType) bool { ...@@ -73,7 +74,14 @@ func ValidTraceType(value TraceType) bool {
return false return false
} }
const DefaultCannonSnapshotFreq = uint(1_000_000_000) const (
DefaultCannonSnapshotFreq = uint(1_000_000_000)
// DefaultGameWindow is the default maximum time duration in the past
// that the challenger will look for games to progress.
// The default value is 11 days, which is a 4 day resolution buffer
// plus the 7 day game finalization window.
DefaultGameWindow = time.Duration(11 * 24 * time.Hour)
)
// Config is a well typed config that is parsed from the CLI params. // Config is a well typed config that is parsed from the CLI params.
// This also contains config options for auxiliary services. // This also contains config options for auxiliary services.
...@@ -82,8 +90,8 @@ type Config struct { ...@@ -82,8 +90,8 @@ type Config struct {
L1EthRpc string // L1 RPC Url L1EthRpc string // L1 RPC Url
GameFactoryAddress common.Address // Address of the dispute game factory GameFactoryAddress common.Address // Address of the dispute game factory
GameAllowlist []common.Address // Allowlist of fault game addresses GameAllowlist []common.Address // Allowlist of fault game addresses
GameWindow time.Duration // Maximum time duration to look for games to progress
AgreeWithProposedOutput bool // Temporary config if we agree or disagree with the posted output AgreeWithProposedOutput bool // Temporary config if we agree or disagree with the posted output
TraceType TraceType // Type of trace TraceType TraceType // Type of trace
// Specific to the alphabet trace provider // Specific to the alphabet trace provider
...@@ -124,6 +132,7 @@ func NewConfig( ...@@ -124,6 +132,7 @@ func NewConfig(
PprofConfig: oppprof.DefaultCLIConfig(), PprofConfig: oppprof.DefaultCLIConfig(),
CannonSnapshotFreq: DefaultCannonSnapshotFreq, CannonSnapshotFreq: DefaultCannonSnapshotFreq,
GameWindow: DefaultGameWindow,
} }
} }
......
...@@ -55,7 +55,7 @@ func TestBuildFaultDefendData(t *testing.T) { ...@@ -55,7 +55,7 @@ func TestBuildFaultDefendData(t *testing.T) {
_, opts, _, contract, err := setupFaultDisputeGame() _, opts, _, contract, err := setupFaultDisputeGame()
require.NoError(t, err) require.NoError(t, err)
responder, _ := newTestFaultResponder(t, false) responder, _ := newTestFaultResponder(t)
data, err := responder.buildFaultDefendData(1, [32]byte{0x02, 0x03}) data, err := responder.buildFaultDefendData(1, [32]byte{0x02, 0x03})
require.NoError(t, err) require.NoError(t, err)
...@@ -72,7 +72,7 @@ func TestBuildFaultAttackData(t *testing.T) { ...@@ -72,7 +72,7 @@ func TestBuildFaultAttackData(t *testing.T) {
_, opts, _, contract, err := setupFaultDisputeGame() _, opts, _, contract, err := setupFaultDisputeGame()
require.NoError(t, err) require.NoError(t, err)
responder, _ := newTestFaultResponder(t, false) responder, _ := newTestFaultResponder(t)
data, err := responder.buildFaultAttackData(1, [32]byte{0x02, 0x03}) data, err := responder.buildFaultAttackData(1, [32]byte{0x02, 0x03})
require.NoError(t, err) require.NoError(t, err)
...@@ -89,7 +89,7 @@ func TestBuildFaultStepData(t *testing.T) { ...@@ -89,7 +89,7 @@ func TestBuildFaultStepData(t *testing.T) {
_, opts, _, contract, err := setupFaultDisputeGame() _, opts, _, contract, err := setupFaultDisputeGame()
require.NoError(t, err) require.NoError(t, err)
responder, _ := newTestFaultResponder(t, false) responder, _ := newTestFaultResponder(t)
data, err := responder.buildStepTxData(types.StepCallData{ data, err := responder.buildStepTxData(types.StepCallData{
ClaimIndex: 2, ClaimIndex: 2,
......
...@@ -13,7 +13,7 @@ import ( ...@@ -13,7 +13,7 @@ import (
// Responder takes a response action & executes. // Responder takes a response action & executes.
// For full op-challenger this means executing the transaction on chain. // For full op-challenger this means executing the transaction on chain.
type Responder interface { type Responder interface {
CanResolve(ctx context.Context) bool CallResolve(ctx context.Context) (types.GameStatus, error)
Resolve(ctx context.Context) error Resolve(ctx context.Context) error
Respond(ctx context.Context, response types.Claim) error Respond(ctx context.Context, response types.Claim) error
Step(ctx context.Context, stepData types.StepCallData) error Step(ctx context.Context, stepData types.StepCallData) error
...@@ -65,10 +65,27 @@ func (a *Agent) Act(ctx context.Context) error { ...@@ -65,10 +65,27 @@ func (a *Agent) Act(ctx context.Context) error {
return nil return nil
} }
// 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(ctx context.Context, status types.GameStatus) bool {
expected := types.GameStatusDefenderWon
if a.agreeWithProposedOutput {
expected = types.GameStatusChallengerWon
}
if expected != status {
a.log.Warn("Game will be lost", "expected", expected, "actual", status)
}
return expected == status
}
// tryResolve resolves the game if it is in a terminal state // tryResolve resolves the game if it is in a terminal state
// and returns true if the game resolves successfully. // and returns true if the game resolves successfully.
func (a *Agent) tryResolve(ctx context.Context) bool { func (a *Agent) tryResolve(ctx context.Context) bool {
if !a.responder.CanResolve(ctx) { status, err := a.responder.CallResolve(ctx)
if err != nil {
return false
}
if !a.shouldResolve(ctx, status) {
return false return false
} }
a.log.Info("Resolving game") a.log.Info("Resolving game")
......
package fault
import (
"context"
"testing"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum-optimism/optimism/op-node/testlog"
)
// TestShouldResolve tests the resolution logic.
func TestShouldResolve(t *testing.T) {
log := testlog.Logger(t, log.LvlCrit)
t.Run("AgreeWithProposedOutput", func(t *testing.T) {
agent := NewAgent(nil, 0, nil, nil, nil, true, log)
require.False(t, agent.shouldResolve(context.Background(), types.GameStatusDefenderWon))
require.True(t, agent.shouldResolve(context.Background(), types.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(context.Background(), types.GameStatusInProgress))
})
t.Run("DisagreeWithProposedOutput", func(t *testing.T) {
agent := NewAgent(nil, 0, nil, nil, nil, false, log)
require.True(t, agent.shouldResolve(context.Background(), types.GameStatusDefenderWon))
require.False(t, agent.shouldResolve(context.Background(), types.GameStatusChallengerWon))
require.False(t, agent.shouldResolve(context.Background(), types.GameStatusInProgress))
})
}
...@@ -43,7 +43,7 @@ func NewGameLoader(caller MinimalDisputeGameFactoryCaller) *gameLoader { ...@@ -43,7 +43,7 @@ func NewGameLoader(caller MinimalDisputeGameFactoryCaller) *gameLoader {
} }
// FetchAllGamesAtBlock fetches all dispute games from the factory at a given block number. // FetchAllGamesAtBlock fetches all dispute games from the factory at a given block number.
func (l *gameLoader) FetchAllGamesAtBlock(ctx context.Context, blockNumber *big.Int) ([]FaultDisputeGame, error) { func (l *gameLoader) FetchAllGamesAtBlock(ctx context.Context, earliestTimestamp uint64, blockNumber *big.Int) ([]FaultDisputeGame, error) {
if blockNumber == nil { if blockNumber == nil {
return nil, ErrMissingBlockNumber return nil, ErrMissingBlockNumber
} }
...@@ -56,14 +56,19 @@ func (l *gameLoader) FetchAllGamesAtBlock(ctx context.Context, blockNumber *big. ...@@ -56,14 +56,19 @@ func (l *gameLoader) FetchAllGamesAtBlock(ctx context.Context, blockNumber *big.
return nil, fmt.Errorf("failed to fetch game count: %w", err) return nil, fmt.Errorf("failed to fetch game count: %w", err)
} }
games := make([]FaultDisputeGame, gameCount.Uint64()) games := make([]FaultDisputeGame, 0)
for i := uint64(0); i < gameCount.Uint64(); i++ { if gameCount.Uint64() == 0 {
game, err := l.caller.GameAtIndex(callOpts, big.NewInt(int64(i))) return games, nil
}
for i := gameCount.Uint64(); i > 0; i-- {
game, err := l.caller.GameAtIndex(callOpts, big.NewInt(int64(i-1)))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch game at index %d: %w", i, err) return nil, fmt.Errorf("failed to fetch game at index %d: %w", i, err)
} }
if game.Timestamp < earliestTimestamp {
games[i] = game break
}
games = append(games, game)
} }
return games, nil return games, nil
......
...@@ -25,6 +25,7 @@ func TestGameLoader_FetchAllGames(t *testing.T) { ...@@ -25,6 +25,7 @@ func TestGameLoader_FetchAllGames(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
caller *mockMinimalDisputeGameFactoryCaller caller *mockMinimalDisputeGameFactoryCaller
earliest uint64
blockNumber *big.Int blockNumber *big.Int
expectedErr error expectedErr error
expectedLen int expectedLen int
...@@ -33,35 +34,36 @@ func TestGameLoader_FetchAllGames(t *testing.T) { ...@@ -33,35 +34,36 @@ func TestGameLoader_FetchAllGames(t *testing.T) {
name: "success", name: "success",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, false), caller: newMockMinimalDisputeGameFactoryCaller(10, false, false),
blockNumber: big.NewInt(1), blockNumber: big.NewInt(1),
expectedErr: nil,
expectedLen: 10, expectedLen: 10,
}, },
{
name: "expired game ignored",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, false),
earliest: 500,
blockNumber: big.NewInt(1),
expectedLen: 5,
},
{ {
name: "game count error", name: "game count error",
caller: newMockMinimalDisputeGameFactoryCaller(10, true, false), caller: newMockMinimalDisputeGameFactoryCaller(10, true, false),
blockNumber: big.NewInt(1), blockNumber: big.NewInt(1),
expectedErr: gameCountErr, expectedErr: gameCountErr,
expectedLen: 0,
}, },
{ {
name: "game index error", name: "game index error",
caller: newMockMinimalDisputeGameFactoryCaller(10, false, true), caller: newMockMinimalDisputeGameFactoryCaller(10, false, true),
blockNumber: big.NewInt(1), blockNumber: big.NewInt(1),
expectedErr: gameIndexErr, expectedErr: gameIndexErr,
expectedLen: 0,
}, },
{ {
name: "no games", name: "no games",
caller: newMockMinimalDisputeGameFactoryCaller(0, false, false), caller: newMockMinimalDisputeGameFactoryCaller(0, false, false),
blockNumber: big.NewInt(1), blockNumber: big.NewInt(1),
expectedErr: nil,
expectedLen: 0,
}, },
{ {
name: "missing block number", name: "missing block number",
caller: newMockMinimalDisputeGameFactoryCaller(0, false, false), caller: newMockMinimalDisputeGameFactoryCaller(0, false, false),
expectedErr: ErrMissingBlockNumber, expectedErr: ErrMissingBlockNumber,
expectedLen: 0,
}, },
} }
...@@ -72,10 +74,11 @@ func TestGameLoader_FetchAllGames(t *testing.T) { ...@@ -72,10 +74,11 @@ func TestGameLoader_FetchAllGames(t *testing.T) {
t.Parallel() t.Parallel()
loader := NewGameLoader(test.caller) loader := NewGameLoader(test.caller)
games, err := loader.FetchAllGamesAtBlock(context.Background(), test.blockNumber) games, err := loader.FetchAllGamesAtBlock(context.Background(), test.earliest, test.blockNumber)
require.ErrorIs(t, err, test.expectedErr) require.ErrorIs(t, err, test.expectedErr)
require.Len(t, games, test.expectedLen) require.Len(t, games, test.expectedLen)
expectedGames := test.caller.games expectedGames := test.caller.games
expectedGames = expectedGames[len(expectedGames)-test.expectedLen:]
if test.expectedErr != nil { if test.expectedErr != nil {
expectedGames = make([]FaultDisputeGame, 0) expectedGames = make([]FaultDisputeGame, 0)
} }
...@@ -90,7 +93,7 @@ func generateMockGames(count uint64) []FaultDisputeGame { ...@@ -90,7 +93,7 @@ func generateMockGames(count uint64) []FaultDisputeGame {
for i := uint64(0); i < count; i++ { for i := uint64(0); i < count; i++ {
games[i] = FaultDisputeGame{ games[i] = FaultDisputeGame{
Proxy: common.BigToAddress(big.NewInt(int64(i))), Proxy: common.BigToAddress(big.NewInt(int64(i))),
Timestamp: i, Timestamp: i * 100,
} }
} }
......
...@@ -21,24 +21,26 @@ type blockNumberFetcher func(ctx context.Context) (uint64, error) ...@@ -21,24 +21,26 @@ type blockNumberFetcher func(ctx context.Context) (uint64, error)
// gameSource loads information about the games available to play // gameSource loads information about the games available to play
type gameSource interface { type gameSource interface {
FetchAllGamesAtBlock(ctx context.Context, blockNumber *big.Int) ([]FaultDisputeGame, error) FetchAllGamesAtBlock(ctx context.Context, earliest uint64, blockNumber *big.Int) ([]FaultDisputeGame, error)
} }
type gameMonitor struct { type gameMonitor struct {
logger log.Logger logger log.Logger
clock clock.Clock clock clock.Clock
source gameSource source gameSource
gameWindow time.Duration
createPlayer playerCreator createPlayer playerCreator
fetchBlockNumber blockNumberFetcher fetchBlockNumber blockNumberFetcher
allowedGames []common.Address allowedGames []common.Address
players map[common.Address]gamePlayer players map[common.Address]gamePlayer
} }
func newGameMonitor(logger log.Logger, cl clock.Clock, fetchBlockNumber blockNumberFetcher, allowedGames []common.Address, source gameSource, createGame playerCreator) *gameMonitor { func newGameMonitor(logger log.Logger, gameWindow time.Duration, cl clock.Clock, fetchBlockNumber blockNumberFetcher, allowedGames []common.Address, source gameSource, createGame playerCreator) *gameMonitor {
return &gameMonitor{ return &gameMonitor{
logger: logger, logger: logger,
clock: cl, clock: cl,
source: source, source: source,
gameWindow: gameWindow,
createPlayer: createGame, createPlayer: createGame,
fetchBlockNumber: fetchBlockNumber, fetchBlockNumber: fetchBlockNumber,
allowedGames: allowedGames, allowedGames: allowedGames,
...@@ -58,12 +60,24 @@ func (m *gameMonitor) allowedGame(game common.Address) bool { ...@@ -58,12 +60,24 @@ func (m *gameMonitor) allowedGame(game common.Address) bool {
return false return false
} }
func (m *gameMonitor) minGameTimestamp() uint64 {
if m.gameWindow.Seconds() == 0 {
return 0
}
// time: "To compute t-d for a duration d, use t.Add(-d)."
// https://pkg.go.dev/time#Time.Sub
if m.clock.Now().Unix() > int64(m.gameWindow.Seconds()) {
return uint64(m.clock.Now().Add(-m.gameWindow).Unix())
}
return 0
}
func (m *gameMonitor) progressGames(ctx context.Context) error { func (m *gameMonitor) progressGames(ctx context.Context) error {
blockNum, err := m.fetchBlockNumber(ctx) blockNum, err := m.fetchBlockNumber(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to load current block number: %w", err) return fmt.Errorf("failed to load current block number: %w", err)
} }
games, err := m.source.FetchAllGamesAtBlock(ctx, new(big.Int).SetUint64(blockNum)) games, err := m.source.FetchAllGamesAtBlock(ctx, m.minGameTimestamp(), new(big.Int).SetUint64(blockNum))
if err != nil { if err != nil {
return fmt.Errorf("failed to load games: %w", err) return fmt.Errorf("failed to load games: %w", err)
} }
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"math/big" "math/big"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -13,6 +14,32 @@ import ( ...@@ -13,6 +14,32 @@ import (
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
) )
func TestMonitorMinGameTimestamp(t *testing.T) {
t.Parallel()
t.Run("zero game window returns zero", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0))
})
t.Run("non-zero game window with zero clock", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, monitor.minGameTimestamp(), uint64(0))
})
t.Run("minimum computed correctly", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen)
expected := uint64(frozen.Add(-time.Minute).Unix())
require.Equal(t, monitor.minGameTimestamp(), expected)
})
}
func TestMonitorExitsWhenContextDone(t *testing.T) { func TestMonitorExitsWhenContextDone(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{common.Address{}}) monitor, _, _ := setupMonitorTest(t, []common.Address{common.Address{}})
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
...@@ -150,7 +177,7 @@ func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor ...@@ -150,7 +177,7 @@ func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor
fetchBlockNum := func(ctx context.Context) (uint64, error) { fetchBlockNum := func(ctx context.Context) (uint64, error) {
return 1234, nil return 1234, nil
} }
monitor := newGameMonitor(logger, clock.SystemClock, fetchBlockNum, allowedGames, source, games.CreateGame) monitor := newGameMonitor(logger, time.Duration(0), clock.SystemClock, fetchBlockNum, allowedGames, source, games.CreateGame)
return monitor, source, games return monitor, source, games
} }
...@@ -158,7 +185,7 @@ type stubGameSource struct { ...@@ -158,7 +185,7 @@ type stubGameSource struct {
games []FaultDisputeGame games []FaultDisputeGame
} }
func (s *stubGameSource) FetchAllGamesAtBlock(ctx context.Context, blockNumber *big.Int) ([]FaultDisputeGame, error) { func (s *stubGameSource) FetchAllGamesAtBlock(ctx context.Context, earliest uint64, blockNumber *big.Int) ([]FaultDisputeGame, error) {
return s.games, nil return s.games, nil
} }
......
...@@ -79,18 +79,25 @@ func (r *faultResponder) BuildTx(ctx context.Context, response types.Claim) ([]b ...@@ -79,18 +79,25 @@ func (r *faultResponder) BuildTx(ctx context.Context, response types.Claim) ([]b
} }
} }
// CanResolve 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 true if the game can be resolved, otherwise false. // would succeed. Returns the game status if the call would succeed, errors otherwise.
func (r *faultResponder) CanResolve(ctx context.Context) bool { func (r *faultResponder) CallResolve(ctx context.Context) (types.GameStatus, error) {
txData, err := r.buildResolveData() txData, err := r.buildResolveData()
if err != nil { if err != nil {
return false return types.GameStatusInProgress, err
} }
_, err = r.txMgr.Call(ctx, ethereum.CallMsg{ res, err := r.txMgr.Call(ctx, ethereum.CallMsg{
To: &r.fdgAddr, To: &r.fdgAddr,
Data: txData, Data: txData,
}, nil) }, nil)
return err == nil if err != nil {
return types.GameStatusInProgress, err
}
var status uint8
if err = r.fdgAbi.UnpackIntoInterface(&status, "resolve", res); err != nil {
return types.GameStatusInProgress, err
}
return types.GameStatusFromUint8(status)
} }
// Resolve executes a resolve transaction to resolve a fault dispute game. // Resolve executes a resolve transaction to resolve a fault dispute game.
......
...@@ -22,163 +22,96 @@ import ( ...@@ -22,163 +22,96 @@ import (
var ( var (
mockFdgAddress = common.HexToAddress("0x1234") mockFdgAddress = common.HexToAddress("0x1234")
mockSendError = errors.New("mock send error") mockSendError = errors.New("mock send error")
mockCallError = errors.New("mock call error")
) )
type mockTxManager struct { // TestCallResolve tests the [Responder.CallResolve].
from common.Address func TestCallResolve(t *testing.T) {
sends int t.Run("SendFails", func(t *testing.T) {
calls int responder, mockTxMgr := newTestFaultResponder(t)
sendFails bool mockTxMgr.callFails = true
} status, err := responder.CallResolve(context.Background())
require.ErrorIs(t, err, mockCallError)
func (m *mockTxManager) Send(ctx context.Context, candidate txmgr.TxCandidate) (*ethtypes.Receipt, error) { require.Equal(t, types.GameStatusInProgress, status)
if m.sendFails { require.Equal(t, 0, mockTxMgr.calls)
return nil, mockSendError })
}
m.sends++
return ethtypes.NewReceipt(
[]byte{},
false,
0,
), nil
}
func (m *mockTxManager) Call(_ context.Context, _ ethereum.CallMsg, _ *big.Int) ([]byte, error) {
if m.sendFails {
return nil, mockSendError
}
m.calls++
return []byte{}, nil
}
func (m *mockTxManager) BlockNumber(ctx context.Context) (uint64, error) {
panic("not implemented")
}
func (m *mockTxManager) From() common.Address { t.Run("UnpackFails", func(t *testing.T) {
return m.from responder, mockTxMgr := newTestFaultResponder(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, 1, mockTxMgr.calls)
})
func newTestFaultResponder(t *testing.T, sendFails bool) (*faultResponder, *mockTxManager) { t.Run("Success", func(t *testing.T) {
log := testlog.Logger(t, log.LvlError) responder, mockTxMgr := newTestFaultResponder(t)
mockTxMgr := &mockTxManager{} status, err := responder.CallResolve(context.Background())
mockTxMgr.sendFails = sendFails
responder, err := NewFaultResponder(log, mockTxMgr, mockFdgAddress)
require.NoError(t, err) require.NoError(t, err)
return responder, mockTxMgr require.Equal(t, types.GameStatusInProgress, status)
}
// TestResponder_CanResolve_CallFails tests the [Responder.CanResolve] method
// bubbles up the error returned by the [txmgr.Call] method.
func TestResponder_CanResolve_CallFails(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, true)
resolved := responder.CanResolve(context.Background())
require.False(t, resolved)
require.Equal(t, 0, mockTxMgr.sends)
}
// TestResponder_CanResolve_Success tests the [Responder.CanResolve] method
// succeeds when the call message is successfully sent through the txmgr.
func TestResponder_CanResolve_Success(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, false)
resolved := responder.CanResolve(context.Background())
require.True(t, resolved)
require.Equal(t, 1, mockTxMgr.calls) require.Equal(t, 1, mockTxMgr.calls)
})
} }
// TestResponder_Resolve_SendFails tests the [Responder.Resolve] method // TestResolve tests the [Responder.Resolve] method.
// bubbles up the error returned by the [txmgr.Send] method. func TestResolve(t *testing.T) {
func TestResponder_Resolve_SendFails(t *testing.T) { t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, true) responder, mockTxMgr := newTestFaultResponder(t)
mockTxMgr.sendFails = true
err := responder.Resolve(context.Background()) err := responder.Resolve(context.Background())
require.ErrorIs(t, err, mockSendError) require.ErrorIs(t, err, mockSendError)
require.Equal(t, 0, mockTxMgr.sends) require.Equal(t, 0, mockTxMgr.sends)
} })
// TestResponder_Resolve_Success tests the [Responder.Resolve] method t.Run("Success", func(t *testing.T) {
// succeeds when the tx candidate is successfully sent through the txmgr. responder, mockTxMgr := newTestFaultResponder(t)
func TestResponder_Resolve_Success(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, false)
err := responder.Resolve(context.Background()) err := responder.Resolve(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends) require.Equal(t, 1, mockTxMgr.sends)
})
} }
// TestResponder_Respond_SendFails tests the [Responder.Respond] method // TestRespond tests the [Responder.Respond] method.
// bubbles up the error returned by the [txmgr.Send] method. func TestRespond(t *testing.T) {
func TestResponder_Respond_SendFails(t *testing.T) { t.Run("send fails", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, true) responder, mockTxMgr := newTestFaultResponder(t)
err := responder.Respond(context.Background(), types.Claim{ mockTxMgr.sendFails = true
ClaimData: types.ClaimData{ err := responder.Respond(context.Background(), generateMockResponseClaim())
Value: common.Hash{0x01},
Position: types.NewPositionFromGIndex(2),
},
Parent: types.ClaimData{
Value: common.Hash{0x02},
Position: types.NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 0,
})
require.ErrorIs(t, err, mockSendError) require.ErrorIs(t, err, mockSendError)
require.Equal(t, 0, mockTxMgr.sends) require.Equal(t, 0, mockTxMgr.sends)
}
// TestResponder_Respond_Success tests the [Responder.Respond] method
// succeeds when the tx candidate is successfully sent through the txmgr.
func TestResponder_Respond_Success(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, false)
err := responder.Respond(context.Background(), 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,
}) })
t.Run("sends response", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
err := responder.Respond(context.Background(), generateMockResponseClaim())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends) require.Equal(t, 1, mockTxMgr.sends)
})
} }
// TestResponder_BuildTx_Attack tests the [Responder.BuildTx] method // TestBuildTx tests the [Responder.BuildTx] method.
// returns a tx candidate with the correct data for an attack tx. func TestBuildTx(t *testing.T) {
func TestResponder_BuildTx_Attack(t *testing.T) { t.Run("attack", func(t *testing.T) {
responder, _ := newTestFaultResponder(t, false) responder, _ := newTestFaultResponder(t)
responseClaim := types.Claim{ responseClaim := generateMockResponseClaim()
ClaimData: types.ClaimData{ responseClaim.ParentContractIndex = 7
Value: common.Hash{0x01},
Position: types.NewPositionFromGIndex(2),
},
Parent: types.ClaimData{
Value: common.Hash{0x02},
Position: types.NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 7,
}
tx, err := responder.BuildTx(context.Background(), responseClaim) tx, err := responder.BuildTx(context.Background(), responseClaim)
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)
expected, err := fdgAbi.Pack( parent := big.NewInt(int64(7))
"attack", claim := responseClaim.ValueBytes()
big.NewInt(int64(7)), expected, err := fdgAbi.Pack("attack", parent, claim)
responseClaim.ValueBytes(),
)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, tx) require.Equal(t, expected, tx)
} })
// TestResponder_BuildTx_Defend tests the [Responder.BuildTx] method t.Run("defend", func(t *testing.T) {
// returns a tx candidate with the correct data for a defend tx. responder, _ := newTestFaultResponder(t)
func TestResponder_BuildTx_Defend(t *testing.T) {
responder, _ := newTestFaultResponder(t, false)
responseClaim := types.Claim{ responseClaim := types.Claim{
ClaimData: types.ClaimData{ ClaimData: types.ClaimData{
Value: common.Hash{0x01}, Value: common.Hash{0x01},
...@@ -197,11 +130,76 @@ func TestResponder_BuildTx_Defend(t *testing.T) { ...@@ -197,11 +130,76 @@ func TestResponder_BuildTx_Defend(t *testing.T) {
// 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)
expected, err := fdgAbi.Pack( parent := big.NewInt(int64(7))
"defend", claim := responseClaim.ValueBytes()
big.NewInt(int64(7)), expected, err := fdgAbi.Pack("defend", parent, claim)
responseClaim.ValueBytes(),
)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, tx) require.Equal(t, expected, tx)
})
}
func newTestFaultResponder(t *testing.T) (*faultResponder, *mockTxManager) {
log := testlog.Logger(t, log.LvlError)
mockTxMgr := &mockTxManager{}
responder, err := NewFaultResponder(log, mockTxMgr, mockFdgAddress)
require.NoError(t, err)
return responder, mockTxMgr
}
type mockTxManager struct {
from common.Address
sends int
calls int
sendFails bool
callFails bool
callBytes []byte
}
func (m *mockTxManager) Send(ctx context.Context, candidate txmgr.TxCandidate) (*ethtypes.Receipt, error) {
if m.sendFails {
return nil, mockSendError
}
m.sends++
return ethtypes.NewReceipt(
[]byte{},
false,
0,
), nil
}
func (m *mockTxManager) Call(_ context.Context, _ ethereum.CallMsg, _ *big.Int) ([]byte, error) {
if m.callFails {
return nil, mockCallError
}
m.calls++
if m.callBytes != nil {
return m.callBytes, nil
}
return common.Hex2Bytes(
"0000000000000000000000000000000000000000000000000000000000000000",
), nil
}
func (m *mockTxManager) BlockNumber(ctx context.Context) (uint64, error) {
panic("not implemented")
}
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,
}
} }
...@@ -73,7 +73,7 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*se ...@@ -73,7 +73,7 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*se
} }
loader := NewGameLoader(factory) loader := NewGameLoader(factory)
monitor := newGameMonitor(logger, cl, client.BlockNumber, cfg.GameAllowlist, loader, func(addr common.Address) (gamePlayer, error) { monitor := newGameMonitor(logger, cfg.GameWindow, cl, client.BlockNumber, cfg.GameAllowlist, loader, func(addr common.Address) (gamePlayer, error) {
return NewGamePlayer(ctx, logger, cfg, addr, txMgr, client) return NewGamePlayer(ctx, logger, cfg, addr, txMgr, client)
}) })
......
...@@ -3,6 +3,7 @@ package types ...@@ -3,6 +3,7 @@ package types
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -34,6 +35,14 @@ func (s GameStatus) String() string { ...@@ -34,6 +35,14 @@ func (s GameStatus) String() string {
} }
} }
// 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 // PreimageOracleData encapsulates the preimage oracle data
// to load into the onchain oracle. // to load into the onchain oracle.
type PreimageOracleData struct { type PreimageOracleData struct {
......
package types package types
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/require" "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) { func TestNewPreimageOracleData(t *testing.T) {
t.Run("LocalData", func(t *testing.T) { t.Run("LocalData", func(t *testing.T) {
data := NewPreimageOracleData([]byte{1, 2, 3}, []byte{4, 5, 6}, 7) data := NewPreimageOracleData([]byte{1, 2, 3}, []byte{4, 5, 6}, 7)
......
...@@ -109,6 +109,12 @@ var ( ...@@ -109,6 +109,12 @@ var (
EnvVars: prefixEnvVars("CANNON_SNAPSHOT_FREQ"), EnvVars: prefixEnvVars("CANNON_SNAPSHOT_FREQ"),
Value: config.DefaultCannonSnapshotFreq, Value: config.DefaultCannonSnapshotFreq,
} }
GameWindowFlag = &cli.DurationFlag{
Name: "game-window",
Usage: "The time window which the challenger will look for games to progress.",
EnvVars: prefixEnvVars("GAME_WINDOW"),
Value: config.DefaultGameWindow,
}
) )
// requiredFlags are checked by [CheckRequired] // requiredFlags are checked by [CheckRequired]
...@@ -132,6 +138,7 @@ var optionalFlags = []cli.Flag{ ...@@ -132,6 +138,7 @@ var optionalFlags = []cli.Flag{
CannonDatadirFlag, CannonDatadirFlag,
CannonL2Flag, CannonL2Flag,
CannonSnapshotFreqFlag, CannonSnapshotFreqFlag,
GameWindowFlag,
} }
func init() { func init() {
...@@ -222,6 +229,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { ...@@ -222,6 +229,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
TraceType: traceTypeFlag, TraceType: traceTypeFlag,
GameFactoryAddress: gameFactoryAddress, GameFactoryAddress: gameFactoryAddress,
GameAllowlist: allowedGames, GameAllowlist: allowedGames,
GameWindow: ctx.Duration(GameWindowFlag.Name),
AlphabetTrace: ctx.String(AlphabetFlag.Name), AlphabetTrace: ctx.String(AlphabetFlag.Name),
CannonNetwork: ctx.String(CannonNetworkFlag.Name), CannonNetwork: ctx.String(CannonNetworkFlag.Name),
CannonRollupConfigPath: ctx.String(CannonRollupConfigFlag.Name), CannonRollupConfigPath: ctx.String(CannonRollupConfigFlag.Name),
......
...@@ -1005,7 +1005,7 @@ packages: ...@@ -1005,7 +1005,7 @@ packages:
/@changesets/apply-release-plan@6.1.3: /@changesets/apply-release-plan@6.1.3:
resolution: {integrity: sha512-ECDNeoc3nfeAe1jqJb5aFQX7CqzQhD2klXRez2JDb/aVpGUbX673HgKrnrgJRuQR/9f2TtLoYIzrGB9qwD77mg==} resolution: {integrity: sha512-ECDNeoc3nfeAe1jqJb5aFQX7CqzQhD2klXRez2JDb/aVpGUbX673HgKrnrgJRuQR/9f2TtLoYIzrGB9qwD77mg==}
dependencies: dependencies:
'@babel/runtime': 7.20.7 '@babel/runtime': 7.22.6
'@changesets/config': 2.3.0 '@changesets/config': 2.3.0
'@changesets/get-version-range-type': 0.3.2 '@changesets/get-version-range-type': 0.3.2
'@changesets/git': 2.0.0 '@changesets/git': 2.0.0
...@@ -1023,7 +1023,7 @@ packages: ...@@ -1023,7 +1023,7 @@ packages:
/@changesets/assemble-release-plan@5.2.3: /@changesets/assemble-release-plan@5.2.3:
resolution: {integrity: sha512-g7EVZCmnWz3zMBAdrcKhid4hkHT+Ft1n0mLussFMcB1dE2zCuwcvGoy9ec3yOgPGF4hoMtgHaMIk3T3TBdvU9g==} resolution: {integrity: sha512-g7EVZCmnWz3zMBAdrcKhid4hkHT+Ft1n0mLussFMcB1dE2zCuwcvGoy9ec3yOgPGF4hoMtgHaMIk3T3TBdvU9g==}
dependencies: dependencies:
'@babel/runtime': 7.20.7 '@babel/runtime': 7.22.6
'@changesets/errors': 0.1.4 '@changesets/errors': 0.1.4
'@changesets/get-dependents-graph': 1.3.5 '@changesets/get-dependents-graph': 1.3.5
'@changesets/types': 5.2.1 '@changesets/types': 5.2.1
...@@ -1126,7 +1126,7 @@ packages: ...@@ -1126,7 +1126,7 @@ packages:
/@changesets/get-release-plan@3.0.16: /@changesets/get-release-plan@3.0.16:
resolution: {integrity: sha512-OpP9QILpBp1bY2YNIKFzwigKh7Qe9KizRsZomzLe6pK8IUo8onkAAVUD8+JRKSr8R7d4+JRuQrfSSNlEwKyPYg==} resolution: {integrity: sha512-OpP9QILpBp1bY2YNIKFzwigKh7Qe9KizRsZomzLe6pK8IUo8onkAAVUD8+JRKSr8R7d4+JRuQrfSSNlEwKyPYg==}
dependencies: dependencies:
'@babel/runtime': 7.20.7 '@babel/runtime': 7.22.6
'@changesets/assemble-release-plan': 5.2.3 '@changesets/assemble-release-plan': 5.2.3
'@changesets/config': 2.3.0 '@changesets/config': 2.3.0
'@changesets/pre': 1.0.14 '@changesets/pre': 1.0.14
...@@ -1142,7 +1142,7 @@ packages: ...@@ -1142,7 +1142,7 @@ packages:
/@changesets/git@2.0.0: /@changesets/git@2.0.0:
resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==}
dependencies: dependencies:
'@babel/runtime': 7.20.7 '@babel/runtime': 7.22.6
'@changesets/errors': 0.1.4 '@changesets/errors': 0.1.4
'@changesets/types': 5.2.1 '@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3 '@manypkg/get-packages': 1.1.3
...@@ -1167,7 +1167,7 @@ packages: ...@@ -1167,7 +1167,7 @@ packages:
/@changesets/pre@1.0.14: /@changesets/pre@1.0.14:
resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==}
dependencies: dependencies:
'@babel/runtime': 7.20.7 '@babel/runtime': 7.22.6
'@changesets/errors': 0.1.4 '@changesets/errors': 0.1.4
'@changesets/types': 5.2.1 '@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3 '@manypkg/get-packages': 1.1.3
...@@ -1177,7 +1177,7 @@ packages: ...@@ -1177,7 +1177,7 @@ packages:
/@changesets/read@0.5.9: /@changesets/read@0.5.9:
resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==}
dependencies: dependencies:
'@babel/runtime': 7.20.7 '@babel/runtime': 7.22.6
'@changesets/git': 2.0.0 '@changesets/git': 2.0.0
'@changesets/logger': 0.0.5 '@changesets/logger': 0.0.5
'@changesets/parse': 0.3.16 '@changesets/parse': 0.3.16
...@@ -1197,7 +1197,7 @@ packages: ...@@ -1197,7 +1197,7 @@ packages:
/@changesets/write@0.2.3: /@changesets/write@0.2.3:
resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==}
dependencies: dependencies:
'@babel/runtime': 7.20.7 '@babel/runtime': 7.22.6
'@changesets/types': 5.2.1 '@changesets/types': 5.2.1
fs-extra: 7.0.1 fs-extra: 7.0.1
human-id: 1.0.2 human-id: 1.0.2
...@@ -2608,7 +2608,7 @@ packages: ...@@ -2608,7 +2608,7 @@ packages:
/@manypkg/get-packages@1.1.3: /@manypkg/get-packages@1.1.3:
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
dependencies: dependencies:
'@babel/runtime': 7.20.7 '@babel/runtime': 7.22.6
'@changesets/types': 4.1.0 '@changesets/types': 4.1.0
'@manypkg/find-root': 1.1.0 '@manypkg/find-root': 1.1.0
fs-extra: 8.1.0 fs-extra: 8.1.0
......
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