Commit b7c02b49 authored by OptimismBot's avatar OptimismBot Committed by GitHub

Merge pull request #6186 from ethereum-optimism/refcell/claimloader

feat(op-challenger): Claim Loader
parents 7af00426 4e058691
......@@ -17,9 +17,13 @@ type Game interface {
// Put adds a claim into the game state.
Put(claim Claim) error
// PutAll adds a list of claims into the game state.
PutAll(claims []Claim) error
// Claims returns all of the claims in the game.
Claims() []Claim
// IsDuplicate returns true if the provided [Claim] already exists in the game state.
IsDuplicate(claim Claim) bool
}
......@@ -88,6 +92,17 @@ func (g *gameState) recurseTree(treeNode *Node, claim ClaimData) (*Node, error)
return nil, ErrClaimNotFound
}
// PutAll adds a list of claims into the [Game] state.
// If any of the claims already exist in the game state, an error is returned.
func (g *gameState) PutAll(claims []Claim) error {
for _, claim := range claims {
if err := g.Put(claim); err != nil {
return err
}
}
return nil
}
// Put adds a claim into the game state.
func (g *gameState) Put(claim Claim) error {
// The game is always initialized with a root claim. Cannot add a second.
......
......@@ -50,6 +50,56 @@ func TestIsDuplicate(t *testing.T) {
require.False(t, g.IsDuplicate(bottom))
}
// TestGame_PutAll_RootAlreadyExists tests the [Game.PutAll] method using a [gameState]
// instance errors when the root claim already exists in state.
func TestGame_PutAll_RootAlreadyExists(t *testing.T) {
// Setup the game state.
top, _, _ := createTestClaims()
g := NewGameState(top)
// Try to put the root claim into the game state again.
err := g.PutAll([]Claim{top})
require.ErrorIs(t, err, ErrClaimExists)
}
// TestGame_PutAll_AlreadyExists tests the [Game.PutAll] method using a [gameState]
// instance errors when the given claim already exists in state.
func TestGame_PutAll_AlreadyExists(t *testing.T) {
// Setup the game state.
top, middle, _ := createTestClaims()
g := NewGameState(top)
// Put the next claim into state.
err := g.PutAll([]Claim{middle})
require.NoError(t, err)
// Try to put the root claim into the game state again.
err = g.PutAll([]Claim{middle})
require.ErrorIs(t, err, ErrClaimExists)
}
// TestGame_PutAll_ParentsAndChildren tests the [Game.PutAll] method using a [gameState] instance.
func TestGame_PutAll_ParentsAndChildren(t *testing.T) {
// Setup the game state.
top, middle, bottom := createTestClaims()
g := NewGameState(top)
// We should not be able to get the parent of the root claim.
parent, err := g.getParent(top)
require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{})
// Put the rest of the claims in the state.
err = g.PutAll([]Claim{middle, bottom})
require.NoError(t, err)
parent, err = g.getParent(middle)
require.NoError(t, err)
require.Equal(t, parent, top)
parent, err = g.getParent(bottom)
require.NoError(t, err)
require.Equal(t, parent, middle)
}
// TestGame_Put_RootAlreadyExists tests the [Game.Put] method using a [gameState]
// instance errors when the root claim already exists in state.
func TestGame_Put_RootAlreadyExists(t *testing.T) {
......
package fault
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/log"
)
// ClaimFetcher is a minimal interface around [bindings.FaultDisputeGameCaller].
// This needs to be updated if the [bindings.FaultDisputeGameCaller] interface changes.
type ClaimFetcher interface {
ClaimData(opts *bind.CallOpts, arg0 *big.Int) (struct {
ParentIndex uint32
Countered bool
Claim [32]byte
Position *big.Int
Clock *big.Int
}, error)
ClaimDataLen(opts *bind.CallOpts) (*big.Int, error)
}
// Loader is a minimal interface for loading onchain [Claim] data.
type Loader interface {
FetchClaims(ctx context.Context) ([]Claim, error)
}
// loader pulls in fault dispute game claim data periodically and over subscriptions.
type loader struct {
log log.Logger
state Game
claimFetcher ClaimFetcher
}
// NewLoader creates a new [loader].
func NewLoader(log log.Logger, state Game, claimFetcher ClaimFetcher) *loader {
return &loader{
log: log,
state: state,
claimFetcher: claimFetcher,
}
}
// fetchClaim fetches a single [Claim] with a hydrated parent.
func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (Claim, error) {
callOpts := bind.CallOpts{
Context: ctx,
}
fetchedClaim, err := l.claimFetcher.ClaimData(&callOpts, new(big.Int).SetUint64(arrIndex))
if err != nil {
return Claim{}, err
}
claim := Claim{
ClaimData: ClaimData{
Value: fetchedClaim.Claim,
Position: NewPositionFromGIndex(fetchedClaim.Position.Uint64()),
},
}
if !claim.IsRootPosition() {
parentIndex := uint64(fetchedClaim.ParentIndex)
parentClaim, err := l.claimFetcher.ClaimData(&callOpts, new(big.Int).SetUint64(parentIndex))
if err != nil {
return Claim{}, err
}
claim.Parent = ClaimData{
Value: parentClaim.Claim,
Position: NewPositionFromGIndex(parentClaim.Position.Uint64()),
}
}
return claim, nil
}
// FetchClaims fetches all claims from the fault dispute game.
func (l *loader) FetchClaims(ctx context.Context) ([]Claim, error) {
// Get the current claim count.
claimCount, err := l.claimFetcher.ClaimDataLen(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
// Fetch each claim and build a list.
claimList := make([]Claim, claimCount.Uint64())
for i := uint64(0); i < claimCount.Uint64(); i++ {
claim, err := l.fetchClaim(ctx, i)
if err != nil {
return nil, err
}
claimList[i] = claim
}
return claimList, nil
}
package fault
import (
"context"
"fmt"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var (
mockClaimDataError = fmt.Errorf("claim data errored")
mockClaimLenError = fmt.Errorf("claim len errored")
mockPutError = fmt.Errorf("put errored")
)
type mockGameState struct {
putCalled int
putErrors bool
}
func (m *mockGameState) Put(claim Claim) error {
m.putCalled++
if m.putErrors {
return mockPutError
}
return nil
}
func (m *mockGameState) PutAll(claims []Claim) error {
m.putCalled += len(claims)
if m.putErrors {
return mockPutError
}
return nil
}
func (m *mockGameState) Claims() []Claim {
return []Claim{}
}
func (m *mockGameState) IsDuplicate(claim Claim) bool {
return false
}
type mockClaimFetcher struct {
claimDataError bool
claimLenError bool
currentIndex uint64
returnClaims []struct {
ParentIndex uint32
Countered bool
Claim [32]byte
Position *big.Int
Clock *big.Int
}
}
func newMockClaimFetcher() *mockClaimFetcher {
return &mockClaimFetcher{
returnClaims: []struct {
ParentIndex uint32
Countered bool
Claim [32]byte
Position *big.Int
Clock *big.Int
}{
{
Claim: [32]byte{0x00},
Position: big.NewInt(0),
},
{
Claim: [32]byte{0x01},
Position: big.NewInt(0),
},
{
Claim: [32]byte{0x02},
Position: big.NewInt(0),
},
},
}
}
func (m *mockClaimFetcher) ClaimData(opts *bind.CallOpts, arg0 *big.Int) (struct {
ParentIndex uint32
Countered bool
Claim [32]byte
Position *big.Int
Clock *big.Int
}, error) {
if m.claimDataError {
return struct {
ParentIndex uint32
Countered bool
Claim [32]byte
Position *big.Int
Clock *big.Int
}{}, mockClaimDataError
}
returnClaim := m.returnClaims[m.currentIndex]
m.currentIndex++
return returnClaim, nil
}
func (m *mockClaimFetcher) ClaimDataLen(opts *bind.CallOpts) (*big.Int, error) {
if m.claimLenError {
return big.NewInt(0), mockClaimLenError
}
return big.NewInt(int64(len(m.returnClaims))), nil
}
// TestLoader_FetchClaims_Succeeds tests [loader.FetchClaims].
func TestLoader_FetchClaims_Succeeds(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
mockClaimFetcher := newMockClaimFetcher()
expectedClaims := mockClaimFetcher.returnClaims
loader := NewLoader(log, &mockGameState{}, mockClaimFetcher)
claims, err := loader.FetchClaims(context.Background())
require.NoError(t, err)
require.ElementsMatch(t, []Claim{
{
ClaimData: ClaimData{
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[0].Position.Uint64()),
},
Parent: ClaimData{
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[0].Position.Uint64()),
},
},
{
ClaimData: ClaimData{
Value: expectedClaims[1].Claim,
Position: NewPositionFromGIndex(expectedClaims[1].Position.Uint64()),
},
Parent: ClaimData{
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[1].Position.Uint64()),
},
},
{
ClaimData: ClaimData{
Value: expectedClaims[2].Claim,
Position: NewPositionFromGIndex(expectedClaims[2].Position.Uint64()),
},
Parent: ClaimData{
Value: expectedClaims[0].Claim,
Position: NewPositionFromGIndex(expectedClaims[2].Position.Uint64()),
},
},
}, claims)
}
// TestLoader_FetchClaims_ClaimDataErrors tests [loader.FetchClaims]
// when the claim fetcher [ClaimData] function call errors.
func TestLoader_FetchClaims_ClaimDataErrors(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
mockClaimFetcher := newMockClaimFetcher()
mockClaimFetcher.claimDataError = true
loader := NewLoader(log, &mockGameState{}, mockClaimFetcher)
claims, err := loader.FetchClaims(context.Background())
require.ErrorIs(t, err, mockClaimDataError)
require.Empty(t, claims)
}
// TestLoader_FetchClaims_ClaimLenErrors tests [loader.FetchClaims]
// when the claim fetcher [ClaimDataLen] function call errors.
func TestLoader_FetchClaims_ClaimLenErrors(t *testing.T) {
log := testlog.Logger(t, log.LvlError)
mockClaimFetcher := newMockClaimFetcher()
mockClaimFetcher.claimLenError = true
loader := NewLoader(log, &mockGameState{}, mockClaimFetcher)
claims, err := loader.FetchClaims(context.Background())
require.ErrorIs(t, err, mockClaimLenError)
require.Empty(t, claims)
}
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