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

feat(op-dispute-mon): L2BlockNumberChallenged Support (#10451)

* feat(op-dispute-mon): L2BlockNumberChallenged dispute monitor support

feat(op-dispute-mon): query for the l2 block number through the game metadata call

fix(op-dispute-mon): query for the l2 block number through the game metadata call

fix(op-dispute-mon): query for the l2 block number through the game metadata call

* fix(op-dispute-mon): block challenge check

* fix(op-dispute-mon): use agreement and metrice

* op-dispute-mon: Separate l2 challenge metric (#10483)

* op-dispute-mon: Consider l2 block number challenged when forecasting but don't make it a new game status.

Add a separate metric to report the number of successful L2 block number challenges.

* op-challenger: Remove unused blockNumChallenged field from list-games info struct

* op-dispute-mon: Consider l2 block challenger in expected credits. (#10484)

* feat(op-dispute-mon): Update L2 Challenges Metrics (#10492)

* fix: change the l2 challenges to tha gauge vec

* fix: consistent metric labels

---------
Co-authored-by: default avatarrefcell <abigger87@gmail.com>

---------
Co-authored-by: default avatarAdrian Sutton <adrian@oplabs.co>
parent bbc3786d
......@@ -109,14 +109,14 @@ func listGames(ctx context.Context, caller *batching.MultiCaller, factory *contr
wg.Add(1)
go func() {
defer wg.Done()
_, l2BlockNum, rootClaim, status, _, err := gameContract.GetGameMetadata(ctx, rpcblock.ByHash(block))
metadata, err := gameContract.GetGameMetadata(ctx, rpcblock.ByHash(block))
if err != nil {
info.err = fmt.Errorf("failed to retrieve metadata for game %v: %w", gameProxy, err)
return
}
infos[currIndex].status = status
infos[currIndex].l2BlockNum = l2BlockNum
infos[currIndex].rootClaim = rootClaim
infos[currIndex].status = metadata.Status
infos[currIndex].l2BlockNum = metadata.L2BlockNum
infos[currIndex].rootClaim = metadata.RootClaim
claimCount, err := gameContract.GetClaimCount(ctx)
if err != nil {
info.err = fmt.Errorf("failed to retrieve claim count for game %v: %w", gameProxy, err)
......
......@@ -52,6 +52,7 @@ var (
methodCredit = "credit"
methodWETH = "weth"
methodL2BlockNumberChallenged = "l2BlockNumberChallenged"
methodL2BlockNumberChallenger = "l2BlockNumberChallenger"
methodChallengeRootL2Block = "challengeRootL2Block"
)
......@@ -162,30 +163,53 @@ func (f *FaultDisputeGameContractLatest) GetBlockRange(ctx context.Context) (pre
return
}
// GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, and max clock duration.
func (f *FaultDisputeGameContractLatest) GetGameMetadata(ctx context.Context, block rpcblock.Block) (common.Hash, uint64, common.Hash, gameTypes.GameStatus, uint64, error) {
type GameMetadata struct {
L1Head common.Hash
L2BlockNum uint64
RootClaim common.Hash
Status gameTypes.GameStatus
MaxClockDuration uint64
L2BlockNumberChallenged bool
L2BlockNumberChallenger common.Address
}
// GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, max clock duration, and is l2 block number challenged.
func (f *FaultDisputeGameContractLatest) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) {
defer f.metrics.StartContractRequest("GetGameMetadata")()
results, err := f.multiCaller.Call(ctx, block,
f.contract.Call(methodL1Head),
f.contract.Call(methodL2BlockNumber),
f.contract.Call(methodRootClaim),
f.contract.Call(methodStatus),
f.contract.Call(methodMaxClockDuration))
f.contract.Call(methodMaxClockDuration),
f.contract.Call(methodL2BlockNumberChallenged),
f.contract.Call(methodL2BlockNumberChallenger),
)
if err != nil {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("failed to retrieve game metadata: %w", err)
return GameMetadata{}, fmt.Errorf("failed to retrieve game metadata: %w", err)
}
if len(results) != 5 {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("expected 3 results but got %v", len(results))
if len(results) != 7 {
return GameMetadata{}, fmt.Errorf("expected 6 results but got %v", len(results))
}
l1Head := results[0].GetHash(0)
l2BlockNumber := results[1].GetBigInt(0).Uint64()
rootClaim := results[2].GetHash(0)
status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0))
if err != nil {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("failed to convert game status: %w", err)
return GameMetadata{}, fmt.Errorf("failed to convert game status: %w", err)
}
duration := results[4].GetUint64(0)
return l1Head, l2BlockNumber, rootClaim, status, duration, nil
blockChallenged := results[5].GetBool(0)
blockChallenger := results[6].GetAddress(0)
return GameMetadata{
L1Head: l1Head,
L2BlockNum: l2BlockNumber,
RootClaim: rootClaim,
Status: status,
MaxClockDuration: duration,
L2BlockNumberChallenged: blockChallenged,
L2BlockNumberChallenger: blockChallenger,
}, nil
}
func (f *FaultDisputeGameContractLatest) GetStartingRootHash(ctx context.Context) (common.Hash, error) {
......@@ -549,7 +573,7 @@ func (f *FaultDisputeGameContractLatest) decodeClaim(result *batching.CallResult
type FaultDisputeGameContract interface {
GetBalance(ctx context.Context, block rpcblock.Block) (*big.Int, common.Address, error)
GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error)
GetGameMetadata(ctx context.Context, block rpcblock.Block) (common.Hash, uint64, common.Hash, gameTypes.GameStatus, uint64, error)
GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error)
GetStartingRootHash(ctx context.Context) (common.Hash, error)
GetSplitDepth(ctx context.Context) (types.Depth, error)
GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error)
......
......@@ -3,8 +3,10 @@ package contracts
import (
"context"
_ "embed"
"fmt"
"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/sources/batching/rpcblock"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
)
......@@ -16,6 +18,40 @@ type FaultDisputeGameContract0180 struct {
FaultDisputeGameContractLatest
}
// GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, and max clock duration.
func (f *FaultDisputeGameContract0180) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) {
defer f.metrics.StartContractRequest("GetGameMetadata")()
results, err := f.multiCaller.Call(ctx, block,
f.contract.Call(methodL1Head),
f.contract.Call(methodL2BlockNumber),
f.contract.Call(methodRootClaim),
f.contract.Call(methodStatus),
f.contract.Call(methodMaxClockDuration),
)
if err != nil {
return GameMetadata{}, fmt.Errorf("failed to retrieve game metadata: %w", err)
}
if len(results) != 5 {
return GameMetadata{}, fmt.Errorf("expected 5 results but got %v", len(results))
}
l1Head := results[0].GetHash(0)
l2BlockNumber := results[1].GetBigInt(0).Uint64()
rootClaim := results[2].GetHash(0)
status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0))
if err != nil {
return GameMetadata{}, fmt.Errorf("failed to convert game status: %w", err)
}
duration := results[4].GetUint64(0)
return GameMetadata{
L1Head: l1Head,
L2BlockNum: l2BlockNumber,
RootClaim: rootClaim,
Status: status,
MaxClockDuration: duration,
L2BlockNumberChallenged: false,
}, nil
}
func (f *FaultDisputeGameContract0180) IsL2BlockNumberChallenged(_ context.Context, _ rpcblock.Block) (bool, error) {
return false, nil
}
......
......@@ -12,7 +12,6 @@ import (
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
)
//go:embed abis/FaultDisputeGame-0.8.0.json
......@@ -29,7 +28,7 @@ type FaultDisputeGameContract080 struct {
}
// GetGameMetadata returns the game's L1 head, L2 block number, root claim, status, and max clock duration.
func (f *FaultDisputeGameContract080) GetGameMetadata(ctx context.Context, block rpcblock.Block) (common.Hash, uint64, common.Hash, gameTypes.GameStatus, uint64, error) {
func (f *FaultDisputeGameContract080) GetGameMetadata(ctx context.Context, block rpcblock.Block) (GameMetadata, error) {
defer f.metrics.StartContractRequest("GetGameMetadata")()
results, err := f.multiCaller.Call(ctx, block,
f.contract.Call(methodL1Head),
......@@ -38,20 +37,27 @@ func (f *FaultDisputeGameContract080) GetGameMetadata(ctx context.Context, block
f.contract.Call(methodStatus),
f.contract.Call(methodGameDuration))
if err != nil {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("failed to retrieve game metadata: %w", err)
return GameMetadata{}, fmt.Errorf("failed to retrieve game metadata: %w", err)
}
if len(results) != 5 {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("expected 3 results but got %v", len(results))
return GameMetadata{}, fmt.Errorf("expected 5 results but got %v", len(results))
}
l1Head := results[0].GetHash(0)
l2BlockNumber := results[1].GetBigInt(0).Uint64()
rootClaim := results[2].GetHash(0)
status, err := gameTypes.GameStatusFromUint8(results[3].GetUint8(0))
if err != nil {
return common.Hash{}, 0, common.Hash{}, 0, 0, fmt.Errorf("failed to convert game status: %w", err)
return GameMetadata{}, fmt.Errorf("failed to convert game status: %w", err)
}
duration := results[4].GetUint64(0)
return l1Head, l2BlockNumber, rootClaim, status, duration / 2, nil
return GameMetadata{
L1Head: l1Head,
L2BlockNum: l2BlockNumber,
RootClaim: rootClaim,
Status: status,
MaxClockDuration: duration / 2,
L2BlockNumberChallenged: false,
}, nil
}
func (f *FaultDisputeGameContract080) GetMaxClockDuration(ctx context.Context) (time.Duration, error) {
......
......@@ -475,23 +475,38 @@ func TestGetGameMetadata(t *testing.T) {
expectedMaxClockDuration := uint64(456)
expectedRootClaim := common.Hash{0x01, 0x02}
expectedStatus := types.GameStatusChallengerWon
expectedL2BlockNumberChallenged := true
expectedL2BlockNumberChallenger := common.Address{0xee}
block := rpcblock.ByNumber(889)
stubRpc.SetResponse(fdgAddr, methodL1Head, block, nil, []interface{}{expectedL1Head})
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)})
stubRpc.SetResponse(fdgAddr, methodRootClaim, block, nil, []interface{}{expectedRootClaim})
stubRpc.SetResponse(fdgAddr, methodStatus, block, nil, []interface{}{expectedStatus})
if version.version == vers080 {
expectedL2BlockNumberChallenged = false
expectedL2BlockNumberChallenger = common.Address{}
stubRpc.SetResponse(fdgAddr, methodGameDuration, block, nil, []interface{}{expectedMaxClockDuration * 2})
} else if version.version == vers0180 {
expectedL2BlockNumberChallenged = false
expectedL2BlockNumberChallenger = common.Address{}
stubRpc.SetResponse(fdgAddr, methodMaxClockDuration, block, nil, []interface{}{expectedMaxClockDuration})
} else {
stubRpc.SetResponse(fdgAddr, methodMaxClockDuration, block, nil, []interface{}{expectedMaxClockDuration})
stubRpc.SetResponse(fdgAddr, methodL2BlockNumberChallenged, block, nil, []interface{}{expectedL2BlockNumberChallenged})
stubRpc.SetResponse(fdgAddr, methodL2BlockNumberChallenger, block, nil, []interface{}{expectedL2BlockNumberChallenger})
}
actual, err := contract.GetGameMetadata(context.Background(), block)
expected := GameMetadata{
L1Head: expectedL1Head,
L2BlockNum: expectedL2BlockNumber,
RootClaim: expectedRootClaim,
Status: expectedStatus,
MaxClockDuration: expectedMaxClockDuration,
L2BlockNumberChallenged: expectedL2BlockNumberChallenged,
L2BlockNumberChallenger: expectedL2BlockNumberChallenger,
}
l1Head, l2BlockNumber, rootClaim, status, duration, err := contract.GetGameMetadata(context.Background(), block)
require.NoError(t, err)
require.Equal(t, expectedL1Head, l1Head)
require.Equal(t, expectedL2BlockNumber, l2BlockNumber)
require.Equal(t, expectedRootClaim, rootClaim)
require.Equal(t, expectedStatus, status)
require.Equal(t, expectedMaxClockDuration, duration)
require.Equal(t, expected, actual)
})
}
}
......
......@@ -132,6 +132,8 @@ type Metricer interface {
RecordBondCollateral(addr common.Address, required *big.Int, available *big.Int)
RecordL2Challenges(agreement bool, count int)
caching.Metrics
contractMetrics.ContractMetricer
}
......@@ -169,6 +171,7 @@ type Metrics struct {
latestInvalidProposal prometheus.Gauge
ignoredGames prometheus.Gauge
failedGames prometheus.Gauge
l2Challenges prometheus.GaugeVec
requiredCollateral prometheus.GaugeVec
availableCollateral prometheus.GaugeVec
......@@ -309,6 +312,15 @@ func NewMetrics() *Metrics {
"delayedWETH",
"balance",
}),
l2Challenges: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "l2_block_challenges",
Help: "Number of games where the L2 block number has been successfully challenged",
}, []string{
// Agreement with the root claim, not the actual l2 block number challenge.
// An l2 block number challenge with an agreement means the challenge was invalid.
"root_agreement",
}),
}
}
......@@ -467,6 +479,14 @@ func (m *Metrics) RecordBondCollateral(addr common.Address, required *big.Int, a
m.availableCollateral.WithLabelValues(addr.Hex(), zeroBalanceLabel).Set(0)
}
func (m *Metrics) RecordL2Challenges(agreement bool, count int) {
agree := "disagree"
if agreement {
agree = "agree"
}
m.l2Challenges.WithLabelValues(agree).Set(float64(count))
}
const (
inProgress = true
correct = true
......
......@@ -17,8 +17,7 @@ var NoopMetrics Metricer = new(NoopMetricsImpl)
func (*NoopMetricsImpl) RecordInfo(_ string) {}
func (*NoopMetricsImpl) RecordUp() {}
func (i *NoopMetricsImpl) RecordMonitorDuration(_ time.Duration) {
}
func (*NoopMetricsImpl) RecordMonitorDuration(_ time.Duration) {}
func (*NoopMetricsImpl) CacheAdd(_ string, _ int, _ bool) {}
func (*NoopMetricsImpl) CacheGet(_ string, _ bool) {}
......@@ -44,3 +43,5 @@ func (*NoopMetricsImpl) RecordIgnoredGames(_ int) {}
func (*NoopMetricsImpl) RecordFailedGames(_ int) {}
func (*NoopMetricsImpl) RecordBondCollateral(_ common.Address, _ *big.Int, _ *big.Int) {}
func (*NoopMetricsImpl) RecordL2Challenges(_ bool, _ int) {}
......@@ -59,7 +59,10 @@ func (b *Bonds) checkCredits(games []*types.EnrichedGameData) {
}
// The recipient of a resolved claim is the claimant unless it's been countered.
recipient := claim.Claimant
if claim.CounteredBy != (common.Address{}) {
if claim.IsRoot() && game.BlockNumberChallenged {
// The bond for the root claim is paid to the block number challenger if present
recipient = game.BlockNumberChallenger
} else if claim.CounteredBy != (common.Address{}) {
recipient = claim.CounteredBy
}
current := expectedCredits[recipient]
......
......@@ -57,6 +57,7 @@ func TestCheckRecipientCredit(t *testing.T) {
addr2 := common.Address{0x2b}
addr3 := common.Address{0x3c}
addr4 := common.Address{0x4d}
notRootPosition := types.NewPositionFromGIndex(big.NewInt(2))
// Game has not reached max duration
game1 := &monTypes.EnrichedGameData{
MaxClockDuration: 50000,
......@@ -68,7 +69,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 10 credits for addr1
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(10),
Bond: big.NewInt(10),
Position: types.RootPosition,
},
Claimant: addr1,
},
......@@ -77,7 +79,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // No expected credits as not resolved
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(15),
Bond: big.NewInt(15),
Position: notRootPosition,
},
Claimant: addr1,
},
......@@ -86,7 +89,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 5 credits for addr1
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(5),
Bond: big.NewInt(5),
Position: notRootPosition,
},
Claimant: addr1,
},
......@@ -95,7 +99,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 7 credits for addr2
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(7),
Bond: big.NewInt(7),
Position: notRootPosition,
},
Claimant: addr3,
CounteredBy: addr2,
......@@ -105,7 +110,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 3 credits for addr4
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(3),
Bond: big.NewInt(3),
Position: notRootPosition,
},
Claimant: addr4,
},
......@@ -135,7 +141,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 11 credits for addr1
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(11),
Bond: big.NewInt(11),
Position: types.RootPosition,
},
Claimant: addr1,
},
......@@ -144,7 +151,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // No expected credits as not resolved
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(15),
Bond: big.NewInt(15),
Position: notRootPosition,
},
Claimant: addr1,
},
......@@ -153,7 +161,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 6 credits for addr1
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(6),
Bond: big.NewInt(6),
Position: notRootPosition,
},
Claimant: addr1,
},
......@@ -162,7 +171,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 8 credits for addr2
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(8),
Bond: big.NewInt(8),
Position: notRootPosition,
},
Claimant: addr3,
CounteredBy: addr2,
......@@ -172,7 +182,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 4 credits for addr4
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(4),
Bond: big.NewInt(4),
Position: notRootPosition,
},
Claimant: addr4,
},
......@@ -204,7 +215,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 9 credits for addr1
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(9),
Bond: big.NewInt(9),
Position: types.RootPosition,
},
Claimant: addr1,
},
......@@ -213,7 +225,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 6 credits for addr2
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(6),
Bond: big.NewInt(6),
Position: notRootPosition,
},
Claimant: addr4,
CounteredBy: addr2,
......@@ -223,7 +236,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 2 credits for addr4
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(2),
Bond: big.NewInt(2),
Position: notRootPosition,
},
Claimant: addr4,
},
......@@ -250,20 +264,25 @@ func TestCheckRecipientCredit(t *testing.T) {
Proxy: common.Address{44},
Timestamp: uint64(frozen.Unix()) - 22,
},
BlockNumberChallenged: true,
BlockNumberChallenger: addr1,
Claims: []monTypes.EnrichedClaim{
{ // Expect 9 credits for addr1
{ // Expect 9 credits for addr1 as the block number challenger
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(9),
Bond: big.NewInt(9),
Position: types.RootPosition,
},
Claimant: addr1,
Claimant: addr2,
CounteredBy: addr3,
},
Resolved: true,
},
{ // Expect 6 credits for addr2
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(6),
Bond: big.NewInt(6),
Position: notRootPosition,
},
Claimant: addr4,
CounteredBy: addr2,
......@@ -273,7 +292,8 @@ func TestCheckRecipientCredit(t *testing.T) {
{ // Expect 2 credits for addr4
Claim: types.Claim{
ClaimData: types.ClaimData{
Bond: big.NewInt(2),
Bond: big.NewInt(2),
Position: notRootPosition,
},
Claimant: addr4,
},
......
......@@ -27,21 +27,13 @@ func NewBondEnricher() *BondEnricher {
}
func (b *BondEnricher) Enrich(ctx context.Context, block rpcblock.Block, caller GameCaller, game *monTypes.EnrichedGameData) error {
recipients := make(map[common.Address]bool)
for _, claim := range game.Claims {
if claim.CounteredBy != (common.Address{}) {
recipients[claim.CounteredBy] = true
} else {
recipients[claim.Claimant] = true
}
}
recipientAddrs := maps.Keys(recipients)
recipientAddrs := maps.Keys(game.Recipients)
credits, err := caller.GetCredits(ctx, block, recipientAddrs...)
if err != nil {
return err
}
if len(credits) != len(recipients) {
return fmt.Errorf("%w, requested %v values but got %v", ErrIncorrectCreditCount, len(recipients), len(credits))
if len(credits) != len(recipientAddrs) {
return fmt.Errorf("%w, requested %v values but got %v", ErrIncorrectCreditCount, len(recipientAddrs), len(credits))
}
game.Credits = make(map[common.Address]*big.Int)
for i, credit := range credits {
......
......@@ -25,7 +25,7 @@ type GameCallerMetrics interface {
type GameCaller interface {
GetWithdrawals(context.Context, rpcblock.Block, common.Address, ...common.Address) ([]*contracts.WithdrawalRequest, error)
GetGameMetadata(context.Context, rpcblock.Block) (common.Hash, uint64, common.Hash, gameTypes.GameStatus, uint64, error)
GetGameMetadata(context.Context, rpcblock.Block) (contracts.GameMetadata, error)
GetAllClaims(context.Context, rpcblock.Block) ([]faultTypes.Claim, error)
BondCaller
BalanceCaller
......
......@@ -125,7 +125,7 @@ func (e *Extractor) enrichGame(ctx context.Context, blockHash common.Hash, game
if err != nil {
return nil, fmt.Errorf("failed to create contracts: %w", err)
}
l1Head, l2BlockNum, rootClaim, status, duration, err := caller.GetGameMetadata(ctx, rpcblock.ByHash(blockHash))
meta, err := caller.GetGameMetadata(ctx, rpcblock.ByHash(blockHash))
if err != nil {
return nil, fmt.Errorf("failed to fetch game metadata: %w", err)
}
......@@ -138,13 +138,15 @@ func (e *Extractor) enrichGame(ctx context.Context, blockHash common.Hash, game
enrichedClaims[i] = monTypes.EnrichedClaim{Claim: claim}
}
enrichedGame := &monTypes.EnrichedGameData{
GameMetadata: game,
L1Head: l1Head,
L2BlockNumber: l2BlockNum,
RootClaim: rootClaim,
Status: status,
MaxClockDuration: duration,
Claims: enrichedClaims,
GameMetadata: game,
L1Head: meta.L1Head,
L2BlockNumber: meta.L2BlockNum,
RootClaim: meta.RootClaim,
Status: meta.Status,
MaxClockDuration: meta.MaxClockDuration,
BlockNumberChallenged: meta.L2BlockNumberChallenged,
BlockNumberChallenger: meta.L2BlockNumberChallenger,
Claims: enrichedClaims,
}
if err := e.applyEnrichers(ctx, blockHash, caller, enrichedGame); err != nil {
return nil, fmt.Errorf("failed to enrich game: %w", err)
......
......@@ -153,7 +153,7 @@ func TestExtractor_Extract(t *testing.T) {
})
}
func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, metadataErr int, claimsErr int, durationErr int) {
func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr, metadataErr, claimsErr, durationErr int) {
errorLevelFilter := testlog.NewLevelFilter(log.LevelError)
createMessageFilter := testlog.NewAttributesContainsFilter("err", "failed to create contracts")
l := logs.FindLogs(errorLevelFilter, createMessageFilter)
......@@ -254,12 +254,15 @@ func (m *mockGameCaller) GetWithdrawals(_ context.Context, _ rpcblock.Block, _ c
}, nil
}
func (m *mockGameCaller) GetGameMetadata(_ context.Context, _ rpcblock.Block) (common.Hash, uint64, common.Hash, gameTypes.GameStatus, uint64, error) {
func (m *mockGameCaller) GetGameMetadata(_ context.Context, _ rpcblock.Block) (contracts.GameMetadata, error) {
m.metadataCalls++
if m.metadataErr != nil {
return common.Hash{}, 0, common.Hash{}, 0, 0, m.metadataErr
return contracts.GameMetadata{}, m.metadataErr
}
return common.Hash{0xaa}, 0, mockRootClaim, 0, 0, nil
return contracts.GameMetadata{
L1Head: common.Hash{0xaa},
RootClaim: mockRootClaim,
}, nil
}
func (m *mockGameCaller) GetAllClaims(_ context.Context, _ rpcblock.Block) ([]faultTypes.Claim, error) {
......
......@@ -25,6 +25,9 @@ func (w *RecipientEnricher) Enrich(_ context.Context, _ rpcblock.Block, _ GameCa
recipients[claim.Claimant] = true
}
}
if game.BlockNumberChallenger != (common.Address{}) {
recipients[game.BlockNumberChallenger] = true
}
game.Recipients = recipients
return nil
}
......@@ -12,6 +12,7 @@ import (
func TestRecipientEnricher(t *testing.T) {
game, recipients := makeTestGame()
game.Recipients = make(map[common.Address]bool)
game.BlockNumberChallenger = common.Address{0xff, 0xee, 0xdd}
enricher := NewRecipientEnricher()
caller := &mockGameCaller{}
ctx := context.Background()
......@@ -20,4 +21,5 @@ func TestRecipientEnricher(t *testing.T) {
for _, recipient := range recipients {
require.Contains(t, game.Recipients, recipient)
}
require.Contains(t, game.Recipients, game.BlockNumberChallenger)
}
......@@ -111,11 +111,19 @@ func (f *Forecast) forecastGame(game *monTypes.EnrichedGameData, metrics *foreca
return nil
}
// Create the bidirectional tree of claims.
tree := transform.CreateBidirectionalTree(game.Claims)
// Compute the resolution status of the game.
forecastStatus := Resolve(tree)
var forecastStatus types.GameStatus
// Games that have their block number challenged are won
// by the challenger since the counter is proven on-chain.
if game.BlockNumberChallenged {
f.logger.Debug("Found game with challenged block number",
"game", game.Proxy, "blockNum", game.L2BlockNumber, "agreement", agreement)
// If the block number is challenged the challenger will always win
forecastStatus = types.GameStatusChallengerWon
} else {
// Otherwise we go through the resolution process to determine who would win based on the current claims
tree := transform.CreateBidirectionalTree(game.Claims)
forecastStatus = Resolve(tree)
}
if agreement {
// If we agree with the output root proposal, the Defender should win, defending that claim.
......
......@@ -104,6 +104,48 @@ func TestForecast_Forecast_BasicTests(t *testing.T) {
func TestForecast_Forecast_EndLogs(t *testing.T) {
t.Parallel()
t.Run("BlockNumberChallenged_AgreeWithChallenge", func(t *testing.T) {
forecast, m, logs := setupForecastTest(t)
expectedGame := monTypes.EnrichedGameData{
Status: types.GameStatusInProgress,
BlockNumberChallenged: true,
L2BlockNumber: 6,
AgreeWithClaim: false,
}
forecast.Forecast([]*monTypes.EnrichedGameData{&expectedGame}, 0, 0)
l := logs.FindLog(testlog.NewLevelFilter(log.LevelDebug), testlog.NewMessageFilter("Found game with challenged block number"))
require.NotNil(t, l)
require.Equal(t, expectedGame.Proxy, l.AttrValue("game"))
require.Equal(t, expectedGame.L2BlockNumber, l.AttrValue("blockNum"))
require.Equal(t, false, l.AttrValue("agreement"))
expectedMetrics := zeroGameAgreement()
// We disagree with the root claim and the challenger is ahead
expectedMetrics[metrics.DisagreeChallengerAhead] = 1
require.Equal(t, expectedMetrics, m.gameAgreement)
})
t.Run("BlockNumberChallenged_DisagreeWithChallenge", func(t *testing.T) {
forecast, m, logs := setupForecastTest(t)
expectedGame := monTypes.EnrichedGameData{
Status: types.GameStatusInProgress,
BlockNumberChallenged: true,
L2BlockNumber: 6,
AgreeWithClaim: true,
}
forecast.Forecast([]*monTypes.EnrichedGameData{&expectedGame}, 0, 0)
l := logs.FindLog(testlog.NewLevelFilter(log.LevelDebug), testlog.NewMessageFilter("Found game with challenged block number"))
require.NotNil(t, l)
require.Equal(t, expectedGame.Proxy, l.AttrValue("game"))
require.Equal(t, expectedGame.L2BlockNumber, l.AttrValue("blockNum"))
require.Equal(t, true, l.AttrValue("agreement"))
expectedMetrics := zeroGameAgreement()
// We agree with the root claim and the challenger is ahead
expectedMetrics[metrics.AgreeChallengerAhead] = 1
require.Equal(t, expectedMetrics, m.gameAgreement)
})
t.Run("AgreeDefenderWins", func(t *testing.T) {
forecast, _, logs := setupForecastTest(t)
games := []*monTypes.EnrichedGameData{{
......
package mon
import (
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum/go-ethereum/log"
)
type L2ChallengesMetrics interface {
RecordL2Challenges(agreement bool, count int)
}
type L2ChallengesMonitor struct {
logger log.Logger
metrics L2ChallengesMetrics
}
func NewL2ChallengesMonitor(logger log.Logger, metrics L2ChallengesMetrics) *L2ChallengesMonitor {
return &L2ChallengesMonitor{
logger: logger,
metrics: metrics,
}
}
func (m *L2ChallengesMonitor) CheckL2Challenges(games []*types.EnrichedGameData) {
agreeChallengeCount := 0
disagreeChallengeCount := 0
for _, game := range games {
if game.BlockNumberChallenged {
if game.AgreeWithClaim {
m.logger.Warn("Found game with valid block number challenged",
"game", game.Proxy, "blockNum", game.L2BlockNumber, "agreement", game.AgreeWithClaim, "challenger", game.BlockNumberChallenger)
agreeChallengeCount++
} else {
m.logger.Debug("Found game with invalid block number challenged",
"game", game.Proxy, "blockNum", game.L2BlockNumber, "agreement", game.AgreeWithClaim, "challenger", game.BlockNumberChallenger)
disagreeChallengeCount++
}
}
}
m.metrics.RecordL2Challenges(true, agreeChallengeCount)
m.metrics.RecordL2Challenges(false, disagreeChallengeCount)
}
package mon
import (
"testing"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
func TestMonitorL2Challenges(t *testing.T) {
games := []*types.EnrichedGameData{
{GameMetadata: gameTypes.GameMetadata{Proxy: common.Address{0x44}}, BlockNumberChallenged: true, AgreeWithClaim: true, L2BlockNumber: 44, BlockNumberChallenger: common.Address{0x55}},
{BlockNumberChallenged: false, AgreeWithClaim: true},
{GameMetadata: gameTypes.GameMetadata{Proxy: common.Address{0x22}}, BlockNumberChallenged: true, AgreeWithClaim: false, L2BlockNumber: 22, BlockNumberChallenger: common.Address{0x33}},
{BlockNumberChallenged: false, AgreeWithClaim: false},
{BlockNumberChallenged: false, AgreeWithClaim: false},
{BlockNumberChallenged: false, AgreeWithClaim: true},
}
metrics := &stubL2ChallengeMetrics{}
logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug)
monitor := NewL2ChallengesMonitor(logger, metrics)
monitor.CheckL2Challenges(games)
require.Equal(t, 1, metrics.challengeCount[true])
require.Equal(t, 1, metrics.challengeCount[false])
// Warn log for challenged and agreement
levelFilter := testlog.NewLevelFilter(log.LevelWarn)
messageFilter := testlog.NewMessageFilter("Found game with valid block number challenged")
l := capturedLogs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
require.Equal(t, common.Address{0x44}, l.AttrValue("game"))
require.Equal(t, uint64(44), l.AttrValue("blockNum"))
require.Equal(t, true, l.AttrValue("agreement"))
require.Equal(t, common.Address{0x55}, l.AttrValue("challenger"))
// Debug log for challenged but disagreement
levelFilter = testlog.NewLevelFilter(log.LevelDebug)
messageFilter = testlog.NewMessageFilter("Found game with invalid block number challenged")
l = capturedLogs.FindLog(levelFilter, messageFilter)
require.NotNil(t, l)
require.Equal(t, common.Address{0x22}, l.AttrValue("game"))
require.Equal(t, uint64(22), l.AttrValue("blockNum"))
require.Equal(t, false, l.AttrValue("agreement"))
require.Equal(t, common.Address{0x33}, l.AttrValue("challenger"))
}
type stubL2ChallengeMetrics struct {
challengeCount map[bool]int
}
func (s *stubL2ChallengeMetrics) RecordL2Challenges(agreement bool, count int) {
if s.challengeCount == nil {
s.challengeCount = make(map[bool]int)
}
s.challengeCount[agreement] = count
}
......@@ -16,8 +16,7 @@ import (
type ForecastResolution func(games []*types.EnrichedGameData, ignoredCount, failedCount int)
type Bonds func(games []*types.EnrichedGameData)
type Resolutions func(games []*types.EnrichedGameData)
type MonitorClaims func(games []*types.EnrichedGameData)
type MonitorWithdrawals func(games []*types.EnrichedGameData)
type Monitor func(games []*types.EnrichedGameData)
type BlockHashFetcher func(ctx context.Context, number *big.Int) (common.Hash, error)
type BlockNumberFetcher func(ctx context.Context) (uint64, error)
type Extract func(ctx context.Context, blockHash common.Hash, minTimestamp uint64) ([]*types.EnrichedGameData, int, int, error)
......@@ -41,8 +40,9 @@ type gameMonitor struct {
forecast ForecastResolution
bonds Bonds
resolutions Resolutions
claims MonitorClaims
withdrawals MonitorWithdrawals
claims Monitor
withdrawals Monitor
l2Challenges Monitor
extract Extract
fetchBlockHash BlockHashFetcher
fetchBlockNumber BlockNumberFetcher
......@@ -58,8 +58,9 @@ func newGameMonitor(
forecast ForecastResolution,
bonds Bonds,
resolutions Resolutions,
claims MonitorClaims,
withdrawals MonitorWithdrawals,
claims Monitor,
withdrawals Monitor,
l2Challenges Monitor,
extract Extract,
fetchBlockNumber BlockNumberFetcher,
fetchBlockHash BlockHashFetcher,
......@@ -77,6 +78,7 @@ func newGameMonitor(
resolutions: resolutions,
claims: claims,
withdrawals: withdrawals,
l2Challenges: l2Challenges,
extract: extract,
fetchBlockNumber: fetchBlockNumber,
fetchBlockHash: fetchBlockHash,
......@@ -104,6 +106,7 @@ func (m *gameMonitor) monitorGames() error {
m.bonds(enrichedGames)
m.claims(enrichedGames)
m.withdrawals(enrichedGames)
m.l2Challenges(enrichedGames)
timeTaken := m.clock.Since(start)
m.metrics.RecordMonitorDuration(timeTaken)
m.logger.Info("Completed monitoring update", "blockNumber", blockNumber, "blockHash", blockHash, "duration", timeTaken, "games", len(enrichedGames), "ignored", ignored, "failed", failed)
......
......@@ -25,7 +25,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
t.Parallel()
t.Run("FailedFetchBlocknumber", func(t *testing.T) {
monitor, _, _, _, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom")
monitor.fetchBlockNumber = func(ctx context.Context) (uint64, error) {
return 0, boom
......@@ -35,7 +35,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
})
t.Run("FailedFetchBlockHash", func(t *testing.T) {
monitor, _, _, _, _, _, _ := setupMonitorTest(t)
monitor, _, _, _, _, _, _, _ := setupMonitorTest(t)
boom := errors.New("boom")
monitor.fetchBlockHash = func(ctx context.Context, number *big.Int) (common.Hash, error) {
return common.Hash{}, boom
......@@ -45,7 +45,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
})
t.Run("MonitorsWithNoGames", func(t *testing.T) {
monitor, factory, forecast, bonds, withdrawals, resolutions, claims := setupMonitorTest(t)
monitor, factory, forecast, bonds, withdrawals, resolutions, claims, l2Challenges := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{}
err := monitor.monitorGames()
require.NoError(t, err)
......@@ -54,10 +54,11 @@ func TestMonitor_MonitorGames(t *testing.T) {
require.Equal(t, 1, resolutions.calls)
require.Equal(t, 1, claims.calls)
require.Equal(t, 1, withdrawals.calls)
require.Equal(t, 1, l2Challenges.calls)
})
t.Run("MonitorsMultipleGames", func(t *testing.T) {
monitor, factory, forecast, bonds, withdrawals, resolutions, claims := setupMonitorTest(t)
monitor, factory, forecast, bonds, withdrawals, resolutions, claims, l2Challenges := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{{}, {}, {}}
err := monitor.monitorGames()
require.NoError(t, err)
......@@ -66,6 +67,7 @@ func TestMonitor_MonitorGames(t *testing.T) {
require.Equal(t, 1, resolutions.calls)
require.Equal(t, 1, claims.calls)
require.Equal(t, 1, withdrawals.calls)
require.Equal(t, 1, l2Challenges.calls)
})
}
......@@ -73,7 +75,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
t.Run("MonitorsGames", func(t *testing.T) {
addr1 := common.Address{0xaa}
addr2 := common.Address{0xbb}
monitor, factory, forecaster, _, _, _, _ := setupMonitorTest(t)
monitor, factory, forecaster, _, _, _, _, _ := setupMonitorTest(t)
factory.games = []*monTypes.EnrichedGameData{newEnrichedGameData(addr1, 9999), newEnrichedGameData(addr2, 9999)}
factory.maxSuccess = len(factory.games) // Only allow two successful fetches
......@@ -86,7 +88,7 @@ func TestMonitor_StartMonitoring(t *testing.T) {
})
t.Run("FailsToFetchGames", func(t *testing.T) {
monitor, factory, forecaster, _, _, _, _ := setupMonitorTest(t)
monitor, factory, forecaster, _, _, _, _, _ := setupMonitorTest(t)
factory.fetchErr = errors.New("boom")
monitor.StartMonitoring()
......@@ -108,7 +110,7 @@ func newEnrichedGameData(proxy common.Address, timestamp uint64) *monTypes.Enric
}
}
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, *mockBonds, *mockWithdrawalMonitor, *mockResolutionMonitor, *mockClaimMonitor) {
func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast, *mockBonds, *mockMonitor, *mockResolutionMonitor, *mockMonitor, *mockMonitor) {
logger := testlog.Logger(t, log.LvlDebug)
fetchBlockNum := func(ctx context.Context) (uint64, error) {
return 1, nil
......@@ -123,8 +125,9 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast
forecast := &mockForecast{}
bonds := &mockBonds{}
resolutions := &mockResolutionMonitor{}
claims := &mockClaimMonitor{}
withdrawals := &mockWithdrawalMonitor{}
claims := &mockMonitor{}
withdrawals := &mockMonitor{}
l2Challenges := &mockMonitor{}
monitor := newGameMonitor(
context.Background(),
logger,
......@@ -135,13 +138,14 @@ func setupMonitorTest(t *testing.T) (*gameMonitor, *mockExtractor, *mockForecast
forecast.Forecast,
bonds.CheckBonds,
resolutions.CheckResolutions,
claims.CheckClaims,
withdrawals.CheckWithdrawals,
claims.Check,
withdrawals.Check,
l2Challenges.Check,
extractor.Extract,
fetchBlockNum,
fetchBlockHash,
)
return monitor, extractor, forecast, bonds, withdrawals, resolutions, claims
return monitor, extractor, forecast, bonds, withdrawals, resolutions, claims, l2Challenges
}
type mockResolutionMonitor struct {
......@@ -152,19 +156,11 @@ func (m *mockResolutionMonitor) CheckResolutions(games []*monTypes.EnrichedGameD
m.calls++
}
type mockClaimMonitor struct {
type mockMonitor struct {
calls int
}
func (m *mockClaimMonitor) CheckClaims(games []*monTypes.EnrichedGameData) {
m.calls++
}
type mockWithdrawalMonitor struct {
calls int
}
func (m *mockWithdrawalMonitor) CheckWithdrawals(games []*monTypes.EnrichedGameData) {
func (m *mockMonitor) Check(games []*monTypes.EnrichedGameData) {
m.calls++
}
......
......@@ -128,7 +128,7 @@ func (s *Service) initExtractor(cfg *config.Config) {
cfg.IgnoredGames,
cfg.MaxConcurrency,
extract.NewClaimEnricher(),
extract.NewRecipientEnricher(), // Must be called before WithdrawalsEnricher
extract.NewRecipientEnricher(), // Must be called before WithdrawalsEnricher and BondEnricher
extract.NewWithdrawalsEnricher(),
extract.NewBondEnricher(),
extract.NewBalanceEnricher(),
......@@ -213,6 +213,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
}
return block.Hash(), nil
}
l2ChallengesMonitor := NewL2ChallengesMonitor(s.logger, s.metrics)
s.monitor = newGameMonitor(
ctx,
s.logger,
......@@ -225,6 +226,7 @@ func (s *Service) initMonitor(ctx context.Context, cfg *config.Config) {
s.resolutions.CheckResolutions,
s.claims.CheckClaims,
s.withdrawals.CheckWithdrawals,
l2ChallengesMonitor.CheckL2Challenges,
s.extractor.Extract,
s.l1Client.BlockNumber,
blockHashFetcher,
......
......@@ -17,13 +17,15 @@ type EnrichedClaim struct {
type EnrichedGameData struct {
types.GameMetadata
L1Head common.Hash
L1HeadNum uint64
L2BlockNumber uint64
RootClaim common.Hash
Status types.GameStatus
MaxClockDuration uint64
Claims []EnrichedClaim
L1Head common.Hash
L1HeadNum uint64
L2BlockNumber uint64
RootClaim common.Hash
Status types.GameStatus
MaxClockDuration uint64
BlockNumberChallenged bool
BlockNumberChallenger common.Address
Claims []EnrichedClaim
AgreeWithClaim bool
ExpectedRootClaim common.Hash
......
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