Commit f930f68d authored by refcell's avatar refcell Committed by GitHub

chore(op-dispute-mon): Detector Type Refactor (#9443)

* chore(op-dispute-mon): refactor types and unit tests

* chore(op-dispute-mon): refactor output validation into a separate component

---------
Co-authored-by: default avatarAdrian Sutton <adrian@oplabs.co>
parent b6271516
...@@ -5,45 +5,13 @@ import ( ...@@ -5,45 +5,13 @@ import (
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
type statusBatch struct { type OutputValidator interface {
inProgress, defenderWon, challengerWon int CheckRootAgreement(ctx context.Context, blockNum uint64, root common.Hash) (bool, common.Hash, error)
}
func (s *statusBatch) Add(status types.GameStatus) {
switch status {
case types.GameStatusInProgress:
s.inProgress++
case types.GameStatusDefenderWon:
s.defenderWon++
case types.GameStatusChallengerWon:
s.challengerWon++
}
}
type detectionBatch struct {
inProgress int
agreeDefenderWins int
disagreeDefenderWins int
agreeChallengerWins int
disagreeChallengerWins int
}
func (d *detectionBatch) merge(other detectionBatch) {
d.inProgress += other.inProgress
d.agreeDefenderWins += other.agreeDefenderWins
d.disagreeDefenderWins += other.disagreeDefenderWins
d.agreeChallengerWins += other.agreeChallengerWins
d.disagreeChallengerWins += other.disagreeChallengerWins
}
type OutputRollupClient interface {
OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error)
} }
type MetadataCreator interface { type MetadataCreator interface {
...@@ -56,18 +24,18 @@ type DetectorMetrics interface { ...@@ -56,18 +24,18 @@ type DetectorMetrics interface {
} }
type detector struct { type detector struct {
logger log.Logger logger log.Logger
metrics DetectorMetrics metrics DetectorMetrics
creator MetadataCreator creator MetadataCreator
outputClient OutputRollupClient validator OutputValidator
} }
func newDetector(logger log.Logger, metrics DetectorMetrics, creator MetadataCreator, outputClient OutputRollupClient) *detector { func newDetector(logger log.Logger, metrics DetectorMetrics, creator MetadataCreator, validator OutputValidator) *detector {
return &detector{ return &detector{
logger: logger, logger: logger,
metrics: metrics, metrics: metrics,
creator: creator, creator: creator,
outputClient: outputClient, validator: validator,
} }
} }
...@@ -88,7 +56,7 @@ func (d *detector) Detect(ctx context.Context, games []types.GameMetadata) { ...@@ -88,7 +56,7 @@ func (d *detector) Detect(ctx context.Context, games []types.GameMetadata) {
d.logger.Error("Failed to process game", "err", err) d.logger.Error("Failed to process game", "err", err)
continue continue
} }
detectBatch.merge(processed) detectBatch.Merge(processed)
} }
d.metrics.RecordGamesStatus(statBatch.inProgress, statBatch.defenderWon, statBatch.challengerWon) d.metrics.RecordGamesStatus(statBatch.inProgress, statBatch.defenderWon, statBatch.challengerWon)
d.recordBatch(detectBatch) d.recordBatch(detectBatch)
...@@ -116,36 +84,17 @@ func (d *detector) fetchGameMetadata(ctx context.Context, game types.GameMetadat ...@@ -116,36 +84,17 @@ func (d *detector) fetchGameMetadata(ctx context.Context, game types.GameMetadat
} }
func (d *detector) checkAgreement(ctx context.Context, addr common.Address, blockNum uint64, rootClaim common.Hash, status types.GameStatus) (detectionBatch, error) { func (d *detector) checkAgreement(ctx context.Context, addr common.Address, blockNum uint64, rootClaim common.Hash, status types.GameStatus) (detectionBatch, error) {
agree, err := d.checkRootAgreement(ctx, blockNum, rootClaim) agree, expected, err := d.validator.CheckRootAgreement(ctx, blockNum, rootClaim)
if err != nil { if err != nil {
return detectionBatch{}, err return detectionBatch{}, err
} }
batch := detectionBatch{} batch := detectionBatch{}
switch status { batch.Update(status, agree)
case types.GameStatusInProgress: if !agree && status == types.GameStatusChallengerWon {
batch.inProgress++ d.logger.Error("Challenger won but expected defender to win", "gameAddr", addr, "rootClaim", rootClaim, "expected", expected)
case types.GameStatusDefenderWon:
if agree {
batch.agreeDefenderWins++
} else {
batch.disagreeDefenderWins++
d.logger.Error("Defender won but root claim does not match", "gameAddr", addr, "rootClaim", rootClaim)
}
case types.GameStatusChallengerWon:
if agree {
batch.agreeChallengerWins++
} else {
batch.disagreeChallengerWins++
d.logger.Error("Challenger won but root claim does not match", "gameAddr", addr, "rootClaim", rootClaim)
}
} }
return batch, nil if !agree && status == types.GameStatusDefenderWon {
} d.logger.Error("Defender won but expected challenger to win", "gameAddr", addr, "rootClaim", rootClaim, "expected", expected)
func (d *detector) checkRootAgreement(ctx context.Context, blockNum uint64, rootClaim common.Hash) (bool, error) {
output, err := d.outputClient.OutputAtBlock(ctx, blockNum)
if err != nil {
return false, fmt.Errorf("failed to get output at block: %w", err)
} }
return rootClaim == common.Hash(output.OutputRoot), nil return batch, nil
} }
...@@ -6,17 +6,12 @@ import ( ...@@ -6,17 +6,12 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var (
mockRootClaim = common.HexToHash("0x10")
)
func TestDetector_Detect(t *testing.T) { func TestDetector_Detect(t *testing.T) {
t.Parallel() t.Parallel()
...@@ -213,50 +208,25 @@ func TestDetector_CheckAgreement_Succeeds(t *testing.T) { ...@@ -213,50 +208,25 @@ func TestDetector_CheckAgreement_Succeeds(t *testing.T) {
} }
} }
func TestDetector_CheckRootAgreement(t *testing.T) { func setupDetectorTest(t *testing.T) (*detector, *mockDetectorMetricer, *mockMetadataCreator, *stubOutputValidator) {
t.Parallel()
t.Run("OutputFetchFails", func(t *testing.T) {
detector, _, _, rollup := setupDetectorTest(t)
rollup.err = errors.New("boom")
agree, err := detector.checkRootAgreement(context.Background(), 0, mockRootClaim)
require.ErrorIs(t, err, rollup.err)
require.False(t, agree)
})
t.Run("OutputMismatch", func(t *testing.T) {
detector, _, _, _ := setupDetectorTest(t)
agree, err := detector.checkRootAgreement(context.Background(), 0, common.Hash{})
require.NoError(t, err)
require.False(t, agree)
})
t.Run("OutputMatches", func(t *testing.T) {
detector, _, _, _ := setupDetectorTest(t)
agree, err := detector.checkRootAgreement(context.Background(), 0, mockRootClaim)
require.NoError(t, err)
require.True(t, agree)
})
}
func setupDetectorTest(t *testing.T) (*detector, *mockDetectorMetricer, *mockMetadataCreator, *stubRollupClient) {
logger := testlog.Logger(t, log.LvlDebug) logger := testlog.Logger(t, log.LvlDebug)
metrics := &mockDetectorMetricer{} metrics := &mockDetectorMetricer{}
loader := &mockMetadataLoader{} loader := &mockMetadataLoader{}
creator := &mockMetadataCreator{loader: loader} creator := &mockMetadataCreator{loader: loader}
rollupClient := &stubRollupClient{} validator := &stubOutputValidator{}
detector := newDetector(logger, metrics, creator, rollupClient) detector := newDetector(logger, metrics, creator, validator)
return detector, metrics, creator, rollupClient return detector, metrics, creator, validator
} }
type stubRollupClient struct { type stubOutputValidator struct {
blockNum uint64 err error
err error
} }
func (s *stubRollupClient) OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error) { func (s *stubOutputValidator) CheckRootAgreement(ctx context.Context, blockNum uint64, rootClaim common.Hash) (bool, common.Hash, error) {
s.blockNum = blockNum if s.err != nil {
return &eth.OutputResponse{OutputRoot: eth.Bytes32(mockRootClaim)}, s.err return false, common.Hash{}, s.err
}
return rootClaim == mockRootClaim, mockRootClaim, nil
} }
type mockMetadataCreator struct { type mockMetadataCreator struct {
......
...@@ -37,6 +37,7 @@ type Service struct { ...@@ -37,6 +37,7 @@ type Service struct {
metadata *metadataCreator metadata *metadataCreator
rollupClient *sources.RollupClient rollupClient *sources.RollupClient
detector *detector detector *detector
validator *outputValidator
l1Client *ethclient.Client l1Client *ethclient.Client
...@@ -78,6 +79,8 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -78,6 +79,8 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return fmt.Errorf("failed to init rollup client: %w", err) return fmt.Errorf("failed to init rollup client: %w", err)
} }
s.initMetadataCreator() s.initMetadataCreator()
s.initOutputValidator()
s.initDetector()
s.initDetector() s.initDetector()
s.initMonitor(ctx, cfg) s.initMonitor(ctx, cfg)
...@@ -87,8 +90,12 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error ...@@ -87,8 +90,12 @@ func (s *Service) initFromConfig(ctx context.Context, cfg *config.Config) error
return nil return nil
} }
func (s *Service) initOutputValidator() {
s.validator = newOutputValidator(s.rollupClient)
}
func (s *Service) initDetector() { func (s *Service) initDetector() {
s.detector = newDetector(s.logger, s.metrics, s.metadata, s.rollupClient) s.detector = newDetector(s.logger, s.metrics, s.metadata, s.validator)
} }
func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error { func (s *Service) initOutputRollupClient(ctx context.Context, cfg *config.Config) error {
......
package mon
import (
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
)
type statusBatch struct {
inProgress, defenderWon, challengerWon int
}
func (s *statusBatch) Add(status types.GameStatus) {
switch status {
case types.GameStatusInProgress:
s.inProgress++
case types.GameStatusDefenderWon:
s.defenderWon++
case types.GameStatusChallengerWon:
s.challengerWon++
}
}
type detectionBatch struct {
inProgress int
agreeDefenderWins int
disagreeDefenderWins int
agreeChallengerWins int
disagreeChallengerWins int
}
func (d *detectionBatch) Update(status types.GameStatus, agree bool) {
switch status {
case types.GameStatusInProgress:
d.inProgress++
case types.GameStatusDefenderWon:
if agree {
d.agreeDefenderWins++
} else {
d.disagreeDefenderWins++
}
case types.GameStatusChallengerWon:
if agree {
d.agreeChallengerWins++
} else {
d.disagreeChallengerWins++
}
}
}
func (d *detectionBatch) Merge(other detectionBatch) {
d.inProgress += other.inProgress
d.agreeDefenderWins += other.agreeDefenderWins
d.disagreeDefenderWins += other.disagreeDefenderWins
d.agreeChallengerWins += other.agreeChallengerWins
d.disagreeChallengerWins += other.disagreeChallengerWins
}
package mon
import (
"fmt"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/stretchr/testify/require"
)
func TestStatusBatch_Add(t *testing.T) {
statusExpectations := []struct {
status types.GameStatus
create func(int) statusBatch
}{
{
status: types.GameStatusInProgress,
create: func(inProgress int) statusBatch {
return statusBatch{inProgress, 0, 0}
},
},
{
status: types.GameStatusDefenderWon,
create: func(defenderWon int) statusBatch {
return statusBatch{0, defenderWon, 0}
},
},
{
status: types.GameStatusChallengerWon,
create: func(challengerWon int) statusBatch {
return statusBatch{0, 0, challengerWon}
},
},
}
type test struct {
name string
status types.GameStatus
invocations int
expected statusBatch
}
var tests []test
for i := 0; i < 100; i++ {
for _, exp := range statusExpectations {
tests = append(tests, test{
name: fmt.Sprintf("Invocation-%d", i),
status: exp.status,
invocations: i,
expected: exp.create(i),
})
}
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
s := statusBatch{}
for i := 0; i < test.invocations; i++ {
s.Add(test.status)
}
require.Equal(t, test.expected, s)
})
}
}
func TestDetectionBatch_Update(t *testing.T) {
statusExpectations := []struct {
status types.GameStatus
create func(int, bool) detectionBatch
}{
{
status: types.GameStatusInProgress,
create: func(inProgress int, _ bool) detectionBatch {
return detectionBatch{inProgress, 0, 0, 0, 0}
},
},
{
status: types.GameStatusDefenderWon,
create: func(defenderWon int, agree bool) detectionBatch {
if agree {
return detectionBatch{0, defenderWon, 0, 0, 0}
}
return detectionBatch{0, 0, defenderWon, 0, 0}
},
},
{
status: types.GameStatusChallengerWon,
create: func(challengerWon int, agree bool) detectionBatch {
if agree {
return detectionBatch{0, 0, 0, challengerWon, 0}
}
return detectionBatch{0, 0, 0, 0, challengerWon}
},
},
}
type test struct {
name string
status types.GameStatus
agree bool
invocations int
expected detectionBatch
}
var tests []test
for i := 0; i < 100; i++ {
for _, exp := range statusExpectations {
agree := i%2 == 0
tests = append(tests, test{
name: fmt.Sprintf("Invocation-%d", i),
status: exp.status,
agree: agree,
invocations: i,
expected: exp.create(i, agree),
})
}
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
d := detectionBatch{}
for i := 0; i < test.invocations; i++ {
d.Update(test.status, test.agree)
}
require.Equal(t, test.expected, d)
})
}
}
func TestDetectionBatch_Merge(t *testing.T) {
type test struct {
name string
merge detectionBatch
expected detectionBatch
}
tests := []test{
{
name: "Empty",
merge: detectionBatch{},
expected: detectionBatch{},
},
{
name: "InProgress",
merge: detectionBatch{1, 0, 0, 0, 0},
expected: detectionBatch{1, 0, 0, 0, 0},
},
{
name: "AgreeDefenderWins",
merge: detectionBatch{0, 1, 0, 0, 0},
expected: detectionBatch{0, 1, 0, 0, 0},
},
{
name: "DisagreeDefenderWins",
merge: detectionBatch{0, 0, 1, 0, 0},
expected: detectionBatch{0, 0, 1, 0, 0},
},
{
name: "AgreeChallengerWins",
merge: detectionBatch{0, 0, 0, 1, 0},
expected: detectionBatch{0, 0, 0, 1, 0},
},
{
name: "DisagreeChallengerWins",
merge: detectionBatch{0, 0, 0, 0, 1},
expected: detectionBatch{0, 0, 0, 0, 1},
},
{
name: "All",
merge: detectionBatch{1, 1, 1, 1, 1},
expected: detectionBatch{1, 1, 1, 1, 1},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
d := detectionBatch{}
d.Merge(test.merge)
require.Equal(t, test.expected, d)
})
}
}
package mon
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-service/eth"
)
type OutputRollupClient interface {
OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error)
}
type outputValidator struct {
client OutputRollupClient
}
func newOutputValidator(client OutputRollupClient) *outputValidator {
return &outputValidator{
client: client,
}
}
// CheckRootAgreement validates the specified root claim against the output at the given block number.
func (o *outputValidator) CheckRootAgreement(ctx context.Context, blockNum uint64, rootClaim common.Hash) (bool, common.Hash, error) {
output, err := o.client.OutputAtBlock(ctx, blockNum)
if err != nil {
return false, common.Hash{}, fmt.Errorf("failed to get output at block: %w", err)
}
expected := common.Hash(output.OutputRoot)
return rootClaim == expected, expected, nil
}
package mon
import (
"context"
"errors"
"testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
var (
mockRootClaim = common.HexToHash("0x10")
)
func TestDetector_CheckRootAgreement(t *testing.T) {
t.Parallel()
t.Run("OutputFetchFails", func(t *testing.T) {
validator, rollup := setupOutputValidatorTest(t)
rollup.err = errors.New("boom")
agree, fetched, err := validator.CheckRootAgreement(context.Background(), 0, mockRootClaim)
require.ErrorIs(t, err, rollup.err)
require.Equal(t, common.Hash{}, fetched)
require.False(t, agree)
})
t.Run("OutputMismatch", func(t *testing.T) {
validator, _ := setupOutputValidatorTest(t)
agree, fetched, err := validator.CheckRootAgreement(context.Background(), 0, common.Hash{})
require.NoError(t, err)
require.Equal(t, mockRootClaim, fetched)
require.False(t, agree)
})
t.Run("OutputMatches", func(t *testing.T) {
validator, _ := setupOutputValidatorTest(t)
agree, fetched, err := validator.CheckRootAgreement(context.Background(), 0, mockRootClaim)
require.NoError(t, err)
require.Equal(t, mockRootClaim, fetched)
require.True(t, agree)
})
}
func setupOutputValidatorTest(t *testing.T) (*outputValidator, *stubRollupClient) {
client := &stubRollupClient{}
validator := newOutputValidator(client)
return validator, client
}
type stubRollupClient struct {
blockNum uint64
err error
}
func (s *stubRollupClient) OutputAtBlock(ctx context.Context, blockNum uint64) (*eth.OutputResponse, error) {
s.blockNum = blockNum
return &eth.OutputResponse{OutputRoot: eth.Bytes32(mockRootClaim)}, s.err
}
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