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 ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/dial"
oplog "github.com/ethereum-optimism/optimism/op-service/log" 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"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
...@@ -67,7 +68,7 @@ func listClaims(ctx context.Context, game *contracts.FaultDisputeGameContract) e ...@@ -67,7 +68,7 @@ func listClaims(ctx context.Context, game *contracts.FaultDisputeGameContract) e
return fmt.Errorf("failed to retrieve status: %w", err) return fmt.Errorf("failed to retrieve status: %w", err)
} }
claims, err := game.GetAllClaims(ctx) claims, err := game.GetAllClaims(ctx, rpcblock.Latest)
if err != nil { if err != nil {
return fmt.Errorf("failed to retrieve claims: %w", err) return fmt.Errorf("failed to retrieve claims: %w", err)
} }
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,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-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -26,7 +27,7 @@ type Responder interface { ...@@ -26,7 +27,7 @@ type Responder interface {
} }
type ClaimLoader interface { type ClaimLoader interface {
GetAllClaims(ctx context.Context) ([]types.Claim, error) GetAllClaims(ctx context.Context, block rpcblock.Block) ([]types.Claim, error)
} }
type Agent struct { type Agent struct {
...@@ -140,7 +141,7 @@ func (a *Agent) tryResolve(ctx context.Context) bool { ...@@ -140,7 +141,7 @@ func (a *Agent) tryResolve(ctx context.Context) bool {
var errNoResolvableClaims = errors.New("no resolvable claims") var errNoResolvableClaims = errors.New("no resolvable claims")
func (a *Agent) tryResolveClaims(ctx context.Context) error { 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 { if err != nil {
return fmt.Errorf("failed to fetch claims: %w", err) return fmt.Errorf("failed to fetch claims: %w", err)
} }
...@@ -202,7 +203,7 @@ func (a *Agent) resolveClaims(ctx context.Context) error { ...@@ -202,7 +203,7 @@ func (a *Agent) resolveClaims(ctx context.Context) error {
// newGameFromContracts initializes a new game state from the state in the contract // newGameFromContracts initializes a new game state from the state in the contract
func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) { 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 { if err != nil {
return nil, fmt.Errorf("failed to fetch claims: %w", err) return nil, fmt.Errorf("failed to fetch claims: %w", err)
} }
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace" "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/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -162,7 +163,7 @@ type stubClaimLoader struct { ...@@ -162,7 +163,7 @@ type stubClaimLoader struct {
claims []types.Claim 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++ s.callCount++
if s.callCount > s.maxLoads && s.maxLoads != 0 { if s.callCount > s.maxLoads && s.maxLoads != 0 {
return []types.Claim{}, nil return []types.Claim{}, nil
......
...@@ -38,6 +38,7 @@ var ( ...@@ -38,6 +38,7 @@ var (
methodRequiredBond = "getRequiredBond" methodRequiredBond = "getRequiredBond"
methodClaimCredit = "claimCredit" methodClaimCredit = "claimCredit"
methodCredit = "credit" methodCredit = "credit"
methodWETH = "weth"
) )
type FaultDisputeGameContract struct { type FaultDisputeGameContract struct {
...@@ -62,12 +63,28 @@ func NewFaultDisputeGameContract(addr common.Address, caller *batching.MultiCall ...@@ -62,12 +63,28 @@ func NewFaultDisputeGameContract(addr common.Address, caller *batching.MultiCall
}, nil }, 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) // 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). // 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) { func (f *FaultDisputeGameContract) GetBlockRange(ctx context.Context) (prestateBlock uint64, poststateBlock uint64, retErr error) {
results, err := c.multiCaller.Call(ctx, rpcblock.Latest, results, err := f.multiCaller.Call(ctx, rpcblock.Latest,
c.contract.Call(methodGenesisBlockNumber), f.contract.Call(methodGenesisBlockNumber),
c.contract.Call(methodL2BlockNumber)) f.contract.Call(methodL2BlockNumber))
if err != nil { if err != nil {
retErr = fmt.Errorf("failed to retrieve game block range: %w", err) retErr = fmt.Errorf("failed to retrieve game block range: %w", err)
return return
...@@ -82,12 +99,12 @@ func (c *FaultDisputeGameContract) GetBlockRange(ctx context.Context) (prestateB ...@@ -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. // 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) { func (f *FaultDisputeGameContract) GetGameMetadata(ctx context.Context, block rpcblock.Block) (uint64, common.Hash, gameTypes.GameStatus, uint64, error) {
results, err := c.multiCaller.Call(ctx, rpcblock.Latest, results, err := f.multiCaller.Call(ctx, block,
c.contract.Call(methodL2BlockNumber), f.contract.Call(methodL2BlockNumber),
c.contract.Call(methodRootClaim), f.contract.Call(methodRootClaim),
c.contract.Call(methodStatus), f.contract.Call(methodStatus),
c.contract.Call(methodGameDuration)) f.contract.Call(methodGameDuration))
if err != nil { if err != nil {
return 0, common.Hash{}, 0, 0, fmt.Errorf("failed to retrieve game metadata: %w", err) 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, ...@@ -104,26 +121,26 @@ func (c *FaultDisputeGameContract) GetGameMetadata(ctx context.Context) (uint64,
return l2BlockNumber, rootClaim, status, duration, nil return l2BlockNumber, rootClaim, status, duration, nil
} }
func (c *FaultDisputeGameContract) GetGenesisOutputRoot(ctx context.Context) (common.Hash, error) { func (f *FaultDisputeGameContract) GetGenesisOutputRoot(ctx context.Context) (common.Hash, error) {
genesisOutputRoot, err := c.multiCaller.SingleCall(ctx, rpcblock.Latest, c.contract.Call(methodGenesisOutputRoot)) genesisOutputRoot, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodGenesisOutputRoot))
if err != nil { if err != nil {
return common.Hash{}, fmt.Errorf("failed to retrieve genesis output root: %w", err) return common.Hash{}, fmt.Errorf("failed to retrieve genesis output root: %w", err)
} }
return genesisOutputRoot.GetHash(0), nil return genesisOutputRoot.GetHash(0), nil
} }
func (c *FaultDisputeGameContract) GetSplitDepth(ctx context.Context) (types.Depth, error) { func (f *FaultDisputeGameContract) GetSplitDepth(ctx context.Context) (types.Depth, error) {
splitDepth, err := c.multiCaller.SingleCall(ctx, rpcblock.Latest, c.contract.Call(methodSplitDepth)) splitDepth, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodSplitDepth))
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to retrieve split depth: %w", err) return 0, fmt.Errorf("failed to retrieve split depth: %w", err)
} }
return types.Depth(splitDepth.GetBigInt(0).Uint64()), nil return types.Depth(splitDepth.GetBigInt(0).Uint64()), nil
} }
func (c *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error) { func (f *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient common.Address) (*big.Int, gameTypes.GameStatus, error) {
results, err := c.multiCaller.Call(ctx, rpcblock.Latest, results, err := f.multiCaller.Call(ctx, rpcblock.Latest,
c.contract.Call(methodCredit, recipient), f.contract.Call(methodCredit, recipient),
c.contract.Call(methodStatus)) f.contract.Call(methodStatus))
if err != nil { if err != nil {
return nil, gameTypes.GameStatusInProgress, err return nil, gameTypes.GameStatusInProgress, err
} }
...@@ -138,12 +155,12 @@ func (c *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient comm ...@@ -138,12 +155,12 @@ func (c *FaultDisputeGameContract) GetCredit(ctx context.Context, recipient comm
return credit, status, nil 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)) calls := make([]batching.Call, 0, len(recipients))
for _, recipient := range 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 { if err != nil {
return nil, fmt.Errorf("failed to retrieve credit: %w", err) return nil, fmt.Errorf("failed to retrieve credit: %w", err)
} }
...@@ -159,8 +176,8 @@ func (f *FaultDisputeGameContract) ClaimCredit(recipient common.Address) (txmgr. ...@@ -159,8 +176,8 @@ func (f *FaultDisputeGameContract) ClaimCredit(recipient common.Address) (txmgr.
return call.ToTxCandidate() return call.ToTxCandidate()
} }
func (c *FaultDisputeGameContract) GetRequiredBond(ctx context.Context, position types.Position) (*big.Int, error) { func (f *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())) bond, err := f.multiCaller.SingleCall(ctx, rpcblock.Latest, f.contract.Call(methodRequiredBond, position.ToGIndex()))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve required bond: %w", err) 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 ...@@ -256,8 +273,8 @@ func (f *FaultDisputeGameContract) GetClaim(ctx context.Context, idx uint64) (ty
return f.decodeClaim(result, int(idx)), nil return f.decodeClaim(result, int(idx)), nil
} }
func (f *FaultDisputeGameContract) GetAllClaims(ctx context.Context) ([]types.Claim, error) { func (f *FaultDisputeGameContract) GetAllClaims(ctx context.Context, block rpcblock.Block) ([]types.Claim, error) {
results, err := batching.ReadArray(ctx, f.multiCaller, rpcblock.Latest, f.contract.Call(methodClaimCount), func(i *big.Int) *batching.ContractCall { results, err := batching.ReadArray(ctx, f.multiCaller, block, f.contract.Call(methodClaimCount), func(i *big.Int) *batching.ContractCall {
return f.contract.Call(methodClaim, i) return f.contract.Call(methodClaim, i)
}) })
if err != nil { if err != nil {
......
...@@ -219,15 +219,30 @@ func TestGetAllClaims(t *testing.T) { ...@@ -219,15 +219,30 @@ func TestGetAllClaims(t *testing.T) {
ParentContractIndex: 1, ParentContractIndex: 1,
} }
expectedClaims := []faultTypes.Claim{claim0, claim1, claim2} 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 { 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.NoError(t, err)
require.Equal(t, expectedClaims, claims) 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) { func TestCallResolveClaim(t *testing.T) {
stubRpc, game := setupFaultDisputeGameTest(t) stubRpc, game := setupFaultDisputeGameTest(t)
stubRpc.SetResponse(fdgAddr, methodResolveClaim, rpcblock.Latest, []interface{}{big.NewInt(123)}, nil) stubRpc.SetResponse(fdgAddr, methodResolveClaim, rpcblock.Latest, []interface{}{big.NewInt(123)}, nil)
...@@ -279,11 +294,11 @@ func TestStepTx(t *testing.T) { ...@@ -279,11 +294,11 @@ func TestStepTx(t *testing.T) {
stubRpc.VerifyTxCandidate(tx) stubRpc.VerifyTxCandidate(tx)
} }
func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, claim faultTypes.Claim) { func expectGetClaim(stubRpc *batchingTest.AbiBasedRpc, block rpcblock.Block, claim faultTypes.Claim) {
stubRpc.SetResponse( stubRpc.SetResponse(
fdgAddr, fdgAddr,
methodClaim, methodClaim,
rpcblock.Latest, block,
[]interface{}{big.NewInt(int64(claim.ContractIndex))}, []interface{}{big.NewInt(int64(claim.ContractIndex))},
[]interface{}{ []interface{}{
uint32(claim.ParentContractIndex), uint32(claim.ParentContractIndex),
...@@ -323,11 +338,12 @@ func TestGetGameMetadata(t *testing.T) { ...@@ -323,11 +338,12 @@ func TestGetGameMetadata(t *testing.T) {
expectedGameDuration := uint64(456) expectedGameDuration := uint64(456)
expectedRootClaim := common.Hash{0x01, 0x02} expectedRootClaim := common.Hash{0x01, 0x02}
expectedStatus := types.GameStatusChallengerWon expectedStatus := types.GameStatusChallengerWon
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)}) block := rpcblock.ByNumber(889)
stubRpc.SetResponse(fdgAddr, methodRootClaim, rpcblock.Latest, nil, []interface{}{expectedRootClaim}) stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)})
stubRpc.SetResponse(fdgAddr, methodStatus, rpcblock.Latest, nil, []interface{}{expectedStatus}) stubRpc.SetResponse(fdgAddr, methodRootClaim, block, nil, []interface{}{expectedRootClaim})
stubRpc.SetResponse(fdgAddr, methodGameDuration, rpcblock.Latest, nil, []interface{}{expectedGameDuration}) stubRpc.SetResponse(fdgAddr, methodStatus, block, nil, []interface{}{expectedStatus})
l2BlockNumber, rootClaim, status, duration, err := contract.GetGameMetadata(context.Background()) stubRpc.SetResponse(fdgAddr, methodGameDuration, block, nil, []interface{}{expectedGameDuration})
l2BlockNumber, rootClaim, status, duration, err := contract.GetGameMetadata(context.Background(), block)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedL2BlockNumber, l2BlockNumber) require.Equal(t, expectedL2BlockNumber, l2BlockNumber)
require.Equal(t, expectedRootClaim, rootClaim) 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 ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
...@@ -16,8 +17,10 @@ import ( ...@@ -16,8 +17,10 @@ import (
const metricsLabel = "game_caller_creator" const metricsLabel = "game_caller_creator"
type GameCaller interface { type GameCaller interface {
GetGameMetadata(context.Context) (uint64, common.Hash, types.GameStatus, uint64, error) GetGameMetadata(context.Context, rpcblock.Block) (uint64, common.Hash, types.GameStatus, uint64, error)
GetAllClaims(context.Context) ([]faultTypes.Claim, error) GetAllClaims(context.Context, rpcblock.Block) ([]faultTypes.Claim, error)
BondCaller
BalanceCaller
} }
type GameCallerCreator struct { type GameCallerCreator struct {
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -14,17 +15,23 @@ import ( ...@@ -14,17 +15,23 @@ import (
type CreateGameCaller func(game gameTypes.GameMetadata) (GameCaller, error) type CreateGameCaller func(game gameTypes.GameMetadata) (GameCaller, error)
type FactoryGameFetcher func(ctx context.Context, blockHash common.Hash, earliestTimestamp uint64) ([]gameTypes.GameMetadata, 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 { type Extractor struct {
logger log.Logger logger log.Logger
createContract CreateGameCaller createContract CreateGameCaller
fetchGames FactoryGameFetcher 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{ return &Extractor{
logger: logger, logger: logger,
createContract: creator, createContract: creator,
fetchGames: fetchGames, fetchGames: fetchGames,
enrichers: enrichers,
} }
} }
...@@ -33,35 +40,49 @@ func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimes ...@@ -33,35 +40,49 @@ func (e *Extractor) Extract(ctx context.Context, blockHash common.Hash, minTimes
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load games: %w", err) 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 var enrichedGames []*monTypes.EnrichedGameData
for _, game := range games { for _, game := range games {
caller, err := e.createContract(game) caller, err := e.createContract(game)
if err != nil { if err != nil {
e.logger.Error("failed to create game caller", "err", err) e.logger.Error("Failed to create game caller", "err", err)
continue continue
} }
l2BlockNum, rootClaim, status, duration, err := caller.GetGameMetadata(ctx) l2BlockNum, rootClaim, status, duration, err := caller.GetGameMetadata(ctx, rpcblock.ByHash(blockHash))
if err != nil { if err != nil {
e.logger.Error("failed to fetch game metadata", "err", err) e.logger.Error("Failed to fetch game metadata", "err", err)
continue continue
} }
claims, err := caller.GetAllClaims(ctx) claims, err := caller.GetAllClaims(ctx, rpcblock.ByHash(blockHash))
if err != nil { if err != nil {
e.logger.Error("failed to fetch game claims", "err", err) e.logger.Error("Failed to fetch game claims", "err", err)
continue continue
} }
enrichedGames = append(enrichedGames, &monTypes.EnrichedGameData{ enrichedGame := &monTypes.EnrichedGameData{
GameMetadata: game, GameMetadata: game,
L2BlockNumber: l2BlockNum, L2BlockNumber: l2BlockNum,
RootClaim: rootClaim, RootClaim: rootClaim,
Status: status, Status: status,
Duration: duration, Duration: duration,
Claims: claims, 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 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 ...@@ -3,8 +3,11 @@ package extract
import ( import (
"context" "context"
"errors" "errors"
"math/big"
"testing" "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" "github.com/stretchr/testify/require"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
...@@ -79,37 +82,69 @@ func TestExtractor_Extract(t *testing.T) { ...@@ -79,37 +82,69 @@ func TestExtractor_Extract(t *testing.T) {
require.Equal(t, 1, creator.caller.metadataCalls) require.Equal(t, 1, creator.caller.metadataCalls)
require.Equal(t, 1, creator.caller.claimsCalls) 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) { func verifyLogs(t *testing.T, logs *testlog.CapturingHandler, createErr int, metadataErr int, claimsErr int, durationErr int) {
errorLevelFilter := testlog.NewLevelFilter(log.LevelError) 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) l := logs.FindLogs(errorLevelFilter, createMessageFilter)
require.Len(t, l, createErr) 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) l = logs.FindLogs(errorLevelFilter, fetchMessageFilter)
require.Len(t, l, metadataErr) 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) l = logs.FindLogs(errorLevelFilter, claimsMessageFilter)
require.Len(t, l, claimsErr) 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) l = logs.FindLogs(errorLevelFilter, durationMessageFilter)
require.Len(t, l, durationErr) 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) logger, capturedLogs := testlog.CaptureLogger(t, log.LvlDebug)
games := &mockGameFetcher{} games := &mockGameFetcher{}
caller := &mockGameCaller{rootClaim: mockRootClaim} caller := &mockGameCaller{rootClaim: mockRootClaim}
creator := &mockGameCallerCreator{caller: caller} creator := &mockGameCallerCreator{caller: caller}
return NewExtractor( extractor := NewExtractor(
logger, logger,
creator.CreateGameCaller, creator.CreateGameCaller,
games.FetchGames, games.FetchGames,
), enrichers...,
creator, )
games, return extractor, creator, games, capturedLogs
capturedLogs
} }
type mockGameFetcher struct { type mockGameFetcher struct {
...@@ -141,15 +176,21 @@ func (m *mockGameCallerCreator) CreateGameCaller(_ gameTypes.GameMetadata) (Game ...@@ -141,15 +176,21 @@ func (m *mockGameCallerCreator) CreateGameCaller(_ gameTypes.GameMetadata) (Game
} }
type mockGameCaller struct { type mockGameCaller struct {
metadataCalls int metadataCalls int
metadataErr error metadataErr error
claimsCalls int claimsCalls int
claimsErr error claimsErr error
rootClaim common.Hash rootClaim common.Hash
claims []faultTypes.Claim 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++ m.metadataCalls++
if m.metadataErr != nil { if m.metadataErr != nil {
return 0, common.Hash{}, 0, 0, m.metadataErr return 0, common.Hash{}, 0, 0, m.metadataErr
...@@ -157,10 +198,35 @@ func (m *mockGameCaller) GetGameMetadata(_ context.Context) (uint64, common.Hash ...@@ -157,10 +198,35 @@ func (m *mockGameCaller) GetGameMetadata(_ context.Context) (uint64, common.Hash
return 0, mockRootClaim, 0, 0, nil 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++ m.claimsCalls++
if m.claimsErr != nil { if m.claimsErr != nil {
return nil, m.claimsErr return nil, m.claimsErr
} }
return m.claims, nil 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() { ...@@ -112,7 +112,10 @@ func (s *Service) initDelayCalculator() {
} }
func (s *Service) initExtractor() { 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) { func (s *Service) initForecast(cfg *config.Config) {
......
...@@ -18,6 +18,17 @@ type EnrichedGameData struct { ...@@ -18,6 +18,17 @@ type EnrichedGameData struct {
Status types.GameStatus Status types.GameStatus
Duration uint64 Duration uint64
Claims []faultTypes.Claim 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. // BidirectionalTree is a tree of claims represented as a flat list of claims.
......
...@@ -317,7 +317,7 @@ func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context, ...@@ -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 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) g.require.NoError(err)
game := types.NewGameState(claims, g.MaxDepth(ctx)) game := types.NewGameState(claims, g.MaxDepth(ctx))
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "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-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -92,7 +93,7 @@ func (h *OutputHonestHelper) StepFails(ctx context.Context, claimIdx int64, isAt ...@@ -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) { 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") h.require.NoError(err, "Failed to load claims from game")
game := types.NewGameState(claims, h.game.MaxDepth(ctx)) game := types.NewGameState(claims, h.game.MaxDepth(ctx))
......
...@@ -25,6 +25,10 @@ func NewBoundContract(abi *abi.ABI, addr common.Address) *BoundContract { ...@@ -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 { func (b *BoundContract) Call(method string, args ...interface{}) *ContractCall {
return NewContractCall(b.abi, b.addr, method, args...) 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