Commit cece372b authored by refcell.eth's avatar refcell.eth Committed by GitHub

Merge pull request #7517 from ethereum-optimism/aj/simplify-game

op-challenger: Simplify game implementation
parents 8064f0c9 3cedb234
...@@ -196,9 +196,6 @@ func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) { ...@@ -196,9 +196,6 @@ func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
if len(claims) == 0 { if len(claims) == 0 {
return nil, errors.New("no claims") return nil, errors.New("no claims")
} }
game := types.NewGameState(a.agreeWithProposedOutput, claims[0], uint64(a.maxDepth)) game := types.NewGameState(a.agreeWithProposedOutput, claims, uint64(a.maxDepth))
if err := game.PutAll(claims[1:]); err != nil {
return nil, fmt.Errorf("failed to load claims into the local state: %w", err)
}
return game, nil return game, nil
} }
...@@ -6,15 +6,17 @@ import ( ...@@ -6,15 +6,17 @@ import (
) )
type GameBuilder struct { type GameBuilder struct {
builder *ClaimBuilder builder *ClaimBuilder
Game types.Game Game types.Game
ExpectedActions []types.Action ExpectedActions []types.Action
agreeWithOutputRoot bool
} }
func (c *ClaimBuilder) GameBuilder(agreeWithOutputRoot bool, rootCorrect bool) *GameBuilder { func (c *ClaimBuilder) GameBuilder(agreeWithOutputRoot bool, rootCorrect bool) *GameBuilder {
return &GameBuilder{ return &GameBuilder{
builder: c, builder: c,
Game: types.NewGameState(agreeWithOutputRoot, c.CreateRootClaim(rootCorrect), uint64(c.maxDepth)), agreeWithOutputRoot: agreeWithOutputRoot,
Game: types.NewGameState(agreeWithOutputRoot, []types.Claim{c.CreateRootClaim(rootCorrect)}, uint64(c.maxDepth)),
} }
} }
...@@ -22,62 +24,60 @@ type GameBuilderSeq struct { ...@@ -22,62 +24,60 @@ type GameBuilderSeq struct {
gameBuilder *GameBuilder gameBuilder *GameBuilder
builder *ClaimBuilder builder *ClaimBuilder
lastClaim types.Claim lastClaim types.Claim
game types.Game
} }
func (g *GameBuilder) Seq() *GameBuilderSeq { func (g *GameBuilder) Seq() *GameBuilderSeq {
return &GameBuilderSeq{ return &GameBuilderSeq{
gameBuilder: g, gameBuilder: g,
builder: g.builder, builder: g.builder,
game: g.Game,
lastClaim: g.Game.Claims()[0], lastClaim: g.Game.Claims()[0],
} }
} }
// addClaimToGame replaces the game being built with a new instance that has claim as the latest claim.
// The ContractIndex in claim is updated with its position in the game's claim array.
func (s *GameBuilderSeq) addClaimToGame(claim *types.Claim) {
claim.ContractIndex = len(s.gameBuilder.Game.Claims())
claims := append(s.gameBuilder.Game.Claims(), *claim)
s.gameBuilder.Game = types.NewGameState(s.gameBuilder.agreeWithOutputRoot, claims, uint64(s.builder.maxDepth))
}
func (s *GameBuilderSeq) AttackCorrect() *GameBuilderSeq { func (s *GameBuilderSeq) AttackCorrect() *GameBuilderSeq {
claim := s.builder.AttackClaim(s.lastClaim, true) claim := s.builder.AttackClaim(s.lastClaim, true)
claim.ContractIndex = len(s.game.Claims()) s.addClaimToGame(&claim)
s.builder.require.NoError(s.game.Put(claim))
return &GameBuilderSeq{ return &GameBuilderSeq{
gameBuilder: s.gameBuilder, gameBuilder: s.gameBuilder,
builder: s.builder, builder: s.builder,
game: s.game,
lastClaim: claim, lastClaim: claim,
} }
} }
func (s *GameBuilderSeq) Attack(value common.Hash) *GameBuilderSeq { func (s *GameBuilderSeq) Attack(value common.Hash) *GameBuilderSeq {
claim := s.builder.AttackClaimWithValue(s.lastClaim, value) claim := s.builder.AttackClaimWithValue(s.lastClaim, value)
claim.ContractIndex = len(s.game.Claims()) s.addClaimToGame(&claim)
s.builder.require.NoError(s.game.Put(claim))
return &GameBuilderSeq{ return &GameBuilderSeq{
gameBuilder: s.gameBuilder, gameBuilder: s.gameBuilder,
builder: s.builder, builder: s.builder,
game: s.game,
lastClaim: claim, lastClaim: claim,
} }
} }
func (s *GameBuilderSeq) DefendCorrect() *GameBuilderSeq { func (s *GameBuilderSeq) DefendCorrect() *GameBuilderSeq {
claim := s.builder.DefendClaim(s.lastClaim, true) claim := s.builder.DefendClaim(s.lastClaim, true)
claim.ContractIndex = len(s.game.Claims()) s.addClaimToGame(&claim)
s.builder.require.NoError(s.game.Put(claim))
return &GameBuilderSeq{ return &GameBuilderSeq{
gameBuilder: s.gameBuilder, gameBuilder: s.gameBuilder,
builder: s.builder, builder: s.builder,
game: s.game,
lastClaim: claim, lastClaim: claim,
} }
} }
func (s *GameBuilderSeq) Defend(value common.Hash) *GameBuilderSeq { func (s *GameBuilderSeq) Defend(value common.Hash) *GameBuilderSeq {
claim := s.builder.DefendClaimWithValue(s.lastClaim, value) claim := s.builder.DefendClaimWithValue(s.lastClaim, value)
claim.ContractIndex = len(s.game.Claims()) s.addClaimToGame(&claim)
s.builder.require.NoError(s.game.Put(claim))
return &GameBuilderSeq{ return &GameBuilderSeq{
gameBuilder: s.gameBuilder, gameBuilder: s.gameBuilder,
builder: s.builder, builder: s.builder,
game: s.game,
lastClaim: claim, lastClaim: claim,
} }
} }
......
...@@ -2,6 +2,10 @@ package types ...@@ -2,6 +2,10 @@ package types
import ( import (
"errors" "errors"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
) )
var ( var (
...@@ -14,12 +18,6 @@ var ( ...@@ -14,12 +18,6 @@ var (
// Game is an interface that represents the state of a dispute game. // Game is an interface that represents the state of a dispute game.
type Game interface { 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 returns all of the claims in the game.
Claims() []Claim Claims() []Claim
...@@ -36,45 +34,37 @@ type Game interface { ...@@ -36,45 +34,37 @@ type Game interface {
MaxDepth() uint64 MaxDepth() uint64
} }
type claimEntry struct { type claimID common.Hash
ClaimData
ParentContractIndex int
}
type extendedClaim struct { func computeClaimID(claim Claim) claimID {
self Claim return claimID(crypto.Keccak256Hash(
children []claimEntry new(big.Int).SetUint64(claim.Position.ToGIndex()).Bytes(),
claim.Value.Bytes(),
big.NewInt(int64(claim.ParentContractIndex)).Bytes(),
))
} }
// gameState is a struct that represents the state of a dispute game. // gameState is a struct that represents the state of a dispute game.
// The game state implements the [Game] interface. // The game state implements the [Game] interface.
type gameState struct { type gameState struct {
agreeWithProposedOutput bool agreeWithProposedOutput bool
root claimEntry // claims is the list of claims in the same order as the contract
// contractIndicies maps a contract index to it's extended claim. claims []Claim
// This is used to perform O(1) parent lookups. claimIDs map[claimID]bool
contractIndicies map[int]*extendedClaim depth uint64
// claims maps a claim entry to it's extended claim.
claims map[claimEntry]*extendedClaim
depth uint64
} }
// NewGameState returns a new game state. // NewGameState returns a new game state.
// The provided [Claim] is used as the root node. // The provided [Claim] is used as the root node.
func NewGameState(agreeWithProposedOutput bool, root Claim, depth uint64) *gameState { func NewGameState(agreeWithProposedOutput bool, claims []Claim, depth uint64) *gameState {
claims := make(map[claimEntry]*extendedClaim) claimIDs := make(map[claimID]bool)
parents := make(map[int]*extendedClaim) for _, claim := range claims {
rootClaimEntry := makeClaimEntry(root) claimIDs[computeClaimID(claim)] = true
claims[rootClaimEntry] = &extendedClaim{
self: root,
children: make([]claimEntry, 0),
} }
parents[root.ContractIndex] = claims[rootClaimEntry]
return &gameState{ return &gameState{
agreeWithProposedOutput: agreeWithProposedOutput, agreeWithProposedOutput: agreeWithProposedOutput,
root: rootClaimEntry,
claims: claims, claims: claims,
contractIndicies: parents, claimIDs: claimIDs,
depth: depth, depth: depth,
} }
} }
...@@ -91,83 +81,34 @@ func (g *gameState) AgreeWithClaimLevel(claim Claim) bool { ...@@ -91,83 +81,34 @@ func (g *gameState) AgreeWithClaimLevel(claim Claim) bool {
} }
} }
// 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 {
if claim.IsRoot() || g.IsDuplicate(claim) {
return ErrClaimExists
}
parent := g.getParent(claim)
if parent == nil {
return errors.New("no parent claim")
}
parent.children = append(parent.children, makeClaimEntry(claim))
claimWithExtension := &extendedClaim{
self: claim,
children: make([]claimEntry, 0),
}
g.claims[makeClaimEntry(claim)] = claimWithExtension
g.contractIndicies[claim.ContractIndex] = claimWithExtension
return nil
}
func (g *gameState) IsDuplicate(claim Claim) bool { func (g *gameState) IsDuplicate(claim Claim) bool {
_, ok := g.claims[makeClaimEntry(claim)] return g.claimIDs[computeClaimID(claim)]
return ok
} }
func (g *gameState) Claims() []Claim { func (g *gameState) Claims() []Claim {
queue := []claimEntry{g.root} // Defensively copy to avoid modifications to the underlying array.
var out []Claim return append([]Claim(nil), g.claims...)
for len(queue) > 0 {
item := queue[0]
queue = queue[1:]
queue = append(queue, g.getChildren(item)...)
out = append(out, g.claims[item].self)
}
return out
} }
func (g *gameState) MaxDepth() uint64 { func (g *gameState) MaxDepth() uint64 {
return g.depth return g.depth
} }
func (g *gameState) getChildren(c claimEntry) []claimEntry {
return g.claims[c].children
}
func (g *gameState) GetParent(claim Claim) (Claim, error) { func (g *gameState) GetParent(claim Claim) (Claim, error) {
parent := g.getParent(claim) parent := g.getParent(claim)
if parent == nil { if parent == nil {
return Claim{}, ErrClaimNotFound return Claim{}, ErrClaimNotFound
} }
return parent.self, nil return *parent, nil
} }
func (g *gameState) getParent(claim Claim) *extendedClaim { func (g *gameState) getParent(claim Claim) *Claim {
if claim.IsRoot() { if claim.IsRoot() {
return nil return nil
} }
if parent, ok := g.contractIndicies[claim.ParentContractIndex]; ok { if claim.ParentContractIndex >= len(g.claims) || claim.ParentContractIndex < 0 {
return parent return nil
}
return nil
}
func makeClaimEntry(claim Claim) claimEntry {
return claimEntry{
ClaimData: claim.ClaimData,
ParentContractIndex: claim.ParentContractIndex,
} }
parent := g.claims[claim.ParentContractIndex]
return &parent
} }
...@@ -54,8 +54,7 @@ func createTestClaims() (Claim, Claim, Claim, Claim) { ...@@ -54,8 +54,7 @@ func createTestClaims() (Claim, Claim, Claim, Claim) {
func TestIsDuplicate(t *testing.T) { func TestIsDuplicate(t *testing.T) {
// Setup the game state. // Setup the game state.
root, top, middle, bottom := createTestClaims() root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth) g := NewGameState(false, []Claim{root, top}, testMaxDepth)
require.NoError(t, g.Put(top))
// Root + Top should be duplicates // Root + Top should be duplicates
require.True(t, g.IsDuplicate(root)) require.True(t, g.IsDuplicate(root))
...@@ -66,137 +65,13 @@ func TestIsDuplicate(t *testing.T) { ...@@ -66,137 +65,13 @@ func TestIsDuplicate(t *testing.T) {
require.False(t, g.IsDuplicate(bottom)) require.False(t, g.IsDuplicate(bottom))
} }
// TestGame_Put_RootAlreadyExists tests the [Game.Put] method using a [gameState] func TestGame_Claims(t *testing.T) {
// instance errors when the root claim already exists in state.
func TestGame_Put_RootAlreadyExists(t *testing.T) {
// Setup the game state. // Setup the game state.
top, _, _, _ := createTestClaims()
g := NewGameState(false, top, testMaxDepth)
// Try to put the root claim into the game state again.
err := g.Put(top)
require.ErrorIs(t, err, ErrClaimExists)
}
// 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.
root, _, _, _ := createTestClaims()
g := NewGameState(false, root, testMaxDepth)
// Try to put the root claim into the game state again.
err := g.PutAll([]Claim{root})
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) {
root, top, middle, bottom := createTestClaims() root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth) expected := []Claim{root, top, middle, bottom}
g := NewGameState(false, expected, testMaxDepth)
err := g.PutAll([]Claim{top, middle})
require.NoError(t, err)
err = g.PutAll([]Claim{middle, bottom})
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.
root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth)
// We should not be able to get the parent of the root claim.
parent, err := g.GetParent(root)
require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{})
// Put the rest of the claims in the state.
err = g.PutAll([]Claim{top, middle, bottom})
require.NoError(t, err)
parent, err = g.GetParent(top)
require.NoError(t, err)
require.Equal(t, parent, root)
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_AlreadyExists tests the [Game.Put] method using a [gameState]
// instance errors when the given claim already exists in state.
func TestGame_Put_AlreadyExists(t *testing.T) {
// Setup the game state.
top, middle, _, _ := createTestClaims()
g := NewGameState(false, top, testMaxDepth)
// Put the next claim into state.
err := g.Put(middle)
require.NoError(t, err)
// Put the claim into the game state again.
err = g.Put(middle)
require.ErrorIs(t, err, ErrClaimExists)
}
// TestGame_Put_ParentsAndChildren tests the [Game.Put] method using a [gameState] instance.
func TestGame_Put_ParentsAndChildren(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth)
// We should not be able to get the parent of the root claim.
parent, err := g.GetParent(root)
require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{})
// Put + Check Top
err = g.Put(top)
require.NoError(t, err)
parent, err = g.GetParent(top)
require.NoError(t, err)
require.Equal(t, parent, root)
// Put + Check Top Middle
err = g.Put(middle)
require.NoError(t, err)
parent, err = g.GetParent(middle)
require.NoError(t, err)
require.Equal(t, parent, top)
// Put + Check Top Bottom
err = g.Put(bottom)
require.NoError(t, err)
parent, err = g.GetParent(bottom)
require.NoError(t, err)
require.Equal(t, parent, middle)
}
// TestGame_ClaimPairs tests the [Game.ClaimPairs] method using a [gameState] instance.
func TestGame_ClaimPairs(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth)
// Add top claim to the game state.
err := g.Put(top)
require.NoError(t, err)
// Add the middle claim to the game state.
err = g.Put(middle)
require.NoError(t, err)
// Add the bottom claim to the game state.
err = g.Put(bottom)
require.NoError(t, err)
// Validate claim pairs. // Validate claim pairs.
expected := []Claim{root, top, middle, bottom} actual := g.Claims()
claims := g.Claims() require.ElementsMatch(t, expected, actual)
require.ElementsMatch(t, expected, 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