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) {
if len(claims) == 0 {
return nil, errors.New("no claims")
}
game := types.NewGameState(a.agreeWithProposedOutput, claims[0], 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)
}
game := types.NewGameState(a.agreeWithProposedOutput, claims, uint64(a.maxDepth))
return game, nil
}
......@@ -6,15 +6,17 @@ import (
)
type GameBuilder struct {
builder *ClaimBuilder
Game types.Game
ExpectedActions []types.Action
builder *ClaimBuilder
Game types.Game
ExpectedActions []types.Action
agreeWithOutputRoot bool
}
func (c *ClaimBuilder) GameBuilder(agreeWithOutputRoot bool, rootCorrect bool) *GameBuilder {
return &GameBuilder{
builder: c,
Game: types.NewGameState(agreeWithOutputRoot, c.CreateRootClaim(rootCorrect), uint64(c.maxDepth)),
builder: c,
agreeWithOutputRoot: agreeWithOutputRoot,
Game: types.NewGameState(agreeWithOutputRoot, []types.Claim{c.CreateRootClaim(rootCorrect)}, uint64(c.maxDepth)),
}
}
......@@ -22,62 +24,60 @@ type GameBuilderSeq struct {
gameBuilder *GameBuilder
builder *ClaimBuilder
lastClaim types.Claim
game types.Game
}
func (g *GameBuilder) Seq() *GameBuilderSeq {
return &GameBuilderSeq{
gameBuilder: g,
builder: g.builder,
game: g.Game,
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 {
claim := s.builder.AttackClaim(s.lastClaim, true)
claim.ContractIndex = len(s.game.Claims())
s.builder.require.NoError(s.game.Put(claim))
s.addClaimToGame(&claim)
return &GameBuilderSeq{
gameBuilder: s.gameBuilder,
builder: s.builder,
game: s.game,
lastClaim: claim,
}
}
func (s *GameBuilderSeq) Attack(value common.Hash) *GameBuilderSeq {
claim := s.builder.AttackClaimWithValue(s.lastClaim, value)
claim.ContractIndex = len(s.game.Claims())
s.builder.require.NoError(s.game.Put(claim))
s.addClaimToGame(&claim)
return &GameBuilderSeq{
gameBuilder: s.gameBuilder,
builder: s.builder,
game: s.game,
lastClaim: claim,
}
}
func (s *GameBuilderSeq) DefendCorrect() *GameBuilderSeq {
claim := s.builder.DefendClaim(s.lastClaim, true)
claim.ContractIndex = len(s.game.Claims())
s.builder.require.NoError(s.game.Put(claim))
s.addClaimToGame(&claim)
return &GameBuilderSeq{
gameBuilder: s.gameBuilder,
builder: s.builder,
game: s.game,
lastClaim: claim,
}
}
func (s *GameBuilderSeq) Defend(value common.Hash) *GameBuilderSeq {
claim := s.builder.DefendClaimWithValue(s.lastClaim, value)
claim.ContractIndex = len(s.game.Claims())
s.builder.require.NoError(s.game.Put(claim))
s.addClaimToGame(&claim)
return &GameBuilderSeq{
gameBuilder: s.gameBuilder,
builder: s.builder,
game: s.game,
lastClaim: claim,
}
}
......
......@@ -2,6 +2,10 @@ package types
import (
"errors"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
var (
......@@ -14,12 +18,6 @@ var (
// Game is an interface that represents the state of a dispute game.
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
......@@ -36,45 +34,37 @@ type Game interface {
MaxDepth() uint64
}
type claimEntry struct {
ClaimData
ParentContractIndex int
}
type claimID common.Hash
type extendedClaim struct {
self Claim
children []claimEntry
func computeClaimID(claim Claim) claimID {
return claimID(crypto.Keccak256Hash(
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.
// The game state implements the [Game] interface.
type gameState struct {
agreeWithProposedOutput bool
root claimEntry
// contractIndicies maps a contract index to it's extended claim.
// This is used to perform O(1) parent lookups.
contractIndicies map[int]*extendedClaim
// claims maps a claim entry to it's extended claim.
claims map[claimEntry]*extendedClaim
depth uint64
// claims is the list of claims in the same order as the contract
claims []Claim
claimIDs map[claimID]bool
depth uint64
}
// NewGameState returns a new game state.
// The provided [Claim] is used as the root node.
func NewGameState(agreeWithProposedOutput bool, root Claim, depth uint64) *gameState {
claims := make(map[claimEntry]*extendedClaim)
parents := make(map[int]*extendedClaim)
rootClaimEntry := makeClaimEntry(root)
claims[rootClaimEntry] = &extendedClaim{
self: root,
children: make([]claimEntry, 0),
func NewGameState(agreeWithProposedOutput bool, claims []Claim, depth uint64) *gameState {
claimIDs := make(map[claimID]bool)
for _, claim := range claims {
claimIDs[computeClaimID(claim)] = true
}
parents[root.ContractIndex] = claims[rootClaimEntry]
return &gameState{
agreeWithProposedOutput: agreeWithProposedOutput,
root: rootClaimEntry,
claims: claims,
contractIndicies: parents,
claimIDs: claimIDs,
depth: depth,
}
}
......@@ -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 {
_, ok := g.claims[makeClaimEntry(claim)]
return ok
return g.claimIDs[computeClaimID(claim)]
}
func (g *gameState) Claims() []Claim {
queue := []claimEntry{g.root}
var out []Claim
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
// Defensively copy to avoid modifications to the underlying array.
return append([]Claim(nil), g.claims...)
}
func (g *gameState) MaxDepth() uint64 {
return g.depth
}
func (g *gameState) getChildren(c claimEntry) []claimEntry {
return g.claims[c].children
}
func (g *gameState) GetParent(claim Claim) (Claim, error) {
parent := g.getParent(claim)
if parent == nil {
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() {
return nil
}
if parent, ok := g.contractIndicies[claim.ParentContractIndex]; ok {
return parent
}
return nil
}
func makeClaimEntry(claim Claim) claimEntry {
return claimEntry{
ClaimData: claim.ClaimData,
ParentContractIndex: claim.ParentContractIndex,
if claim.ParentContractIndex >= len(g.claims) || claim.ParentContractIndex < 0 {
return nil
}
parent := g.claims[claim.ParentContractIndex]
return &parent
}
......@@ -54,8 +54,7 @@ func createTestClaims() (Claim, Claim, Claim, Claim) {
func TestIsDuplicate(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.Put(top))
g := NewGameState(false, []Claim{root, top}, testMaxDepth)
// Root + Top should be duplicates
require.True(t, g.IsDuplicate(root))
......@@ -66,137 +65,13 @@ func TestIsDuplicate(t *testing.T) {
require.False(t, g.IsDuplicate(bottom))
}
// 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) {
func TestGame_Claims(t *testing.T) {
// 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()
g := NewGameState(false, root, 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)
expected := []Claim{root, top, middle, bottom}
g := NewGameState(false, expected, testMaxDepth)
// Validate claim pairs.
expected := []Claim{root, top, middle, bottom}
claims := g.Claims()
require.ElementsMatch(t, expected, claims)
actual := g.Claims()
require.ElementsMatch(t, expected, actual)
}
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