Commit 8c3a849d authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-dispute-mon: Add extractor for bond data (#9787)

parent 951e46e9
......@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/dial"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/urfave/cli/v2"
)
......@@ -67,7 +68,7 @@ func listClaims(ctx context.Context, game *contracts.FaultDisputeGameContract) e
return fmt.Errorf("failed to retrieve status: %w", err)
}
claims, err := game.GetAllClaims(ctx)
claims, err := game.GetAllClaims(ctx, rpcblock.Latest)
if err != nil {
return fmt.Errorf("failed to retrieve claims: %w", err)
}
......
......@@ -11,6 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
......@@ -26,7 +27,7 @@ type Responder interface {
}
type ClaimLoader interface {
GetAllClaims(ctx context.Context) ([]types.Claim, error)
GetAllClaims(ctx context.Context, block rpcblock.Block) ([]types.Claim, error)
}
type Agent struct {
......@@ -140,7 +141,7 @@ func (a *Agent) tryResolve(ctx context.Context) bool {
var errNoResolvableClaims = errors.New("no resolvable claims")
func (a *Agent) tryResolveClaims(ctx context.Context) error {
claims, err := a.loader.GetAllClaims(ctx)
claims, err := a.loader.GetAllClaims(ctx, rpcblock.Latest)
if err != nil {
return fmt.Errorf("failed to fetch claims: %w", err)
}
......@@ -202,7 +203,7 @@ func (a *Agent) resolveClaims(ctx context.Context) error {
// newGameFromContracts initializes a new game state from the state in the contract
func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
claims, err := a.loader.GetAllClaims(ctx)
claims, err := a.loader.GetAllClaims(ctx, rpcblock.Latest)
if err != nil {
return nil, fmt.Errorf("failed to fetch claims: %w", err)
}
......
......@@ -7,6 +7,7 @@ import (
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
......@@ -162,7 +163,7 @@ type stubClaimLoader struct {
claims []types.Claim
}
func (s *stubClaimLoader) GetAllClaims(ctx context.Context) ([]types.Claim, error) {
func (s *stubClaimLoader) GetAllClaims(_ context.Context, _ rpcblock.Block) ([]types.Claim, error) {
s.callCount++
if s.callCount > s.maxLoads && s.maxLoads != 0 {
return []types.Claim{}, nil
......
......@@ -38,6 +38,7 @@ var (
methodRequiredBond = "getRequiredBond"
methodClaimCredit = "claimCredit"
methodCredit = "credit"
methodWETH = "weth"
)
type FaultDisputeGameContract struct {
......@@ -62,12 +63,28 @@ func NewFaultDisputeGameContract(addr common.Address, caller *batching.MultiCall
}, nil
}
// GetBalance returns the total amount of ETH controlled by this contract.
// Note that the ETH is actually held by the DelayedWETH contract which may be shared by multiple games.
// Returns the balance and the address of the contract that actually holds the balance.
func (f *FaultDisputeGameContract) GetBalance(ctx context.Context, block rpcblock.Block) (*big.Int, common.Address, error) {
result, err := f.multiCaller.SingleCall(ctx, block, f.contract.Call(methodWETH))
if err != nil {
return nil, common.Address{}, fmt.Errorf("failed to load weth address: %w", err)
}
wethAddr := result.GetAddress(0)
result, err = f.multiCaller.SingleCall(ctx, block, batching.NewBalanceCall(wethAddr))
if err != nil {
return nil, common.Address{}, fmt.Errorf("failed to retrieve game balance: %w", err)
}
return result.GetBigInt(0), wethAddr, nil
}
// GetBlockRange returns the block numbers of the absolute pre-state block (typically genesis or the bedrock activation block)
// and the post-state block (that the proposed output root is for).
func (c *FaultDisputeGameContract) GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) {
results, err := c.multiCaller.Call(ctx, rpcblock.Latest,
c.contract.Call(methodGenesisBlockNumber),
c.contract.Call(methodL2BlockNumber))
func (f *FaultDisputeGameContract) GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) {
results, err := f.multiCaller.Call(ctx, rpcblock.Latest,
f.contract.Call(methodGenesisBlockNumber),
f.contract.Call(methodL2BlockNumber))
if err != nil {
retErr = fmt.Errorf("failed to retrieve game block range: %w", err)
return
......@@ -82,12 +99,12 @@ func (c *FaultDisputeGameContract) GetBlockRange(ctx context.Context) (prestateB
}
// GetGameMetadata returns the game's L2 block number, root claim, status, and game duration.
func (c *FaultDisputeGameContract) GetGameMetadata(ctx context.Context) (uint64, common.Hash, gameTypes.GameStatus, uint64, error) {
results, err := c.multiCaller.Call(ctx, rpcblock.Latest,
c.contract.Call(methodL2BlockNumber),
c.contract.Call(methodRootClaim),
c.contract.Call(methodStatus),
c.contract.Call(methodGameDuration))
func (f *FaultDisputeGameContract) GetGameMetadata(ctx context.Context, block rpcblock.Block) (uint64, common.Hash, gameTypes.GameStatus, uint64, error) {
results, err := f.multiCaller.Call(ctx, block,
f.contract.Call(methodL2BlockNumber),
f.contract.Call(methodRootClaim),
f.contract.Call(methodStatus),
f.contract.Call(methodGameDuration))
if err != nil {
return 0, common.Hash{}, 0, 0, fmt.Errorf("failed to retrieve game metadata: %w", err)
}
......@@ -104,26 +121,26 @@ func (c *FaultDisputeGameContract) GetGameMetadata(ctx context.Context) (uint64,
return l2BlockNumber, rootClaim, status, duration, nil
}
func (c *FaultDisputeGameContract) GetGenesisOutputRoot(ctx context.Context) (common.Hash, error) {
genesisOutputRoot, err := c.multiCaller.SingleCall(ctx, rpcblock.Latest, c.contract.Call(methodGenesisOutputRoot))
func (f *FaultDisputeGameContract) GetGenesisOutputRoot(ctx context.Context) (common.Hash, error) {
genesisOutputRoot, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodGenesisOutputRoot))
if err != nil {
return common.Hash{}, fmt.Errorf("failed to retrieve genesis output root: %w", err)
}
return genesisOutputRoot.GetHash(0), nil
}
func (c *FaultDisputeGameContract) GetSplitDepth(ctx context.Context) (types.Depth, error) {
splitDepth, err := c.multiCaller.SingleCall(ctx, rpcblock.Latest, c.contract.Call(methodSplitDepth))
func (f *FaultDisputeGameContract) GetSplitDepth(ctx context.Context) (types.Depth, error) {
splitDepth, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodSplitDepth))
if err != nil {
return 0, fmt.Errorf("failed to retrieve split depth: %w", err)
}
return types.Depth(splitDepth.GetBigInt(0).Uint64()), nil
}
func (c *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error) {
results, err := c.multiCaller.Call(ctx, rpcblock.Latest,
c.contract.Call(methodCredit, recipient),
c.contract.Call(methodStatus))
func (f *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error) {
results, err := f.multiCaller.Call(ctx, rpcblock.Latest,
f.contract.Call(methodCredit, recipient),
f.contract.Call(methodStatus))
if err != nil {
return nil, gameTypes.GameStatusInProgress, err
}
......@@ -138,12 +155,12 @@ func (c *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient comm
return credit, status, nil
}
func (c *FaultDisputeGameContract) GetCredits(ctx context.Context, block rpcblock.Block, recipients ...common.Address) ([]*big.Int, error) {
func (f *FaultDisputeGameContract) GetCredits(ctx context.Context, block rpcblock.Block, recipients ...common.Address) ([]*big.Int, error) {
calls := make([]batching.Call, 0, len(recipients))
for _, recipient := range recipients {
calls = append(calls, c.contract.Call(methodCredit, recipient))
calls = append(calls, f.contract.Call(methodCredit, recipient))
}
results, err := c.multiCaller.Call(ctx, block, calls...)
results, err := f.multiCaller.Call(ctx, block, calls...)
if err != nil {
return nil, fmt.Errorf("failed to retrieve credit: %w", err)
}
......@@ -159,8 +176,8 @@ func (f *FaultDisputeGameContract) ClaimCredit(recipient common.Address) (txmgr.
return call.ToTxCandidate()
}
func (c *FaultDisputeGameContract) GetRequiredBond(ctx context.Context, position types.Position) (*big.Int, error) {
bond, err := c.multiCaller.SingleCall(ctx, rpcblock.Latest, c.contract.Call(methodRequiredBond, position.ToGIndex()))
func (f *FaultDisputeGameContract) GetRequiredBond(ctx context.Context, position types.Position) (*big.Int, error) {
bond, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodRequiredBond, position.ToGIndex()))
if err != nil {
return nil, fmt.Errorf("failed to retrieve required bond: %w", err)
}
......@@ -256,8 +273,8 @@ func (f *FaultDisputeGameContract) GetClaim(ctx context.Context, idx uint64) (ty
return f.decodeClaim(result, int(idx)), nil
}
func (f *FaultDisputeGameContract) GetAllClaims(ctx context.Context) ([]types.Claim, error) {
results, err := batching.ReadArray(ctx, f.multiCaller, rpcblock.Latest, f.contract.Call(methodClaimCount), func(i *big.Int) *batching.ContractCall {
func (f *FaultDisputeGameContract) GetAllClaims(ctx context.Context, block rpcblock.Block) ([]types.Claim, error) {
results, err := batching.ReadArray(ctx, f.multiCaller, block, f.contract.Call(methodClaimCount), func(i *big.Int) *batching.ContractCall {
return f.contract.Call(methodClaim, i)
})
if err != nil {
......
......@@ -219,15 +219,30 @@ func TestGetAllClaims(t *testing.T) {
ParentContractIndex: 1,
}
expectedClaims := []faultTypes.Claim{claim0, claim1, claim2}
stubRpc.SetResponse(fdgAddr, methodClaimCount, rpcblock.Latest, nil, []interface{}{big.NewInt(int64(len(expectedClaims)))})
block := rpcblock.ByNumber(42)
stubRpc.SetResponse(fdgAddr, methodClaimCount, block, nil, []interface{}{big.NewInt(int64(len(expectedClaims)))})
for _, claim := range expectedClaims {
expectGetClaim(stubRpc, claim)
expectGetClaim(stubRpc, block, claim)
}
claims, err := game.GetAllClaims(context.Background())
claims, err := game.GetAllClaims(context.Background(), block)
require.NoError(t, err)
require.Equal(t, expectedClaims, claims)
}
func TestGetBalance(t *testing.T) {
wethAddr := common.Address{0x11, 0x55, 0x66}
balance := big.NewInt(9995877)
block := rpcblock.ByNumber(424)
stubRpc, game := setupFaultDisputeGameTest(t)
stubRpc.SetResponse(fdgAddr, methodWETH, block, nil, []interface{}{wethAddr})
stubRpc.AddExpectedCall(batchingTest.NewGetBalanceCall(wethAddr, block, balance))
actualBalance, actualAddr, err := game.GetBalance(context.Background(), block)
require.NoError(t, err)
require.Equal(t, wethAddr, actualAddr)
require.Truef(t, balance.Cmp(actualBalance) == 0, "Expected balance %v but was %v", balance, actualBalance)
}
func TestCallResolveClaim(t *testing.T) {
stubRpc, game := setupFaultDisputeGameTest(t)
stubRpc.SetResponse(fdgAddr, methodResolveClaim, rpcblock.Latest, []interface{}{big.NewInt(123)}, nil)
......@@ -279,11 +294,11 @@ func TestStepTx(t *testing.T) {
stubRpc.VerifyTxCandidate(tx)
}
func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, claim faultTypes.Claim) {
func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, block rpcblock.Block, claim faultTypes.Claim) {
stubRpc.SetResponse(
fdgAddr,
methodClaim,
rpcblock.Latest,
block,
[]interface{}{big.NewInt(int64(claim.ContractIndex))},
[]interface{}{
uint32(claim.ParentContractIndex),
......@@ -323,11 +338,12 @@ func TestGetGameMetadata(t *testing.T) {
expectedGameDuration := uint64(456)
expectedRootClaim := common.Hash{0x01, 0x02}
expectedStatus := types.GameStatusChallengerWon
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)})
stubRpc.SetResponse(fdgAddr, methodRootClaim, rpcblock.Latest, nil, []interface{}{expectedRootClaim})
stubRpc.SetResponse(fdgAddr, methodStatus, rpcblock.Latest, nil, []interface{}{expectedStatus})
stubRpc.SetResponse(fdgAddr, methodGameDuration, rpcblock.Latest, nil, []interface{}{expectedGameDuration})
l2BlockNumber, rootClaim, status, duration, err := contract.GetGameMetadata(context.Background())
block := rpcblock.ByNumber(889)
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})
stubRpc.SetResponse(fdgAddr, methodGameDuration, block, nil, []interface{}{expectedGameDuration})
l2BlockNumber, rootClaim, status, duration, err := contract.GetGameMetadata(context.Background(), block)
require.NoError(t, err)
require.Equal(t, expectedL2BlockNumber, l2BlockNumber)
require.Equal(t, expectedRootClaim, rootClaim)
......
package extract
import (
"context"
"fmt"
"math/big"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common"
)
type BalanceCaller interface {
GetBalance(context.Context, rpcblock.Block) (*big.Int, common.Address, error)
}
type BalanceEnricher struct {
}
func NewBalanceEnricher() *BalanceEnricher {
return &BalanceEnricher{}
}
func (b *BalanceEnricher) Enrich(ctx context.Context, block rpcblock.Block, caller GameCaller, game *monTypes.EnrichedGameData) error {
balance, holdingAddr, err := caller.GetBalance(ctx, block)
if err != nil {
return fmt.Errorf("failed to fetch balance: %w", err)
}
game.ETHCollateral = balance
game.WETHContract = holdingAddr
return nil
}
package extract
import (
"context"
"errors"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestBalanceEnricher(t *testing.T) {
t.Run("GetBalanceError", func(t *testing.T) {
enricher := NewBalanceEnricher()
caller := &mockGameCaller{balanceErr: errors.New("nope")}
game := &types.EnrichedGameData{}
err := enricher.Enrich(context.Background(), rpcblock.Latest, caller, game)
require.ErrorIs(t, err, caller.balanceErr)
})
t.Run("GetBalanceSuccess", func(t *testing.T) {
enricher := NewBalanceEnricher()
caller := &mockGameCaller{balance: big.NewInt(84242), balanceAddr: common.Address{0xdd}}
game := &types.EnrichedGameData{}
err := enricher.Enrich(context.Background(), rpcblock.Latest, caller, game)
require.NoError(t, err)
require.Equal(t, game.WETHContract, caller.balanceAddr)
require.Equal(t, game.ETHCollateral, caller.balance)
})
}
package extract
import (
"context"
"errors"
"fmt"
"math/big"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common"
"golang.org/x/exp/maps"
)
var ErrIncorrectCreditCount = errors.New("incorrect credit count")
type BondCaller interface {
GetCredits(context.Context, rpcblock.Block, ...common.Address) ([]*big.Int, error)
}
type BondEnricher struct {
}
func NewBondEnricher() *BondEnricher {
return &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 {
recipients[claim.Claimant] = true
if claim.CounteredBy != (common.Address{}) {
recipients[claim.CounteredBy] = true
}
}
recipientAddrs := maps.Keys(recipients)
credits, err := caller.GetCredits(ctx, block, recipientAddrs...)
if err != nil {
return err
}
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 {
game.Credits[recipientAddrs[i]] = credit
}
return nil
}
package extract
import (
"context"
"errors"
"math/big"
"testing"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestBondEnricher(t *testing.T) {
makeGame := func() *monTypes.EnrichedGameData {
return &monTypes.EnrichedGameData{
Claims: []faultTypes.Claim{
{
ClaimData: faultTypes.ClaimData{
Bond: monTypes.ResolvedBondAmount,
},
Claimant: common.Address{0x01},
CounteredBy: common.Address{0x02},
},
{
ClaimData: faultTypes.ClaimData{
Bond: big.NewInt(5),
},
Claimant: common.Address{0x03},
CounteredBy: common.Address{},
},
{
ClaimData: faultTypes.ClaimData{
Bond: big.NewInt(7),
},
Claimant: common.Address{0x03},
CounteredBy: common.Address{},
},
},
}
}
t.Run("GetCreditsFails", func(t *testing.T) {
enricher := NewBondEnricher()
caller := &mockGameCaller{creditsErr: errors.New("nope")}
game := makeGame()
err := enricher.Enrich(context.Background(), rpcblock.Latest, caller, game)
require.ErrorIs(t, err, caller.creditsErr)
})
t.Run("GetCreditsWrongNumberOfResults", func(t *testing.T) {
enricher := NewBondEnricher()
caller := &mockGameCaller{credits: []*big.Int{big.NewInt(4)}}
game := makeGame()
err := enricher.Enrich(context.Background(), rpcblock.Latest, caller, game)
require.ErrorIs(t, err, ErrIncorrectCreditCount)
})
t.Run("GetCreditsSuccess", func(t *testing.T) {
game := makeGame()
expectedRecipients := []common.Address{
game.Claims[0].Claimant,
game.Claims[0].CounteredBy,
game.Claims[1].Claimant,
// Claim 1 CounteredBy is unset
// Claim 2 Claimant is same as claim 1 Claimant
// Claim 2 CounteredBy is unset
}
enricher := NewBondEnricher()
credits := []*big.Int{big.NewInt(10), big.NewInt(20), big.NewInt(30)}
caller := &mockGameCaller{credits: credits}
err := enricher.Enrich(context.Background(), rpcblock.Latest, caller, game)
require.NoError(t, err)
require.Equal(t, expectedRecipients, caller.requestedCredits)
expectedCredits := map[common.Address]*big.Int{
expectedRecipients[0]: credits[0],
expectedRecipients[1]: credits[1],
expectedRecipients[2]: credits[2],
}
require.Equal(t, expectedCredits, game.Credits)
})
}
......@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
......@@ -16,8 +17,10 @@ import (
const metricsLabel = "game_caller_creator"
type GameCaller interface {
GetGameMetadata(context.Context) (uint64, common.Hash, types.GameStatus, uint64, error)
GetAllClaims(context.Context) ([]faultTypes.Claim, error)
GetGameMetadata(context.Context, rpcblock.Block) (uint64, common.Hash, types.GameStatus, uint64, error)
GetAllClaims(context.Context, rpcblock.Block) ([]faultTypes.Claim, error)
BondCaller
BalanceCaller
}
type GameCallerCreator struct {
......
......@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
......@@ -14,17 +15,23 @@ import (
type CreateGameCaller func(game gameTypes.GameMetadata) (GameCaller, error)
type FactoryGameFetcher func(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]gameTypes.GameMetadata, error)
type Enricher interface {
Enrich(ctx context.Context, block rpcblock.Block, caller GameCaller, game *monTypes.EnrichedGameData) error
}
type Extractor struct {
logger log.Logger
createContract CreateGameCaller
fetchGames FactoryGameFetcher
enrichers []Enricher
}
func NewExtractor(logger log.Logger, creator CreateGameCaller, fetchGames FactoryGameFetcher) *Extractor {
func NewExtractor(logger log.Logger, creator CreateGameCaller, fetchGames FactoryGameFetcher, enrichers ...Enricher) *Extractor {
return &Extractor{
logger: logger,
createContract: creator,
fetchGames: fetchGames,
enrichers: enrichers,
}
}
......@@ -33,35 +40,49 @@ func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimes
if err != nil {
return nil, fmt.Errorf("failed to load games: %w", err)
}
return e.enrichGames(ctx, games), nil
return e.enrichGames(ctx, blockHash, games), nil
}
func (e *Extractor) enrichGames(ctx context.Context, games []gameTypes.GameMetadata) []*monTypes.EnrichedGameData {
func (e *Extractor) enrichGames(ctx context.Context, blockHash common.Hash, games []gameTypes.GameMetadata) []*monTypes.EnrichedGameData {
var enrichedGames []*monTypes.EnrichedGameData
for _, game := range games {
caller, err := e.createContract(game)
if err != nil {
e.logger.Error("failed to create game caller", "err", err)
e.logger.Error("Failed to create game caller", "err", err)
continue
}
l2BlockNum, rootClaim, status, duration, err := caller.GetGameMetadata(ctx)
l2BlockNum, rootClaim, status, duration, err := caller.GetGameMetadata(ctx, rpcblock.ByHash(blockHash))
if err != nil {
e.logger.Error("failed to fetch game metadata", "err", err)
e.logger.Error("Failed to fetch game metadata", "err", err)
continue
}
claims, err := caller.GetAllClaims(ctx)
claims, err := caller.GetAllClaims(ctx, rpcblock.ByHash(blockHash))
if err != nil {
e.logger.Error("failed to fetch game claims", "err", err)
e.logger.Error("Failed to fetch game claims", "err", err)
continue
}
enrichedGames = append(enrichedGames, &monTypes.EnrichedGameData{
enrichedGame := &monTypes.EnrichedGameData{
GameMetadata: game,
L2BlockNumber: l2BlockNum,
RootClaim: rootClaim,
Status: status,
Duration: duration,
Claims: claims,
})
}
if err := e.applyEnrichers(ctx, blockHash, caller, enrichedGame); err != nil {
e.logger.Error("Failed to enrich game", "err", err)
continue
}
enrichedGames = append(enrichedGames, enrichedGame)
}
return enrichedGames
}
func (e *Extractor) applyEnrichers(ctx context.Context, blockHash common.Hash, caller GameCaller, game *monTypes.EnrichedGameData) error {
for _, enricher := range e.enrichers {
if err := enricher.Enrich(ctx, rpcblock.ByHash(blockHash), caller, game); err != nil {
return err
}
}
return nil
}
......@@ -3,8 +3,11 @@ package extract
import (
"context"
"errors"
"math/big"
"testing"
monTypes "github.com/ethereum-optimism/optimism/op-dispute-mon/mon/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/stretchr/testify/require"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
......@@ -79,37 +82,69 @@ func TestExtractor_Extract(t *testing.T) {
require.Equal(t, 1, creator.caller.metadataCalls)
require.Equal(t, 1, creator.caller.claimsCalls)
})
t.Run("EnricherFails", func(t *testing.T) {
enricher := &mockEnricher{err: errors.New("whoops")}
extractor, _, games, logs := setupExtractorTest(t, enricher)
games.games = []gameTypes.GameMetadata{{}}
enriched, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err)
l := logs.FindLogs(testlog.NewMessageFilter("Failed to enrich game"))
require.Len(t, l, 1, "Should have logged error")
require.Len(t, enriched, 0, "Should not return games that failed to enrich")
})
t.Run("EnricherSuccess", func(t *testing.T) {
enricher := &mockEnricher{}
extractor, _, games, _ := setupExtractorTest(t, enricher)
games.games = []gameTypes.GameMetadata{{}}
enriched, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err)
require.Len(t, enriched, 1)
require.Equal(t, 1, enricher.calls)
})
t.Run("MultipleEnrichersMultipleGames", func(t *testing.T) {
enricher1 := &mockEnricher{}
enricher2 := &mockEnricher{}
extractor, _, games, _ := setupExtractorTest(t, enricher1, enricher2)
games.games = []gameTypes.GameMetadata{{}, {}}
enriched, err := extractor.Extract(context.Background(), common.Hash{}, 0)
require.NoError(t, err)
require.Len(t, enriched, 2)
require.Equal(t, 2, enricher1.calls)
require.Equal(t, 2, enricher2.calls)
})
}
func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, metadataErr int, claimsErr int, durationErr int) {
errorLevelFilter := testlog.NewLevelFilter(log.LevelError)
createMessageFilter := testlog.NewMessageFilter("failed to create game caller")
createMessageFilter := testlog.NewMessageFilter("Failed to create game caller")
l := logs.FindLogs(errorLevelFilter, createMessageFilter)
require.Len(t, l, createErr)
fetchMessageFilter := testlog.NewMessageFilter("failed to fetch game metadata")
fetchMessageFilter := testlog.NewMessageFilter("Failed to fetch game metadata")
l = logs.FindLogs(errorLevelFilter, fetchMessageFilter)
require.Len(t, l, metadataErr)
claimsMessageFilter := testlog.NewMessageFilter("failed to fetch game claims")
claimsMessageFilter := testlog.NewMessageFilter("Failed to fetch game claims")
l = logs.FindLogs(errorLevelFilter, claimsMessageFilter)
require.Len(t, l, claimsErr)
durationMessageFilter := testlog.NewMessageFilter("failed to fetch game duration")
durationMessageFilter := testlog.NewMessageFilter("Failed to fetch game duration")
l = logs.FindLogs(errorLevelFilter, durationMessageFilter)
require.Len(t, l, durationErr)
}
func setupExtractorTest(t *testing.T) (*Extractor, *mockGameCallerCreator, *mockGameFetcher, *testlog.CapturingHandler) {
func setupExtractorTest(t *testing.T, enrichers ...Enricher) (*Extractor, *mockGameCallerCreator, *mockGameFetcher, *testlog.CapturingHandler) {
logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug)
games := &mockGameFetcher{}
caller := &mockGameCaller{rootClaim: mockRootClaim}
creator := &mockGameCallerCreator{caller: caller}
return NewExtractor(
logger,
creator.CreateGameCaller,
games.FetchGames,
),
creator,
games,
capturedLogs
extractor := NewExtractor(
logger,
creator.CreateGameCaller,
games.FetchGames,
enrichers...,
)
return extractor, creator, games, capturedLogs
}
type mockGameFetcher struct {
......@@ -141,15 +176,21 @@ func (m *mockGameCallerCreator) CreateGameCaller(_ gameTypes.GameMetadata) (Game
}
type mockGameCaller struct {
metadataCalls int
metadataErr error
claimsCalls int
claimsErr error
rootClaim common.Hash
claims []faultTypes.Claim
metadataCalls int
metadataErr error
claimsCalls int
claimsErr error
rootClaim common.Hash
claims []faultTypes.Claim
requestedCredits []common.Address
creditsErr error
credits []*big.Int
balanceErr error
balance *big.Int
balanceAddr common.Address
}
func (m *mockGameCaller) GetGameMetadata(_ context.Context) (uint64, common.Hash, types.GameStatus, uint64, error) {
func (m *mockGameCaller) GetGameMetadata(_ context.Context, _ rpcblock.Block) (uint64, common.Hash, types.GameStatus, uint64, error) {
m.metadataCalls++
if m.metadataErr != nil {
return 0, common.Hash{}, 0, 0, m.metadataErr
......@@ -157,10 +198,35 @@ func (m *mockGameCaller) GetGameMetadata(_ context.Context) (uint64, common.Hash
return 0, mockRootClaim, 0, 0, nil
}
func (m *mockGameCaller) GetAllClaims(ctx context.Context) ([]faultTypes.Claim, error) {
func (m *mockGameCaller) GetAllClaims(_ context.Context, _ rpcblock.Block) ([]faultTypes.Claim, error) {
m.claimsCalls++
if m.claimsErr != nil {
return nil, m.claimsErr
}
return m.claims, nil
}
func (m *mockGameCaller) GetCredits(_ context.Context, _ rpcblock.Block, recipients ...common.Address) ([]*big.Int, error) {
m.requestedCredits = recipients
if m.creditsErr != nil {
return nil, m.creditsErr
}
return m.credits, nil
}
func (m *mockGameCaller) GetBalance(_ context.Context, _ rpcblock.Block) (*big.Int, common.Address, error) {
if m.balanceErr != nil {
return nil, common.Address{}, m.balanceErr
}
return m.balance, m.balanceAddr, nil
}
type mockEnricher struct {
err error
calls int
}
func (m *mockEnricher) Enrich(_ context.Context, _ rpcblock.Block, _ GameCaller, _ *monTypes.EnrichedGameData) error {
m.calls++
return m.err
}
......@@ -112,7 +112,10 @@ func (s *Service) initDelayCalculator() {
}
func (s *Service) initExtractor() {
s.extractor = extract.NewExtractor(s.logger, s.game.CreateContract, s.factoryContract.GetGamesAtOrAfter)
s.extractor = extract.NewExtractor(s.logger, s.game.CreateContract, s.factoryContract.GetGamesAtOrAfter,
extract.NewBondEnricher(),
extract.NewBalanceEnricher(),
)
}
func (s *Service) initForecast(cfg *config.Config) {
......
......@@ -18,6 +18,17 @@ type EnrichedGameData struct {
Status types.GameStatus
Duration uint64
Claims []faultTypes.Claim
// Credits records the paid out bonds for the game, keyed by recipient.
Credits map[common.Address]*big.Int
// WETHContract is the address of the DelayedWETH contract used by this game
// The contract is potentially shared by multiple games.
WETHContract common.Address
// ETHCollateral is the ETH balance of the (potentially shared) WETHContract
// This ETH balance will be used to pay out any bonds required by the games that use the same DelayedWETH contract.
ETHCollateral *big.Int
}
// BidirectionalTree is a tree of claims represented as a flat list of claims.
......
......@@ -317,7 +317,7 @@ func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context,
return cannon.NewTraceProviderForTest(logger, metrics.NoopMetrics, cfg, localInputs, subdir, g.MaxDepth(ctx)-splitDepth-1), nil
})
claims, err := contract.GetAllClaims(ctx)
claims, err := contract.GetAllClaims(ctx, rpcblock.Latest)
g.require.NoError(err)
game := types.NewGameState(claims, g.MaxDepth(ctx))
......
......@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/stretchr/testify/require"
)
......@@ -92,7 +93,7 @@ func (h *OutputHonestHelper) StepFails(ctx context.Context, claimIdx int64, isAt
}
func (h *OutputHonestHelper) loadState(ctx context.Context, claimIdx int64) (types.Game, types.Claim) {
claims, err := h.contract.GetAllClaims(ctx)
claims, err := h.contract.GetAllClaims(ctx, rpcblock.Latest)
h.require.NoError(err, "Failed to load claims from game")
game := types.NewGameState(claims, h.game.MaxDepth(ctx))
......
......@@ -25,6 +25,10 @@ func NewBoundContract(abi *abi.ABI, addr common.Address) *BoundContract {
}
}
func (b *BoundContract) Addr() common.Address {
return b.addr
}
func (b *BoundContract) Call(method string, args ...interface{}) *ContractCall {
return NewContractCall(b.abi, b.addr, method, args...)
}
......
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