Commit 00ed269e authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into clabby/ctb/fault-game-clock-tests

parents d7bdc74a 40af4dcf
---
'@eth-optimism/fault-detector': patch
---
Bump contracts-bedrock version
---
'@eth-optimism/sdk': minor
---
Add support for claiming multicall3 withdrawals
...@@ -51,7 +51,7 @@ jobs: ...@@ -51,7 +51,7 @@ jobs:
version: nightly version: nightly
- name: Build - name: Build
run: pnpm build run: npx nx run-many --target=build --skip-nx-cache
- name: Setup Canary Snapshot - name: Setup Canary Snapshot
run: pnpm changeset version --snapshot run: pnpm changeset version --snapshot
......
...@@ -17,127 +17,153 @@ type Game interface { ...@@ -17,127 +17,153 @@ type Game interface {
// Put adds a claim into the game state. // Put adds a claim into the game state.
Put(claim Claim) error 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
// IsDuplicate returns true if the provided [Claim] already exists in the game state.
IsDuplicate(claim Claim) bool IsDuplicate(claim Claim) bool
// PreStateClaim gets the claim which commits to the pre-state of this specific claim.
// This will return an error if it is called with a non-leaf claim.
PreStateClaim(claim Claim) (Claim, error)
// PostStateClaim gets the claim which commits to the post-state of this specific claim.
// This will return an error if it is called with a non-leaf claim.
PostStateClaim(claim Claim) (Claim, error)
} }
// Node is a node in the game state tree. type extendedClaim struct {
type Node struct { self Claim
self Claim contractIndex int
children []*Node children []ClaimData
} }
// 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 {
root Node root ClaimData
claims map[ClaimData]Claim claims map[ClaimData]*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(root Claim) *gameState { func NewGameState(root Claim, depth uint64) *gameState {
claims := make(map[ClaimData]Claim) claims := make(map[ClaimData]*extendedClaim)
claims[root.ClaimData] = root claims[root.ClaimData] = &extendedClaim{
self: root,
contractIndex: 0,
children: make([]ClaimData, 0),
}
return &gameState{ return &gameState{
root: Node{ root: root.ClaimData,
self: root,
children: make([]*Node, 0),
},
claims: claims, claims: claims,
depth: depth,
} }
} }
// getParent returns the parent of the provided [Claim]. // PutAll adds a list of claims into the [Game] state.
func (g *gameState) getParent(claim Claim) (Claim, error) { // If any of the claims already exist in the game state, an error is returned.
// If the claim is the root node, return an error. func (g *gameState) PutAll(claims []Claim) error {
if claim.IsRoot() { for _, claim := range claims {
return Claim{}, ErrClaimNotFound if err := g.Put(claim); err != nil {
return err
}
} }
return nil
}
// Walk down the tree from the root node to find the parent. // Put adds a claim into the game state.
found, err := g.recurseTree(&g.root, claim.Parent) func (g *gameState) Put(claim Claim) error {
if err != nil { if claim.IsRoot() || g.IsDuplicate(claim) {
return Claim{}, err return ErrClaimExists
} }
if parent, ok := g.claims[claim.Parent]; !ok {
// Return the parent of the found node. return errors.New("no parent claim")
return found.self, nil } else {
parent.children = append(parent.children, claim.ClaimData)
}
g.claims[claim.ClaimData] = &extendedClaim{
self: claim,
contractIndex: claim.ContractIndex,
children: make([]ClaimData, 0),
}
return nil
} }
// recurseTree recursively walks down the tree from the root node to find the func (g *gameState) IsDuplicate(claim Claim) bool {
// node with the provided [Claim]. _, ok := g.claims[claim.ClaimData]
func (g *gameState) recurseTree(treeNode *Node, claim ClaimData) (*Node, error) { return ok
// Check if the current node is the claim. }
if treeNode.self.ClaimData == claim {
return treeNode, nil
}
// Check all children of the current node. func (g *gameState) Claims() []Claim {
for _, child := range treeNode.children { queue := []ClaimData{g.root}
// Recurse and drop errors. var out []Claim
n, _ := g.recurseTree(child, claim) for len(queue) > 0 {
if n != nil { item := queue[0]
return n, nil queue = queue[1:]
} queue = append(queue, g.getChildren(item)...)
out = append(out, g.claims[item].self)
} }
return out
}
// If we reach this point, the claim was not found. func (g *gameState) getChildren(c ClaimData) []ClaimData {
return nil, ErrClaimNotFound return g.claims[c].children
} }
// Put adds a claim into the game state. func (g *gameState) getParent(claim Claim) (Claim, error) {
func (g *gameState) Put(claim Claim) error {
// The game is always initialized with a root claim. Cannot add a second.
if claim.IsRoot() { if claim.IsRoot() {
return ErrClaimExists return Claim{}, ErrClaimNotFound
} }
if parent, ok := g.claims[claim.Parent]; !ok {
// Grab the claim's parent. return Claim{}, ErrClaimNotFound
parent := claim.Parent } else {
return parent.self, nil
// Walk down the tree from the root node to find the parent.
found, err := g.recurseTree(&g.root, parent)
if err != nil {
return err
} }
}
// Check that the node is not already in the tree. func (g *gameState) PreStateClaim(claim Claim) (Claim, error) {
for _, child := range found.children { // Do checks in PreStateClaim because these do not hold while walking the tree
if child.self == claim { if claim.Depth() != int(g.depth) {
return ErrClaimExists return Claim{}, errors.New("Only leaf claims have pre or post state")
}
} }
// If the claim is the far left most claim, the pre-state is pulled from the contracts & we can supply at contract index.
// Create a new node. if claim.IndexAtDepth() == 0 {
node := Node{ return Claim{
self: claim, ContractIndex: -1,
children: make([]*Node, 0), }, nil
} }
return g.preStateClaim(claim)
// Add the node to the tree.
found.children = append(found.children, &node)
g.claims[claim.ClaimData] = claim
return nil
} }
func (g *gameState) IsDuplicate(claim Claim) bool { // preStateClaim is the internal tree walker which does not do error handling
_, ok := g.claims[claim.ClaimData] func (g *gameState) preStateClaim(claim Claim) (Claim, error) {
return ok parent, _ := g.getParent(claim)
if claim.DefendsParent() {
return parent, nil
} else {
return g.preStateClaim(parent)
}
} }
func (g *gameState) Claims() []Claim { func (g *gameState) PostStateClaim(claim Claim) (Claim, error) {
return g.root.claims() // Do checks in PostStateClaim because these do not hold while walking the tree
if claim.Depth() != int(g.depth) {
return Claim{}, errors.New("Only leaf claims have pre or post state")
}
return g.postStateClaim(claim)
} }
func (n *Node) claims() []Claim { // postStateClaim is the internal tree walker which does not do error handling
var out []Claim func (g *gameState) postStateClaim(claim Claim) (Claim, error) {
out = append(out, n.self) parent, _ := g.getParent(claim)
for _, c := range n.children { if claim.DefendsParent() {
out = append(out, c.claims()...) return g.postStateClaim(parent)
} else {
return parent, nil
} }
return out
} }
...@@ -7,19 +7,29 @@ import ( ...@@ -7,19 +7,29 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func createTestClaims() (Claim, Claim, Claim) { const testMaxDepth = 3
top := Claim{
func createTestClaims() (Claim, Claim, Claim, Claim) {
// root & middle are from the trace "abcdexyz"
// top & bottom are from the trace "abcdefgh"
root := Claim{
ClaimData: ClaimData{ ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000768"), Value: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000077a"),
Position: NewPosition(0, 0), Position: NewPosition(0, 0),
}, },
Parent: ClaimData{}, // Root claim has no parent
} }
top := Claim{
middle := Claim{
ClaimData: ClaimData{ ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"), Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000364"),
Position: NewPosition(1, 1), Position: NewPosition(1, 0),
},
Parent: root.ClaimData,
}
middle := Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000578"),
Position: NewPosition(2, 2),
}, },
Parent: top.ClaimData, Parent: top.ClaimData,
} }
...@@ -27,26 +37,26 @@ func createTestClaims() (Claim, Claim, Claim) { ...@@ -27,26 +37,26 @@ func createTestClaims() (Claim, Claim, Claim) {
bottom := Claim{ bottom := Claim{
ClaimData: ClaimData{ ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000465"), Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000465"),
Position: NewPosition(2, 2), Position: NewPosition(3, 4),
}, },
Parent: middle.ClaimData, Parent: middle.ClaimData,
} }
return top, middle, bottom return root, top, middle, bottom
} }
func TestIsDuplicate(t *testing.T) { func TestIsDuplicate(t *testing.T) {
// Setup the game state. // Setup the game state.
top, middle, bottom := createTestClaims() root, top, middle, bottom := createTestClaims()
g := NewGameState(top) g := NewGameState(root, testMaxDepth)
err := g.Put(middle) require.NoError(t, g.Put(top))
require.NoError(t, err)
// Top + Middle should be duplicates // Root + Top should be duplicates
require.True(t, g.IsDuplicate(root))
require.True(t, g.IsDuplicate(top)) require.True(t, g.IsDuplicate(top))
require.True(t, g.IsDuplicate(middle))
// Bottom should not be a duplicate // Middle + Bottom should not be a duplicate
require.False(t, g.IsDuplicate(middle))
require.False(t, g.IsDuplicate(bottom)) require.False(t, g.IsDuplicate(bottom))
} }
...@@ -54,20 +64,70 @@ func TestIsDuplicate(t *testing.T) { ...@@ -54,20 +64,70 @@ func TestIsDuplicate(t *testing.T) {
// instance errors when the root claim already exists in state. // instance errors when the root claim already exists in state.
func TestGame_Put_RootAlreadyExists(t *testing.T) { func TestGame_Put_RootAlreadyExists(t *testing.T) {
// Setup the game state. // Setup the game state.
top, _, _ := createTestClaims() top, _, _, _ := createTestClaims()
g := NewGameState(top) g := NewGameState(top, testMaxDepth)
// Try to put the root claim into the game state again. // Try to put the root claim into the game state again.
err := g.Put(top) err := g.Put(top)
require.ErrorIs(t, err, ErrClaimExists) 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(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(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(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] // TestGame_Put_AlreadyExists tests the [Game.Put] method using a [gameState]
// instance errors when the given claim already exists in state. // instance errors when the given claim already exists in state.
func TestGame_Put_AlreadyExists(t *testing.T) { func TestGame_Put_AlreadyExists(t *testing.T) {
// Setup the game state. // Setup the game state.
top, middle, _ := createTestClaims() top, middle, _, _ := createTestClaims()
g := NewGameState(top) g := NewGameState(top, testMaxDepth)
// Put the next claim into state. // Put the next claim into state.
err := g.Put(middle) err := g.Put(middle)
...@@ -81,24 +141,29 @@ func TestGame_Put_AlreadyExists(t *testing.T) { ...@@ -81,24 +141,29 @@ func TestGame_Put_AlreadyExists(t *testing.T) {
// TestGame_Put_ParentsAndChildren tests the [Game.Put] method using a [gameState] instance. // TestGame_Put_ParentsAndChildren tests the [Game.Put] method using a [gameState] instance.
func TestGame_Put_ParentsAndChildren(t *testing.T) { func TestGame_Put_ParentsAndChildren(t *testing.T) {
// Setup the game state. // Setup the game state.
top, middle, bottom := createTestClaims() root, top, middle, bottom := createTestClaims()
g := NewGameState(top) g := NewGameState(root, testMaxDepth)
// We should not be able to get the parent of the root claim. // We should not be able to get the parent of the root claim.
parent, err := g.getParent(top) parent, err := g.getParent(root)
require.ErrorIs(t, err, ErrClaimNotFound) require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{}) require.Equal(t, parent, Claim{})
// Put the middle claim into the game state. // Put + Check Top
// We should expect no parent to exist, yet. 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) err = g.Put(middle)
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(middle) parent, err = g.getParent(middle)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, top) require.Equal(t, parent, top)
// Put the bottom claim into the game state. // Put + Check Top Bottom
// We should expect the parent to be the claim we just added.
err = g.Put(bottom) err = g.Put(bottom)
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(bottom) parent, err = g.getParent(bottom)
...@@ -109,11 +174,15 @@ func TestGame_Put_ParentsAndChildren(t *testing.T) { ...@@ -109,11 +174,15 @@ func TestGame_Put_ParentsAndChildren(t *testing.T) {
// TestGame_ClaimPairs tests the [Game.ClaimPairs] method using a [gameState] instance. // TestGame_ClaimPairs tests the [Game.ClaimPairs] method using a [gameState] instance.
func TestGame_ClaimPairs(t *testing.T) { func TestGame_ClaimPairs(t *testing.T) {
// Setup the game state. // Setup the game state.
top, middle, bottom := createTestClaims() root, top, middle, bottom := createTestClaims()
g := NewGameState(top) g := NewGameState(root, testMaxDepth)
// Add middle claim to the game state. // Add top claim to the game state.
err := g.Put(middle) err := g.Put(top)
require.NoError(t, err)
// Add the middle claim to the game state.
err = g.Put(middle)
require.NoError(t, err) require.NoError(t, err)
// Add the bottom claim to the game state. // Add the bottom claim to the game state.
...@@ -121,7 +190,47 @@ func TestGame_ClaimPairs(t *testing.T) { ...@@ -121,7 +190,47 @@ func TestGame_ClaimPairs(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Validate claim pairs. // Validate claim pairs.
expected := []Claim{top, middle, bottom} expected := []Claim{root, top, middle, bottom}
claims := g.Claims() claims := g.Claims()
require.ElementsMatch(t, expected, claims) require.ElementsMatch(t, expected, claims)
} }
// TestPrePostStateOnlyOnLeafClaim tests that if PreStateClaim or PostStateClaim is called with an non-leaf claim
// those functions return an error.
func TestPrePostStateOnlyOnLeafClaim(t *testing.T) {
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
require.NoError(t, g.PutAll([]Claim{top, middle, bottom}))
_, err := g.PreStateClaim(middle)
require.Error(t, err)
_, err = g.PostStateClaim(middle)
require.Error(t, err)
}
func TestPreStateClaim(t *testing.T) {
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
require.NoError(t, g.Put(top))
require.NoError(t, g.Put(middle))
require.NoError(t, g.Put(bottom))
// Bottom trace index is 4. Pre trace index is then 3
pre, err := g.PreStateClaim(bottom)
require.NoError(t, err)
require.Equal(t, top, pre)
}
func TestPostStateClaim(t *testing.T) {
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
require.NoError(t, g.Put(top))
require.NoError(t, g.Put(middle))
require.NoError(t, g.Put(bottom))
// Bottom trace index is 4. Post trace index is then 5
post, err := g.PostStateClaim(bottom)
require.NoError(t, err)
require.Equal(t, middle, post)
}
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
}
func (m *mockGameState) PreStateClaim(claim Claim) (Claim, error) {
panic("unimplemented")
}
func (m *mockGameState) PostStateClaim(claim Claim) (Claim, error) {
panic("unimplemented")
}
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)
}
...@@ -22,7 +22,7 @@ func NewOrchestrator(maxDepth uint64, traces []TraceProvider, names []string, ro ...@@ -22,7 +22,7 @@ func NewOrchestrator(maxDepth uint64, traces []TraceProvider, names []string, ro
} }
log.Info("Starting game", "root_letter", string(root.Value[31:])) log.Info("Starting game", "root_letter", string(root.Value[31:]))
for i, trace := range traces { for i, trace := range traces {
game := NewGameState(root) game := NewGameState(root, maxDepth)
o.agents[i] = NewAgent(game, int(maxDepth), trace, &o, log.New("role", names[i])) o.agents[i] = NewAgent(game, int(maxDepth), trace, &o, log.New("role", names[i]))
o.outputChs[i] = make(chan Claim) o.outputChs[i] = make(chan Claim)
} }
......
package fault
import (
"context"
"math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
// faultResponder implements the [Responder] interface to send onchain transactions.
type faultResponder struct {
log log.Logger
txMgr txmgr.TxManager
fdgAddr common.Address
fdgAbi *abi.ABI
}
// NewFaultResponder returns a new [faultResponder].
func NewFaultResponder(logger log.Logger, txManagr txmgr.TxManager, fdgAddr common.Address) (*faultResponder, error) {
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
if err != nil {
return nil, err
}
return &faultResponder{
log: logger,
txMgr: txManagr,
fdgAddr: fdgAddr,
fdgAbi: fdgAbi,
}, nil
}
// buildFaultDefendData creates the transaction data for the Defend function.
func (r *faultResponder) buildFaultDefendData(parentContractIndex int, pivot [32]byte) ([]byte, error) {
return r.fdgAbi.Pack(
"defend",
big.NewInt(int64(parentContractIndex)),
pivot,
)
}
// buildFaultAttackData creates the transaction data for the Attack function.
func (r *faultResponder) buildFaultAttackData(parentContractIndex int, pivot [32]byte) ([]byte, error) {
return r.fdgAbi.Pack(
"attack",
big.NewInt(int64(parentContractIndex)),
pivot,
)
}
// BuildTx builds the transaction for the [faultResponder].
func (r *faultResponder) BuildTx(ctx context.Context, response Claim) ([]byte, error) {
if response.DefendsParent() {
txData, err := r.buildFaultDefendData(response.ParentContractIndex, response.ValueBytes())
if err != nil {
return nil, err
}
return txData, nil
} else {
txData, err := r.buildFaultAttackData(response.ParentContractIndex, response.ValueBytes())
if err != nil {
return nil, err
}
return txData, nil
}
}
// Respond takes a [Claim] and executes the response action.
func (r *faultResponder) Respond(ctx context.Context, response Claim) error {
// Build the transaction data.
txData, err := r.BuildTx(ctx, response)
if err != nil {
return err
}
// Send the transaction through the [txmgr].
receipt, err := r.txMgr.Send(ctx, txmgr.TxCandidate{
To: &r.fdgAddr,
TxData: txData,
// Setting GasLimit to 0 performs gas estimation online through the [txmgr].
GasLimit: 0,
})
if err != nil {
return err
}
if receipt.Status == types.ReceiptStatusFailed {
r.log.Error("responder tx successfully published but reverted", "tx_hash", receipt.TxHash)
} else {
r.log.Info("responder tx successfully published", "tx_hash", receipt.TxHash)
}
return nil
}
package fault
import (
"context"
"errors"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var (
mockFdgAddress = common.HexToAddress("0x1234")
mockSendError = errors.New("mock send error")
)
type mockTxManager struct {
from common.Address
sends int
sendFails bool
}
func (m *mockTxManager) Send(ctx context.Context, candidate txmgr.TxCandidate) (*types.Receipt, error) {
if m.sendFails {
return nil, mockSendError
}
m.sends++
return types.NewReceipt(
[]byte{},
false,
0,
), nil
}
func (m *mockTxManager) From() common.Address {
return m.from
}
func newTestFaultResponder(t *testing.T, sendFails bool) (*faultResponder, *mockTxManager) {
log := testlog.Logger(t, log.LvlError)
mockTxMgr := &mockTxManager{}
mockTxMgr.sendFails = sendFails
responder, err := NewFaultResponder(log, mockTxMgr, mockFdgAddress)
require.NoError(t, err)
return responder, mockTxMgr
}
// TestResponder_Respond_SendFails tests the [Responder.Respond] method
// bubbles up the error returned by the [txmgr.Send] method.
func TestResponder_Respond_SendFails(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, true)
err := responder.Respond(context.Background(), Claim{
ClaimData: ClaimData{
Value: common.Hash{0x01},
Position: NewPositionFromGIndex(2),
},
Parent: ClaimData{
Value: common.Hash{0x02},
Position: NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 0,
})
require.ErrorIs(t, err, mockSendError)
require.Equal(t, 0, mockTxMgr.sends)
}
// TestResponder_Respond_Success tests the [Responder.Respond] method
// succeeds when the tx candidate is successfully sent through the txmgr.
func TestResponder_Respond_Success(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t, false)
err := responder.Respond(context.Background(), Claim{
ClaimData: ClaimData{
Value: common.Hash{0x01},
Position: NewPositionFromGIndex(2),
},
Parent: ClaimData{
Value: common.Hash{0x02},
Position: NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 0,
})
require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends)
}
// TestResponder_BuildTx_Attack tests the [Responder.BuildTx] method
// returns a tx candidate with the correct data for an attack tx.
func TestResponder_BuildTx_Attack(t *testing.T) {
responder, _ := newTestFaultResponder(t, false)
responseClaim := Claim{
ClaimData: ClaimData{
Value: common.Hash{0x01},
Position: NewPositionFromGIndex(2),
},
Parent: ClaimData{
Value: common.Hash{0x02},
Position: NewPositionFromGIndex(1),
},
ContractIndex: 0,
ParentContractIndex: 7,
}
tx, err := responder.BuildTx(context.Background(), responseClaim)
require.NoError(t, err)
// Pack the tx data manually.
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err)
expected, err := fdgAbi.Pack(
"attack",
big.NewInt(int64(7)),
responseClaim.ValueBytes(),
)
require.NoError(t, err)
require.Equal(t, expected, tx)
}
// TestResponder_BuildTx_Defend tests the [Responder.BuildTx] method
// returns a tx candidate with the correct data for a defend tx.
func TestResponder_BuildTx_Defend(t *testing.T) {
responder, _ := newTestFaultResponder(t, false)
responseClaim := Claim{
ClaimData: ClaimData{
Value: common.Hash{0x01},
Position: NewPositionFromGIndex(3),
},
Parent: ClaimData{
Value: common.Hash{0x02},
Position: NewPositionFromGIndex(6),
},
ContractIndex: 0,
ParentContractIndex: 7,
}
tx, err := responder.BuildTx(context.Background(), responseClaim)
require.NoError(t, err)
// Pack the tx data manually.
fdgAbi, err := bindings.FaultDisputeGameMetaData.GetAbi()
require.NoError(t, err)
expected, err := fdgAbi.Pack(
"defend",
big.NewInt(int64(7)),
responseClaim.ValueBytes(),
)
require.NoError(t, err)
require.Equal(t, expected, tx)
}
...@@ -25,6 +25,13 @@ type ClaimData struct { ...@@ -25,6 +25,13 @@ type ClaimData struct {
Position Position
} }
func (c *ClaimData) ValueBytes() [32]byte {
responseBytes := c.Value.Bytes()
var responseArr [32]byte
copy(responseArr[:], responseBytes[:32])
return responseArr
}
// Claim extends ClaimData with information about the relationship between two claims. // Claim extends ClaimData with information about the relationship between two claims.
// It uses ClaimData to break cyclicity without using pointers. // It uses ClaimData to break cyclicity without using pointers.
// If the position of the game is Depth 0, IndexAtDepth 0 it is the root claim // If the position of the game is Depth 0, IndexAtDepth 0 it is the root claim
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
"postinstall": "patch-package && (test -d docs/op-stack && cd docs/op-stack && npx yarn@1 install && cd ../.. || exit 0)", "postinstall": "patch-package && (test -d docs/op-stack && cd docs/op-stack && npx yarn@1 install && cd ../.. || exit 0)",
"ready": "pnpm lint && pnpm test", "ready": "pnpm lint && pnpm test",
"prepare": "husky install", "prepare": "husky install",
"release": "pnpm build && pnpm changeset publish", "release": "npx nx run-many --target=build --skip-nx-cache && pnpm changeset publish",
"install:foundry": "curl -L https://foundry.paradigm.xyz | bash && pnpm update:foundry", "install:foundry": "curl -L https://foundry.paradigm.xyz | bash && pnpm update:foundry",
"update:foundry": "foundryup -C $(cat .foundryrc)" "update:foundry": "foundryup -C $(cat .foundryrc)"
}, },
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
}, },
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "^0.8.2", "@eth-optimism/common-ts": "^0.8.2",
"@eth-optimism/contracts-bedrock": "^0.14.0", "@eth-optimism/contracts-bedrock": "0.15.0",
"@eth-optimism/core-utils": "^0.12.1", "@eth-optimism/core-utils": "^0.12.1",
"@eth-optimism/sdk": "^3.0.0", "@eth-optimism/sdk": "^3.0.0",
"@ethersproject/abstract-provider": "^5.7.0" "@ethersproject/abstract-provider": "^5.7.0"
......
import hre from 'hardhat' import hre from 'hardhat'
import '@nomiclabs/hardhat-ethers'
import { Contract, utils } from 'ethers' import { Contract, utils } from 'ethers'
import { toRpcHexString } from '@eth-optimism/core-utils' import { toRpcHexString } from '@eth-optimism/core-utils'
import { getContractFactory } from '@eth-optimism/contracts-bedrock' import Artifact__L2OutputOracle from '@eth-optimism/contracts-bedrock/forge-artifacts/L2OutputOracle.sol/L2OutputOracle.json'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from './setup' import { expect } from './setup'
...@@ -26,7 +27,13 @@ describe('helpers', () => { ...@@ -26,7 +27,13 @@ describe('helpers', () => {
let L2OutputOracle: Contract let L2OutputOracle: Contract
beforeEach(async () => { beforeEach(async () => {
L2OutputOracle = await getContractFactory('L2OutputOracle', signer).deploy( const Factory__L2OutputOracle = new hre.ethers.ContractFactory(
Artifact__L2OutputOracle.abi,
Artifact__L2OutputOracle.bytecode.object,
signer
)
L2OutputOracle = await Factory__L2OutputOracle.deploy(
deployConfig.l2OutputOracleSubmissionInterval, deployConfig.l2OutputOracleSubmissionInterval,
deployConfig.l2BlockTime, deployConfig.l2BlockTime,
deployConfig.l2OutputOracleStartingBlockNumber, deployConfig.l2OutputOracleStartingBlockNumber,
......
This diff is collapsed.
...@@ -565,23 +565,25 @@ describe('CrossChainMessenger', () => { ...@@ -565,23 +565,25 @@ describe('CrossChainMessenger', () => {
}) })
describe('when the transaction sent more than one message', () => { describe('when the transaction sent more than one message', () => {
it('should throw an error', async () => { it('should be able to get second message by passing in an idex', async () => {
const messages = [...Array(2)].map(() => { const messages = [...Array(2)].map(() => {
return DUMMY_MESSAGE return DUMMY_MESSAGE
}) })
const tx = await l1Messenger.triggerSentMessageEvents(messages) const tx = await l1Messenger.triggerSentMessageEvents(messages)
await expect(messenger.toCrossChainMessage(tx)).to.be.rejectedWith( const foundCrossChainMessages =
'expected 1 message, got 2' await messenger.getMessagesByTransaction(tx)
expect(await messenger.toCrossChainMessage(tx, 1)).to.deep.eq(
foundCrossChainMessages[1]
) )
}) })
}) })
describe('when the transaction sent no messages', () => { describe('when the transaction sent no messages', () => {
it('should throw an error', async () => { it('should throw an out of bounds error', async () => {
const tx = await l1Messenger.triggerSentMessageEvents([]) const tx = await l1Messenger.triggerSentMessageEvents([])
await expect(messenger.toCrossChainMessage(tx)).to.be.rejectedWith( await expect(messenger.toCrossChainMessage(tx)).to.be.rejectedWith(
'expected 1 message, got 0' `withdrawal index 0 out of bounds. There are 0 withdrawals`
) )
}) })
}) })
......
...@@ -457,8 +457,8 @@ importers: ...@@ -457,8 +457,8 @@ importers:
specifier: ^0.8.2 specifier: ^0.8.2
version: link:../common-ts version: link:../common-ts
'@eth-optimism/contracts-bedrock': '@eth-optimism/contracts-bedrock':
specifier: ^0.14.0 specifier: 0.15.0
version: 0.14.0 version: link:../contracts-bedrock
'@eth-optimism/core-utils': '@eth-optimism/core-utils':
specifier: ^0.12.1 specifier: ^0.12.1
version: link:../core-utils version: link:../core-utils
...@@ -1482,18 +1482,6 @@ packages: ...@@ -1482,18 +1482,6 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
/@eth-optimism/contracts-bedrock@0.14.0:
resolution: {integrity: sha512-mvbSE2q2cyHUwg1jtHwR4JOQJcwdCVRAkmBdXCKUP0XsP48NT1J92bYileRdiUM5nLIESgNNmPA8L2J87mr62g==}
dependencies:
'@eth-optimism/core-utils': 0.12.1
'@openzeppelin/contracts': 4.7.3
'@openzeppelin/contracts-upgradeable': 4.7.3
ethers: 5.7.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
/@eth-optimism/contracts@0.6.0(ethers@5.7.1): /@eth-optimism/contracts@0.6.0(ethers@5.7.1):
resolution: {integrity: sha512-vQ04wfG9kMf1Fwy3FEMqH2QZbgS0gldKhcBeBUPfO8zu68L61VI97UDXmsMQXzTsEAxK8HnokW3/gosl4/NW3w==} resolution: {integrity: sha512-vQ04wfG9kMf1Fwy3FEMqH2QZbgS0gldKhcBeBUPfO8zu68L61VI97UDXmsMQXzTsEAxK8HnokW3/gosl4/NW3w==}
peerDependencies: peerDependencies:
...@@ -1532,23 +1520,6 @@ packages: ...@@ -1532,23 +1520,6 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@eth-optimism/core-utils@0.12.1:
resolution: {integrity: sha512-H2NnH9HTVDJmr9Yzb5R7GrAaYimcyIY4bF5Oud0xM1/DP4pSoNMtWm1kG3ZBpdqHFFYWH9GiSZZC5/cjFdKBEA==}
dependencies:
'@ethersproject/abi': 5.7.0
'@ethersproject/abstract-provider': 5.7.0
'@ethersproject/address': 5.7.0
'@ethersproject/bignumber': 5.7.0
'@ethersproject/bytes': 5.7.0
'@ethersproject/constants': 5.7.0
'@ethersproject/contracts': 5.7.0
'@ethersproject/keccak256': 5.7.0
'@ethersproject/properties': 5.7.0
'@ethersproject/rlp': 5.7.0
'@ethersproject/web': 5.7.1
chai: 4.3.7
dev: false
/@ethereum-waffle/chai@3.4.4: /@ethereum-waffle/chai@3.4.4:
resolution: {integrity: sha512-/K8czydBtXXkcM9X6q29EqEkc5dN3oYenyH2a9hF7rGAApAJUpH8QBtojxOY/xQ2up5W332jqgxwp0yPiYug1g==} resolution: {integrity: sha512-/K8czydBtXXkcM9X6q29EqEkc5dN3oYenyH2a9hF7rGAApAJUpH8QBtojxOY/xQ2up5W332jqgxwp0yPiYug1g==}
engines: {node: '>=10.0'} engines: {node: '>=10.0'}
......
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