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

Merge branch 'develop' into aj/fix-last-step

parents 27dbdcbe 80fdee6c
...@@ -109,7 +109,6 @@ devnet-down: ...@@ -109,7 +109,6 @@ devnet-down:
devnet-clean: devnet-clean:
rm -rf ./packages/contracts-bedrock/deployments/devnetL1 rm -rf ./packages/contracts-bedrock/deployments/devnetL1
rm -rf ./packages/contracts-bedrock/deploy-config/devnetL1.json
rm -rf ./.devnet rm -rf ./.devnet
cd ./ops-bedrock && docker compose down cd ./ops-bedrock && docker compose down
docker image ls 'ops-bedrock*' --format='{{.Repository}}' | xargs -r docker rmi docker image ls 'ops-bedrock*' --format='{{.Repository}}' | xargs -r docker rmi
......
...@@ -169,6 +169,26 @@ func TestMaxConcurrency(t *testing.T) { ...@@ -169,6 +169,26 @@ func TestMaxConcurrency(t *testing.T) {
}) })
} }
func TestPollInterval(t *testing.T) {
t.Run("UsesDefault", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeCannon))
require.Equal(t, config.DefaultPollInterval, cfg.PollInterval)
})
t.Run("Valid", func(t *testing.T) {
expected := 100 * time.Second
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeAlphabet, "--http-poll-interval", "100s"))
require.Equal(t, expected, cfg.PollInterval)
})
t.Run("Invalid", func(t *testing.T) {
verifyArgsInvalid(
t,
"invalid value \"abc\" for flag -http-poll-interval",
addRequiredArgs(config.TraceTypeAlphabet, "--http-poll-interval", "abc"))
})
}
func TestCannonBin(t *testing.T) { func TestCannonBin(t *testing.T) {
t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) {
configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-bin")) configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-bin"))
......
...@@ -78,6 +78,7 @@ func ValidTraceType(value TraceType) bool { ...@@ -78,6 +78,7 @@ func ValidTraceType(value TraceType) bool {
} }
const ( const (
DefaultPollInterval = time.Second * 12
DefaultCannonSnapshotFreq = uint(1_000_000_000) DefaultCannonSnapshotFreq = uint(1_000_000_000)
DefaultCannonInfoFreq = uint(10_000_000) DefaultCannonInfoFreq = uint(10_000_000)
// DefaultGameWindow is the default maximum time duration in the past // DefaultGameWindow is the default maximum time duration in the past
...@@ -98,6 +99,7 @@ type Config struct { ...@@ -98,6 +99,7 @@ type Config struct {
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
Datadir string // Data Directory Datadir string // Data Directory
MaxConcurrency uint // Maximum number of threads to use when progressing games MaxConcurrency uint // Maximum number of threads to use when progressing games
PollInterval time.Duration // Polling interval for latest-block subscription when using an HTTP RPC provider
TraceType TraceType // Type of trace TraceType TraceType // Type of trace
...@@ -131,6 +133,7 @@ func NewConfig( ...@@ -131,6 +133,7 @@ func NewConfig(
L1EthRpc: l1EthRpc, L1EthRpc: l1EthRpc,
GameFactoryAddress: gameFactoryAddress, GameFactoryAddress: gameFactoryAddress,
MaxConcurrency: uint(runtime.NumCPU()), MaxConcurrency: uint(runtime.NumCPU()),
PollInterval: DefaultPollInterval,
AgreeWithProposedOutput: agreeWithProposedOutput, AgreeWithProposedOutput: agreeWithProposedOutput,
......
...@@ -118,6 +118,13 @@ func TestMaxConcurrency(t *testing.T) { ...@@ -118,6 +118,13 @@ func TestMaxConcurrency(t *testing.T) {
}) })
} }
func TestHttpPollInterval(t *testing.T) {
t.Run("Default", func(t *testing.T) {
config := validConfig(TraceTypeAlphabet)
require.EqualValues(t, DefaultPollInterval, config.PollInterval)
})
}
func TestCannonL2Required(t *testing.T) { func TestCannonL2Required(t *testing.T) {
config := validConfig(TraceTypeCannon) config := validConfig(TraceTypeCannon)
config.CannonL2 = "" config.CannonL2 = ""
......
...@@ -70,6 +70,12 @@ var ( ...@@ -70,6 +70,12 @@ var (
EnvVars: prefixEnvVars("MAX_CONCURRENCY"), EnvVars: prefixEnvVars("MAX_CONCURRENCY"),
Value: uint(runtime.NumCPU()), Value: uint(runtime.NumCPU()),
} }
HTTPPollInterval = &cli.DurationFlag{
Name: "http-poll-interval",
Usage: "Polling interval for latest-block subscription when using an HTTP RPC provider.",
EnvVars: prefixEnvVars("HTTP_POLL_INTERVAL"),
Value: config.DefaultPollInterval,
}
AlphabetFlag = &cli.StringFlag{ AlphabetFlag = &cli.StringFlag{
Name: "alphabet", Name: "alphabet",
Usage: "Correct Alphabet Trace (alphabet trace type only)", Usage: "Correct Alphabet Trace (alphabet trace type only)",
...@@ -142,6 +148,7 @@ var requiredFlags = []cli.Flag{ ...@@ -142,6 +148,7 @@ var requiredFlags = []cli.Flag{
// optionalFlags is a list of unchecked cli flags // optionalFlags is a list of unchecked cli flags
var optionalFlags = []cli.Flag{ var optionalFlags = []cli.Flag{
MaxConcurrencyFlag, MaxConcurrencyFlag,
HTTPPollInterval,
AlphabetFlag, AlphabetFlag,
GameAllowlistFlag, GameAllowlistFlag,
CannonNetworkFlag, CannonNetworkFlag,
...@@ -247,6 +254,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { ...@@ -247,6 +254,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
GameAllowlist: allowedGames, GameAllowlist: allowedGames,
GameWindow: ctx.Duration(GameWindowFlag.Name), GameWindow: ctx.Duration(GameWindowFlag.Name),
MaxConcurrency: maxConcurrency, MaxConcurrency: maxConcurrency,
PollInterval: ctx.Duration(HTTPPollInterval.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),
......
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -66,7 +67,12 @@ func (a *Agent) Act(ctx context.Context) error { ...@@ -66,7 +67,12 @@ func (a *Agent) Act(ctx context.Context) error {
// Perform the actions // Perform the actions
for _, action := range actions { for _, action := range actions {
log := a.log.New("action", action.Type, "is_attack", action.IsAttack, "parent", action.ParentIdx, "value", action.Value) log := a.log.New("action", action.Type, "is_attack", action.IsAttack, "parent", action.ParentIdx)
if action.Type == types.ActionTypeStep {
log = log.New("prestate", common.Bytes2Hex(action.PreState), "proof", common.Bytes2Hex(action.ProofData))
} else {
log = log.New("value", action.Value)
}
if action.OracleData != nil { if action.OracleData != nil {
a.log.Info("Updating oracle data", "oracleKey", action.OracleData.OracleKey, "oracleData", action.OracleData.OracleData) a.log.Info("Updating oracle data", "oracleKey", action.OracleData.OracleKey, "oracleData", action.OracleData.OracleData)
......
...@@ -9,7 +9,12 @@ import ( ...@@ -9,7 +9,12 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -32,6 +37,20 @@ type gameMonitor struct { ...@@ -32,6 +37,20 @@ type gameMonitor struct {
gameWindow time.Duration gameWindow time.Duration
fetchBlockNumber blockNumberFetcher fetchBlockNumber blockNumberFetcher
allowedGames []common.Address allowedGames []common.Address
l1HeadsSub ethereum.Subscription
l1Source *headSource
}
type MinimalSubscriber interface {
EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (ethereum.Subscription, error)
}
type headSource struct {
inner MinimalSubscriber
}
func (s *headSource) SubscribeNewHead(ctx context.Context, ch chan<- *ethTypes.Header) (ethereum.Subscription, error) {
return s.inner.EthSubscribe(ctx, ch, "newHeads")
} }
func newGameMonitor( func newGameMonitor(
...@@ -42,6 +61,7 @@ func newGameMonitor( ...@@ -42,6 +61,7 @@ func newGameMonitor(
gameWindow time.Duration, gameWindow time.Duration,
fetchBlockNumber blockNumberFetcher, fetchBlockNumber blockNumberFetcher,
allowedGames []common.Address, allowedGames []common.Address,
l1Source MinimalSubscriber,
) *gameMonitor { ) *gameMonitor {
return &gameMonitor{ return &gameMonitor{
logger: logger, logger: logger,
...@@ -51,6 +71,7 @@ func newGameMonitor( ...@@ -51,6 +71,7 @@ func newGameMonitor(
gameWindow: gameWindow, gameWindow: gameWindow,
fetchBlockNumber: fetchBlockNumber, fetchBlockNumber: fetchBlockNumber,
allowedGames: allowedGames, allowedGames: allowedGames,
l1Source: &headSource{inner: l1Source},
} }
} }
...@@ -99,29 +120,32 @@ func (m *gameMonitor) progressGames(ctx context.Context, blockNum uint64) error ...@@ -99,29 +120,32 @@ func (m *gameMonitor) progressGames(ctx context.Context, blockNum uint64) error
return nil return nil
} }
func (m *gameMonitor) MonitorGames(ctx context.Context) error { func (m *gameMonitor) onNewL1Head(ctx context.Context, sig eth.L1BlockRef) {
m.logger.Info("Monitoring fault dispute games") if err := m.progressGames(ctx, sig.Number); err != nil {
m.logger.Error("Failed to progress games", "err", err)
}
}
blockNum := uint64(0) func (m *gameMonitor) resubscribeFunction(ctx context.Context) event.ResubscribeErrFunc {
for { return func(innerCtx context.Context, err error) (event.Subscription, error) {
select {
case <-ctx.Done():
return ctx.Err()
default:
nextBlockNum, err := m.fetchBlockNumber(ctx)
if err != nil { if err != nil {
m.logger.Error("Failed to load current block number", "err", err) m.logger.Warn("resubscribing after failed L1 subscription", "err", err)
continue
}
if nextBlockNum > blockNum {
blockNum = nextBlockNum
if err := m.progressGames(ctx, nextBlockNum); err != nil {
m.logger.Error("Failed to progress games", "err", err)
} }
return eth.WatchHeadChanges(ctx, m.l1Source, m.onNewL1Head)
} }
if err := m.clock.SleepCtx(ctx, time.Second); err != nil { }
func (m *gameMonitor) MonitorGames(ctx context.Context) error {
m.l1HeadsSub = event.ResubscribeErr(time.Second*10, m.resubscribeFunction(ctx))
for {
select {
case <-ctx.Done():
return nil
case err, ok := <-m.l1HeadsSub.Err():
if !ok {
return err return err
} }
m.logger.Error("L1 subscription error", "err", err)
} }
} }
} }
...@@ -2,35 +2,40 @@ package game ...@@ -2,35 +2,40 @@ package game
import ( import (
"context" "context"
"fmt"
"math/big" "math/big"
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum/go-ethereum"
"github.com/ethereum-optimism/optimism/op-service/clock"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/clock"
) )
func TestMonitorMinGameTimestamp(t *testing.T) { func TestMonitorMinGameTimestamp(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("zero game window returns zero", func(t *testing.T) { t.Run("zero game window returns zero", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{}) monitor, _, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Duration(0) monitor.gameWindow = time.Duration(0)
require.Equal(t, monitor.minGameTimestamp(), uint64(0)) require.Equal(t, monitor.minGameTimestamp(), uint64(0))
}) })
t.Run("non-zero game window with zero clock", func(t *testing.T) { t.Run("non-zero game window with zero clock", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{}) monitor, _, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Minute monitor.gameWindow = time.Minute
monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0)) monitor.clock = clock.NewDeterministicClock(time.Unix(0, 0))
require.Equal(t, monitor.minGameTimestamp(), uint64(0)) require.Equal(t, monitor.minGameTimestamp(), uint64(0))
}) })
t.Run("minimum computed correctly", func(t *testing.T) { t.Run("minimum computed correctly", func(t *testing.T) {
monitor, _, _ := setupMonitorTest(t, []common.Address{}) monitor, _, _, _ := setupMonitorTest(t, []common.Address{})
monitor.gameWindow = time.Minute monitor.gameWindow = time.Minute
frozen := time.Unix(int64(time.Hour.Seconds()), 0) frozen := time.Unix(int64(time.Hour.Seconds()), 0)
monitor.clock = clock.NewDeterministicClock(frozen) monitor.clock = clock.NewDeterministicClock(frozen)
...@@ -39,29 +44,95 @@ func TestMonitorMinGameTimestamp(t *testing.T) { ...@@ -39,29 +44,95 @@ func TestMonitorMinGameTimestamp(t *testing.T) {
}) })
} }
func TestMonitorExitsWhenContextDone(t *testing.T) { // TestMonitorGames tests that the monitor can handle a new head event
monitor, _, _ := setupMonitorTest(t, []common.Address{{}}) // and resubscribe to new heads if the subscription errors.
func TestMonitorGames(t *testing.T) {
t.Run("Schedules games", func(t *testing.T) {
addr1 := common.Address{0xaa}
addr2 := common.Address{0xbb}
monitor, source, sched, mockHeadSource := setupMonitorTest(t, []common.Address{})
source.games = []FaultDisputeGame{newFDG(addr1, 9999), newFDG(addr2, 9999)}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
headerNotSent := true
waitErr := wait.For(context.Background(), 100*time.Millisecond, func() (bool, error) {
if len(sched.scheduled) >= 1 {
return true, nil
}
if mockHeadSource.sub == nil {
return false, nil
}
if headerNotSent {
mockHeadSource.sub.headers <- &ethtypes.Header{
Number: big.NewInt(1),
}
headerNotSent = false
}
return false, nil
})
require.NoError(t, waitErr)
mockHeadSource.err = fmt.Errorf("eth subscribe test error")
cancel() cancel()
}()
err := monitor.MonitorGames(ctx) err := monitor.MonitorGames(ctx)
require.ErrorIs(t, err, context.Canceled) require.NoError(t, err)
require.Len(t, sched.scheduled, 1)
require.Equal(t, []common.Address{addr1, addr2}, sched.scheduled[0])
})
t.Run("Resubscribes on error", func(t *testing.T) {
addr1 := common.Address{0xaa}
addr2 := common.Address{0xbb}
monitor, source, sched, mockHeadSource := setupMonitorTest(t, []common.Address{})
source.games = []FaultDisputeGame{newFDG(addr1, 9999), newFDG(addr2, 9999)}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
headerNotSent := true
waitErr := wait.For(context.Background(), 100*time.Millisecond, func() (bool, error) {
return mockHeadSource.sub != nil, nil
})
require.NoError(t, waitErr)
mockHeadSource.sub.errChan <- fmt.Errorf("test error")
waitErr = wait.For(context.Background(), 100*time.Millisecond, func() (bool, error) {
if len(sched.scheduled) >= 1 {
return true, nil
}
if mockHeadSource.sub == nil {
return false, nil
}
if headerNotSent {
mockHeadSource.sub.headers <- &ethtypes.Header{
Number: big.NewInt(1),
}
headerNotSent = false
}
return false, nil
})
require.NoError(t, waitErr)
mockHeadSource.err = fmt.Errorf("eth subscribe test error")
cancel()
}()
err := monitor.MonitorGames(ctx)
require.NoError(t, err)
require.Len(t, sched.scheduled, 1)
require.Equal(t, []common.Address{addr1, addr2}, sched.scheduled[0])
})
} }
func TestMonitorCreateAndProgressGameAgents(t *testing.T) { func TestMonitorCreateAndProgressGameAgents(t *testing.T) {
monitor, source, sched := setupMonitorTest(t, []common.Address{}) monitor, source, sched, _ := setupMonitorTest(t, []common.Address{})
addr1 := common.Address{0xaa} addr1 := common.Address{0xaa}
addr2 := common.Address{0xbb} addr2 := common.Address{0xbb}
source.games = []FaultDisputeGame{ source.games = []FaultDisputeGame{newFDG(addr1, 9999), newFDG(addr2, 9999)}
{
Proxy: addr1,
Timestamp: 9999,
},
{
Proxy: addr2,
Timestamp: 9999,
},
}
require.NoError(t, monitor.progressGames(context.Background(), uint64(1))) require.NoError(t, monitor.progressGames(context.Background(), uint64(1)))
...@@ -72,18 +143,8 @@ func TestMonitorCreateAndProgressGameAgents(t *testing.T) { ...@@ -72,18 +143,8 @@ func TestMonitorCreateAndProgressGameAgents(t *testing.T) {
func TestMonitorOnlyScheduleSpecifiedGame(t *testing.T) { func TestMonitorOnlyScheduleSpecifiedGame(t *testing.T) {
addr1 := common.Address{0xaa} addr1 := common.Address{0xaa}
addr2 := common.Address{0xbb} addr2 := common.Address{0xbb}
monitor, source, sched := setupMonitorTest(t, []common.Address{addr2}) monitor, source, sched, _ := setupMonitorTest(t, []common.Address{addr2})
source.games = []FaultDisputeGame{newFDG(addr1, 9999), newFDG(addr2, 9999)}
source.games = []FaultDisputeGame{
{
Proxy: addr1,
Timestamp: 9999,
},
{
Proxy: addr2,
Timestamp: 9999,
},
}
require.NoError(t, monitor.progressGames(context.Background(), uint64(1))) require.NoError(t, monitor.progressGames(context.Background(), uint64(1)))
...@@ -91,7 +152,17 @@ func TestMonitorOnlyScheduleSpecifiedGame(t *testing.T) { ...@@ -91,7 +152,17 @@ func TestMonitorOnlyScheduleSpecifiedGame(t *testing.T) {
require.Equal(t, []common.Address{addr2}, sched.scheduled[0]) require.Equal(t, []common.Address{addr2}, sched.scheduled[0])
} }
func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor, *stubGameSource, *stubScheduler) { func newFDG(proxy common.Address, timestamp uint64) FaultDisputeGame {
return FaultDisputeGame{
Proxy: proxy,
Timestamp: timestamp,
}
}
func setupMonitorTest(
t *testing.T,
allowedGames []common.Address,
) (*gameMonitor, *stubGameSource, *stubScheduler, *mockNewHeadSource) {
logger := testlog.Logger(t, log.LvlDebug) logger := testlog.Logger(t, log.LvlDebug)
source := &stubGameSource{} source := &stubGameSource{}
i := uint64(1) i := uint64(1)
...@@ -100,15 +171,58 @@ func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor ...@@ -100,15 +171,58 @@ func setupMonitorTest(t *testing.T, allowedGames []common.Address) (*gameMonitor
return i, nil return i, nil
} }
sched := &stubScheduler{} sched := &stubScheduler{}
monitor := newGameMonitor(logger, clock.SystemClock, source, sched, time.Duration(0), fetchBlockNum, allowedGames) mockHeadSource := &mockNewHeadSource{}
return monitor, source, sched monitor := newGameMonitor(
logger,
clock.SystemClock,
source,
sched,
time.Duration(0),
fetchBlockNum,
allowedGames,
mockHeadSource,
)
return monitor, source, sched, mockHeadSource
}
type mockNewHeadSource struct {
sub *mockSubscription
err error
}
func (m *mockNewHeadSource) EthSubscribe(
ctx context.Context,
ch any,
args ...any,
) (ethereum.Subscription, error) {
errChan := make(chan error)
m.sub = &mockSubscription{errChan, (ch).(chan<- *ethtypes.Header)}
if m.err != nil {
return nil, m.err
}
return m.sub, nil
}
type mockSubscription struct {
errChan chan error
headers chan<- *ethtypes.Header
}
func (m *mockSubscription) Unsubscribe() {}
func (m *mockSubscription) Err() <-chan error {
return m.errChan
} }
type stubGameSource struct { type stubGameSource struct {
games []FaultDisputeGame games []FaultDisputeGame
} }
func (s *stubGameSource) FetchAllGamesAtBlock(ctx context.Context, earliest uint64, 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
} }
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler"
"github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-challenger/version" "github.com/ethereum-optimism/optimism/op-challenger/version"
opClient "github.com/ethereum-optimism/optimism/op-node/client"
"github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/clock" "github.com/ethereum-optimism/optimism/op-service/clock"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof" oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
...@@ -34,7 +35,7 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se ...@@ -34,7 +35,7 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se
return nil, fmt.Errorf("failed to create the transaction manager: %w", err) return nil, fmt.Errorf("failed to create the transaction manager: %w", err)
} }
client, err := client.DialEthClientWithTimeout(client.DefaultDialTimeout, logger, cfg.L1EthRpc) l1Client, err := client.DialEthClientWithTimeout(client.DefaultDialTimeout, logger, cfg.L1EthRpc)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to dial L1: %w", err) return nil, fmt.Errorf("failed to dial L1: %w", err)
} }
...@@ -57,10 +58,10 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se ...@@ -57,10 +58,10 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se
logger.Error("error starting metrics server", "err", err) logger.Error("error starting metrics server", "err", err)
} }
}() }()
m.StartBalanceMetrics(ctx, logger, client, txMgr.From()) m.StartBalanceMetrics(ctx, logger, l1Client, txMgr.From())
} }
factory, err := bindings.NewDisputeGameFactory(cfg.GameFactoryAddress, client) factory, err := bindings.NewDisputeGameFactory(cfg.GameFactoryAddress, l1Client)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to bind the fault dispute game factory contract: %w", err) return nil, fmt.Errorf("failed to bind the fault dispute game factory contract: %w", err)
} }
...@@ -73,10 +74,14 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se ...@@ -73,10 +74,14 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*Se
disk, disk,
cfg.MaxConcurrency, cfg.MaxConcurrency,
func(addr common.Address, dir string) (scheduler.GamePlayer, error) { func(addr common.Address, dir string) (scheduler.GamePlayer, error) {
return fault.NewGamePlayer(ctx, logger, m, cfg, dir, addr, txMgr, client) return fault.NewGamePlayer(ctx, logger, m, cfg, dir, addr, txMgr, l1Client)
}) })
monitor := newGameMonitor(logger, cl, loader, sched, cfg.GameWindow, client.BlockNumber, cfg.GameAllowlist) pollClient, err := opClient.NewRPCWithClient(ctx, logger, cfg.L1EthRpc, opClient.NewBaseRPCClient(l1Client.Client()), cfg.PollInterval)
if err != nil {
return nil, fmt.Errorf("failed to create RPC client: %w", err)
}
monitor := newGameMonitor(logger, cl, loader, sched, cfg.GameWindow, l1Client.BlockNumber, cfg.GameAllowlist, pollClient)
m.RecordInfo(version.SimpleWithMeta) m.RecordInfo(version.SimpleWithMeta)
m.RecordUp() m.RecordUp()
......
...@@ -145,6 +145,10 @@ func NewChallengerConfig(t *testing.T, l1Endpoint string, options ...Option) *co ...@@ -145,6 +145,10 @@ func NewChallengerConfig(t *testing.T, l1Endpoint string, options ...Option) *co
_, err := os.Stat(cfg.CannonAbsolutePreState) _, err := os.Stat(cfg.CannonAbsolutePreState)
require.NoError(t, err, "cannon pre-state should be built. Make sure you've run make cannon-prestate") require.NoError(t, err, "cannon pre-state should be built. Make sure you've run make cannon-prestate")
} }
if cfg.PollInterval == 0 {
cfg.PollInterval = time.Second
}
return &cfg return &cfg
} }
......
...@@ -65,7 +65,7 @@ func (g *FaultGameHelper) MaxDepth(ctx context.Context) int64 { ...@@ -65,7 +65,7 @@ func (g *FaultGameHelper) MaxDepth(ctx context.Context) int64 {
} }
func (g *FaultGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) { func (g *FaultGameHelper) waitForClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) {
timedCtx, cancel := context.WithTimeout(ctx, time.Minute) timedCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel() defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) { err := wait.For(timedCtx, time.Second, func() (bool, error) {
count, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx}) count, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx})
...@@ -89,6 +89,31 @@ func (g *FaultGameHelper) waitForClaim(ctx context.Context, errorMsg string, pre ...@@ -89,6 +89,31 @@ func (g *FaultGameHelper) waitForClaim(ctx context.Context, errorMsg string, pre
} }
} }
func (g *FaultGameHelper) waitForNoClaim(ctx context.Context, errorMsg string, predicate func(claim ContractClaim) bool) {
timedCtx, cancel := context.WithTimeout(ctx, 3*time.Minute)
defer cancel()
err := wait.For(timedCtx, time.Second, func() (bool, error) {
count, err := g.game.ClaimDataLen(&bind.CallOpts{Context: timedCtx})
if err != nil {
return false, fmt.Errorf("retrieve number of claims: %w", err)
}
// Search backwards because the new claims are at the end and more likely the ones we will fail on.
for i := count.Int64() - 1; i >= 0; i-- {
claimData, err := g.game.ClaimData(&bind.CallOpts{Context: timedCtx}, big.NewInt(i))
if err != nil {
return false, fmt.Errorf("retrieve claim %v: %w", i, err)
}
if predicate(claimData) {
return false, nil
}
}
return true, nil
})
if err != nil { // Avoid waiting time capturing game data when there's no error
g.require.NoErrorf(err, "%v\n%v", errorMsg, g.gameData(ctx))
}
}
func (g *FaultGameHelper) GetClaimValue(ctx context.Context, claimIdx int64) common.Hash { func (g *FaultGameHelper) GetClaimValue(ctx context.Context, claimIdx int64) common.Hash {
g.WaitForClaimCount(ctx, claimIdx+1) g.WaitForClaimCount(ctx, claimIdx+1)
claim := g.getClaim(ctx, claimIdx) claim := g.getClaim(ctx, claimIdx)
...@@ -105,6 +130,16 @@ func (g *FaultGameHelper) getClaim(ctx context.Context, claimIdx int64) Contract ...@@ -105,6 +130,16 @@ func (g *FaultGameHelper) getClaim(ctx context.Context, claimIdx int64) Contract
return claimData return claimData
} }
func (g *FaultGameHelper) WaitForClaimAtDepth(ctx context.Context, depth int) {
g.waitForClaim(
ctx,
fmt.Sprintf("Could not find claim depth %v", depth),
func(claim ContractClaim) bool {
pos := types.NewPositionFromGIndex(claim.Position.Uint64())
return pos.Depth() == depth
})
}
func (g *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered bool) { func (g *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered bool) {
maxDepth := g.MaxDepth(ctx) maxDepth := g.MaxDepth(ctx)
g.waitForClaim( g.waitForClaim(
...@@ -116,6 +151,15 @@ func (g *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered ...@@ -116,6 +151,15 @@ func (g *FaultGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered
}) })
} }
func (g *FaultGameHelper) WaitForAllClaimsCountered(ctx context.Context) {
g.waitForNoClaim(
ctx,
"Did not find all claims countered",
func(claim ContractClaim) bool {
return !claim.Countered
})
}
func (g *FaultGameHelper) Resolve(ctx context.Context) { func (g *FaultGameHelper) Resolve(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, time.Minute) ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel() defer cancel()
......
...@@ -100,7 +100,7 @@ func (h *FactoryHelper) StartAlphabetGame(ctx context.Context, claimedAlphabet s ...@@ -100,7 +100,7 @@ func (h *FactoryHelper) StartAlphabetGame(ctx context.Context, claimedAlphabet s
l2BlockNumber := h.waitForProposals(ctx) l2BlockNumber := h.waitForProposals(ctx)
l1Head := h.checkpointL1Block(ctx) l1Head := h.checkpointL1Block(ctx)
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel() defer cancel()
trace := alphabet.NewTraceProvider(claimedAlphabet, alphabetGameDepth) trace := alphabet.NewTraceProvider(claimedAlphabet, alphabetGameDepth)
......
...@@ -341,6 +341,66 @@ func TestCannonProposedOutputRootInvalid(t *testing.T) { ...@@ -341,6 +341,66 @@ func TestCannonProposedOutputRootInvalid(t *testing.T) {
} }
} }
func TestCannonPoisonedPostState(t *testing.T) {
t.Skip("Known failure case")
InitParallel(t)
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
l1Endpoint := sys.NodeEndpoint("l1")
l2Endpoint := sys.NodeEndpoint("sequencer")
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
game, correctTrace := disputeGameFactory.StartCannonGameWithCorrectRoot(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint,
challenger.WithPrivKey(sys.cfg.Secrets.Mallory),
)
require.NotNil(t, game)
game.LogGameData(ctx)
// Honest first attack at "honest" level
correctTrace.Attack(ctx, 0)
// Honest defense at "dishonest" level
correctTrace.Defend(ctx, 1)
// Dishonest attack at "honest" level - honest move would be to defend
game.Attack(ctx, 2, common.Hash{0x03, 0xaa})
// Start the honest challenger
game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Honest",
// Agree with the proposed output, so disagree with the root claim
challenger.WithAgreeProposedOutput(true),
challenger.WithPrivKey(sys.cfg.Secrets.Bob),
)
// Start dishonest challenger that posts correct claims
game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "DishonestCorrect",
// Disagree with the proposed output, so agree with the root claim
challenger.WithAgreeProposedOutput(false),
challenger.WithPrivKey(sys.cfg.Secrets.Mallory),
)
// Give the challengers time to progress down the full game depth
depth := game.MaxDepth(ctx)
for i := 3; i <= int(depth); i++ {
game.WaitForClaimAtDepth(ctx, i)
game.LogGameData(ctx)
}
// Wait for all the leaf nodes to be countered
// Wait for the challengers to drive the game down to the leaf node which should be countered
game.WaitForAllClaimsCountered(ctx)
// Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins)
game.LogGameData(ctx)
}
// setupDisputeGameForInvalidOutputRoot sets up an L2 chain with at least one valid output root followed by an invalid output root. // setupDisputeGameForInvalidOutputRoot sets up an L2 chain with at least one valid output root followed by an invalid output root.
// A cannon dispute game is started to dispute the invalid output root with the correct root claim provided. // A cannon dispute game is started to dispute the invalid output root with the correct root claim provided.
// An honest challenger is run to defend the root claim (ie disagree with the invalid output root). // An honest challenger is run to defend the root claim (ie disagree with the invalid output root).
......
...@@ -80,9 +80,11 @@ func NewRPC(ctx context.Context, lgr log.Logger, addr string, opts ...RPCOption) ...@@ -80,9 +80,11 @@ func NewRPC(ctx context.Context, lgr log.Logger, addr string, opts ...RPCOption)
return nil, fmt.Errorf("rpc option %d failed to apply to RPC config: %w", i, err) return nil, fmt.Errorf("rpc option %d failed to apply to RPC config: %w", i, err)
} }
} }
if cfg.backoffAttempts < 1 { // default to at least 1 attempt, or it always fails to dial. if cfg.backoffAttempts < 1 { // default to at least 1 attempt, or it always fails to dial.
cfg.backoffAttempts = 1 cfg.backoffAttempts = 1
} }
underlying, err := dialRPCClientWithBackoff(ctx, lgr, addr, cfg.backoffAttempts, cfg.gethRPCOptions...) underlying, err := dialRPCClientWithBackoff(ctx, lgr, addr, cfg.backoffAttempts, cfg.gethRPCOptions...)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -94,11 +96,15 @@ func NewRPC(ctx context.Context, lgr log.Logger, addr string, opts ...RPCOption) ...@@ -94,11 +96,15 @@ func NewRPC(ctx context.Context, lgr log.Logger, addr string, opts ...RPCOption)
wrapped = NewRateLimitingClient(wrapped, rate.Limit(cfg.limit), cfg.burst) wrapped = NewRateLimitingClient(wrapped, rate.Limit(cfg.limit), cfg.burst)
} }
return NewRPCWithClient(ctx, lgr, addr, wrapped, cfg.httpPollInterval)
}
// NewRPCWithClient builds a new polling client with the given underlying RPC client.
func NewRPCWithClient(ctx context.Context, lgr log.Logger, addr string, underlying RPC, pollInterval time.Duration) (RPC, error) {
if httpRegex.MatchString(addr) { if httpRegex.MatchString(addr) {
wrapped = NewPollingClient(ctx, lgr, wrapped, WithPollRate(cfg.httpPollInterval)) underlying = NewPollingClient(ctx, lgr, underlying, WithPollRate(pollInterval))
} }
return underlying, nil
return wrapped, nil
} }
// Dials a JSON-RPC endpoint repeatedly, with a backoff, until a client connection is established. Auth is optional. // Dials a JSON-RPC endpoint repeatedly, with a backoff, until a client connection is established. Auth is optional.
......
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