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:
version: nightly
- name: Build
run: pnpm build
run: npx nx run-many --target=build --skip-nx-cache
- name: Setup Canary Snapshot
run: pnpm changeset version --snapshot
......
......@@ -17,127 +17,153 @@ 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
// 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 Node struct {
self Claim
children []*Node
type extendedClaim struct {
self Claim
contractIndex int
children []ClaimData
}
// gameState is a struct that represents the state of a dispute game.
// The game state implements the [Game] interface.
type gameState struct {
root Node
claims map[ClaimData]Claim
root ClaimData
claims map[ClaimData]*extendedClaim
depth uint64
}
// NewGameState returns a new game state.
// The provided [Claim] is used as the root node.
func NewGameState(root Claim) *gameState {
claims := make(map[ClaimData]Claim)
claims[root.ClaimData] = root
func NewGameState(root Claim, depth uint64) *gameState {
claims := make(map[ClaimData]*extendedClaim)
claims[root.ClaimData] = &extendedClaim{
self: root,
contractIndex: 0,
children: make([]ClaimData, 0),
}
return &gameState{
root: Node{
self: root,
children: make([]*Node, 0),
},
root: root.ClaimData,
claims: claims,
depth: depth,
}
}
// getParent returns the parent of the provided [Claim].
func (g *gameState) getParent(claim Claim) (Claim, error) {
// If the claim is the root node, return an error.
if claim.IsRoot() {
return Claim{}, 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
}
// Walk down the tree from the root node to find the parent.
found, err := g.recurseTree(&g.root, claim.Parent)
if err != nil {
return Claim{}, err
// Put adds a claim into the game state.
func (g *gameState) Put(claim Claim) error {
if claim.IsRoot() || g.IsDuplicate(claim) {
return ErrClaimExists
}
// Return the parent of the found node.
return found.self, nil
if parent, ok := g.claims[claim.Parent]; !ok {
return errors.New("no parent claim")
} 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
// node with the provided [Claim].
func (g *gameState) recurseTree(treeNode *Node, claim ClaimData) (*Node, error) {
// Check if the current node is the claim.
if treeNode.self.ClaimData == claim {
return treeNode, nil
}
func (g *gameState) IsDuplicate(claim Claim) bool {
_, ok := g.claims[claim.ClaimData]
return ok
}
// Check all children of the current node.
for _, child := range treeNode.children {
// Recurse and drop errors.
n, _ := g.recurseTree(child, claim)
if n != nil {
return n, nil
}
func (g *gameState) Claims() []Claim {
queue := []ClaimData{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
}
// If we reach this point, the claim was not found.
return nil, ErrClaimNotFound
func (g *gameState) getChildren(c ClaimData) []ClaimData {
return g.claims[c].children
}
// 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.
func (g *gameState) getParent(claim Claim) (Claim, error) {
if claim.IsRoot() {
return ErrClaimExists
return Claim{}, ErrClaimNotFound
}
// Grab the claim's parent.
parent := claim.Parent
// Walk down the tree from the root node to find the parent.
found, err := g.recurseTree(&g.root, parent)
if err != nil {
return err
if parent, ok := g.claims[claim.Parent]; !ok {
return Claim{}, ErrClaimNotFound
} else {
return parent.self, nil
}
}
// Check that the node is not already in the tree.
for _, child := range found.children {
if child.self == claim {
return ErrClaimExists
}
func (g *gameState) PreStateClaim(claim Claim) (Claim, error) {
// Do checks in PreStateClaim 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")
}
// Create a new node.
node := Node{
self: claim,
children: make([]*Node, 0),
// If the claim is the far left most claim, the pre-state is pulled from the contracts & we can supply at contract index.
if claim.IndexAtDepth() == 0 {
return Claim{
ContractIndex: -1,
}, nil
}
// Add the node to the tree.
found.children = append(found.children, &node)
g.claims[claim.ClaimData] = claim
return nil
return g.preStateClaim(claim)
}
func (g *gameState) IsDuplicate(claim Claim) bool {
_, ok := g.claims[claim.ClaimData]
return ok
// preStateClaim is the internal tree walker which does not do error handling
func (g *gameState) preStateClaim(claim Claim) (Claim, error) {
parent, _ := g.getParent(claim)
if claim.DefendsParent() {
return parent, nil
} else {
return g.preStateClaim(parent)
}
}
func (g *gameState) Claims() []Claim {
return g.root.claims()
func (g *gameState) PostStateClaim(claim Claim) (Claim, error) {
// 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 {
var out []Claim
out = append(out, n.self)
for _, c := range n.children {
out = append(out, c.claims()...)
// postStateClaim is the internal tree walker which does not do error handling
func (g *gameState) postStateClaim(claim Claim) (Claim, error) {
parent, _ := g.getParent(claim)
if claim.DefendsParent() {
return g.postStateClaim(parent)
} else {
return parent, nil
}
return out
}
......@@ -7,19 +7,29 @@ import (
"github.com/stretchr/testify/require"
)
func createTestClaims() (Claim, Claim, Claim) {
top := Claim{
const testMaxDepth = 3
func createTestClaims() (Claim, Claim, Claim, Claim) {
// root & middle are from the trace "abcdexyz"
// top & bottom are from the trace "abcdefgh"
root := Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000768"),
Value: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000077a"),
Position: NewPosition(0, 0),
},
Parent: ClaimData{},
// Root claim has no parent
}
middle := Claim{
top := Claim{
ClaimData: ClaimData{
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,
}
......@@ -27,26 +37,26 @@ func createTestClaims() (Claim, Claim, Claim) {
bottom := Claim{
ClaimData: ClaimData{
Value: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000465"),
Position: NewPosition(2, 2),
Position: NewPosition(3, 4),
},
Parent: middle.ClaimData,
}
return top, middle, bottom
return root, top, middle, bottom
}
func TestIsDuplicate(t *testing.T) {
// Setup the game state.
top, middle, bottom := createTestClaims()
g := NewGameState(top)
err := g.Put(middle)
require.NoError(t, err)
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
require.NoError(t, g.Put(top))
// 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(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))
}
......@@ -54,20 +64,70 @@ func TestIsDuplicate(t *testing.T) {
// instance errors when the root claim already exists in state.
func TestGame_Put_RootAlreadyExists(t *testing.T) {
// Setup the game state.
top, _, _ := createTestClaims()
g := NewGameState(top)
top, _, _, _ := createTestClaims()
g := NewGameState(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(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]
// 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(top)
top, middle, _, _ := createTestClaims()
g := NewGameState(top, testMaxDepth)
// Put the next claim into state.
err := g.Put(middle)
......@@ -81,24 +141,29 @@ func TestGame_Put_AlreadyExists(t *testing.T) {
// TestGame_Put_ParentsAndChildren tests the [Game.Put] method using a [gameState] instance.
func TestGame_Put_ParentsAndChildren(t *testing.T) {
// Setup the game state.
top, middle, bottom := createTestClaims()
g := NewGameState(top)
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(top)
parent, err := g.getParent(root)
require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{})
// Put the middle claim into the game state.
// We should expect no parent to exist, yet.
// 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 the bottom claim into the game state.
// We should expect the parent to be the claim we just added.
// Put + Check Top Bottom
err = g.Put(bottom)
require.NoError(t, err)
parent, err = g.getParent(bottom)
......@@ -109,11 +174,15 @@ func TestGame_Put_ParentsAndChildren(t *testing.T) {
// TestGame_ClaimPairs tests the [Game.ClaimPairs] method using a [gameState] instance.
func TestGame_ClaimPairs(t *testing.T) {
// Setup the game state.
top, middle, bottom := createTestClaims()
g := NewGameState(top)
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
// Add middle claim to the game state.
err := g.Put(middle)
// 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.
......@@ -121,7 +190,47 @@ func TestGame_ClaimPairs(t *testing.T) {
require.NoError(t, err)
// Validate claim pairs.
expected := []Claim{top, middle, bottom}
expected := []Claim{root, top, middle, bottom}
claims := g.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
}
log.Info("Starting game", "root_letter", string(root.Value[31:]))
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.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 {
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.
// 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
......
......@@ -24,7 +24,7 @@
"postinstall": "patch-package && (test -d docs/op-stack && cd docs/op-stack && npx yarn@1 install && cd ../.. || exit 0)",
"ready": "pnpm lint && pnpm test",
"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",
"update:foundry": "foundryup -C $(cat .foundryrc)"
},
......
......@@ -51,7 +51,7 @@
},
"dependencies": {
"@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/sdk": "^3.0.0",
"@ethersproject/abstract-provider": "^5.7.0"
......
import hre from 'hardhat'
import '@nomiclabs/hardhat-ethers'
import { Contract, utils } from 'ethers'
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 { expect } from './setup'
......@@ -26,7 +27,13 @@ describe('helpers', () => {
let L2OutputOracle: Contract
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.l2BlockTime,
deployConfig.l2OutputOracleStartingBlockNumber,
......
......@@ -329,9 +329,13 @@ export class CrossChainMessenger {
* @returns Bedrock representation of the message.
*/
public async toBedrockCrossChainMessage(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<CrossChainMessage> {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
// Bedrock messages are already in the correct format.
const { version } = decodeVersionedNonce(resolved.messageNonce)
......@@ -375,9 +379,13 @@ export class CrossChainMessenger {
* @return Transformed message.
*/
public async toLowLevelMessage(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<LowLevelMessage> {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only convert L2 to L1 messages to low level`)
}
......@@ -386,7 +394,7 @@ export class CrossChainMessenger {
const { version } = decodeVersionedNonce(resolved.messageNonce)
let updated: CrossChainMessage
if (version.eq(0)) {
updated = await this.toBedrockCrossChainMessage(resolved)
updated = await this.toBedrockCrossChainMessage(resolved, messageIndex)
} else {
updated = resolved
}
......@@ -401,6 +409,8 @@ export class CrossChainMessenger {
updated.message
)
// EVERYTHING following here is basically repeating the logic from getMessagesByTransaction
// consider cleaning this up
// We need to figure out the final withdrawal data that was used to compute the withdrawal hash
// inside the L2ToL1Message passer contract. Exact mechanism here depends on whether or not
// this is a legacy message or a new Bedrock message.
......@@ -412,10 +422,12 @@ export class CrossChainMessenger {
messageNonce = resolved.messageNonce
} else {
const receipt = await this.l2Provider.getTransactionReceipt(
resolved.transactionHash
(
await this.toCrossChainMessage(message)
).transactionHash
)
const withdrawals: any[] = []
const withdrawals: ethers.utils.Result[] = []
for (const log of receipt.logs) {
if (log.address === this.contracts.l2.BedrockMessagePasser.address) {
const decoded =
......@@ -431,12 +443,12 @@ export class CrossChainMessenger {
throw new Error(`no withdrawals found in receipt`)
}
// TODO: Add support for multiple withdrawals.
if (withdrawals.length > 1) {
throw new Error(`multiple withdrawals found in receipt`)
const withdrawal = withdrawals[messageIndex]
if (!withdrawal) {
throw new Error(
`withdrawal index ${messageIndex} out of bounds there are ${withdrawals.length} withdrawals`
)
}
const withdrawal = withdrawals[0]
messageNonce = withdrawal.nonce
gasLimit = withdrawal.gasLimit
}
......@@ -577,7 +589,11 @@ export class CrossChainMessenger {
* @returns Message coerced into a CrossChainMessage.
*/
public async toCrossChainMessage(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<CrossChainMessage> {
if (!message) {
throw new Error('message is undefined')
......@@ -621,14 +637,13 @@ export class CrossChainMessenger {
message as TransactionLike
)
// We only want to treat TransactionLike objects as MessageLike if they only emit a single
// message (very common). It's unintuitive to treat a TransactionLike as a MessageLike if
// they emit more than one message (which message do you pick?), so we throw an error.
if (messages.length !== 1) {
throw new Error(`expected 1 message, got ${messages.length}`)
const out = messages[messageIndex]
if (!out) {
throw new Error(
`withdrawal index ${messageIndex} out of bounds. There are ${messages.length} withdrawals`
)
}
return messages[0]
return out
}
}
......@@ -638,9 +653,16 @@ export class CrossChainMessenger {
* @param message Cross chain message to check the status of.
* @returns Status of the message.
*/
public async getMessageStatus(message: MessageLike): Promise<MessageStatus> {
const resolved = await this.toCrossChainMessage(message)
const receipt = await this.getMessageReceipt(resolved)
public async getMessageStatus(
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<MessageStatus> {
const resolved = await this.toCrossChainMessage(message, messageIndex)
const receipt = await this.getMessageReceipt(resolved, messageIndex)
if (resolved.direction === MessageDirection.L1_TO_L2) {
if (receipt === null) {
......@@ -656,13 +678,19 @@ export class CrossChainMessenger {
if (receipt === null) {
let timestamp: number
if (this.bedrock) {
const output = await this.getMessageBedrockOutput(resolved)
const output = await this.getMessageBedrockOutput(
resolved,
messageIndex
)
if (output === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
}
// Convert the message to the low level message that was proven.
const withdrawal = await this.toLowLevelMessage(resolved)
const withdrawal = await this.toLowLevelMessage(
resolved,
messageIndex
)
// Attempt to fetch the proven withdrawal.
const provenWithdrawal =
......@@ -679,7 +707,10 @@ export class CrossChainMessenger {
// Set the timestamp to the provenWithdrawal's timestamp
timestamp = provenWithdrawal.timestamp.toNumber()
} else {
const stateRoot = await this.getMessageStateRoot(resolved)
const stateRoot = await this.getMessageStateRoot(
resolved,
messageIndex
)
if (stateRoot === null) {
return MessageStatus.STATE_ROOT_NOT_PUBLISHED
}
......@@ -715,9 +746,14 @@ export class CrossChainMessenger {
* given message.
*/
public async getMessageReceipt(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<MessageReceipt> {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
// legacy withdrawals relayed prebedrock are v1
const messageHashV0 = hashCrossDomainMessagev0(
resolved.target,
......@@ -819,15 +855,20 @@ export class CrossChainMessenger {
confirmations?: number
pollIntervalMs?: number
timeoutMs?: number
} = {}
} = {},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<MessageReceipt> {
// Resolving once up-front is slightly more efficient.
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
let totalTimeMs = 0
while (totalTimeMs < (opts.timeoutMs || Infinity)) {
const tick = Date.now()
const receipt = await this.getMessageReceipt(resolved)
const receipt = await this.getMessageReceipt(resolved, messageIndex)
if (receipt !== null) {
return receipt
} else {
......@@ -857,15 +898,20 @@ export class CrossChainMessenger {
opts: {
pollIntervalMs?: number
timeoutMs?: number
} = {}
} = {},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<void> {
// Resolving once up-front is slightly more efficient.
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
let totalTimeMs = 0
while (totalTimeMs < (opts.timeoutMs || Infinity)) {
const tick = Date.now()
const currentStatus = await this.getMessageStatus(resolved)
const currentStatus = await this.getMessageStatus(resolved, messageIndex)
// Handle special cases for L1 to L2 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
......@@ -933,7 +979,8 @@ export class CrossChainMessenger {
opts?: {
bufferPercent?: number
from?: string
}
},
messageIndex = 0
): Promise<BigNumber> {
let resolved: CrossChainMessage | CrossChainMessageRequest
let from: string
......@@ -941,7 +988,10 @@ export class CrossChainMessenger {
resolved = message as CrossChainMessageRequest
from = opts?.from
} else {
resolved = await this.toCrossChainMessage(message as MessageLike)
resolved = await this.toCrossChainMessage(
message as MessageLike,
messageIndex
)
from = opts?.from || (resolved as CrossChainMessage).sender
}
......@@ -971,10 +1021,15 @@ export class CrossChainMessenger {
* @returns Estimated amount of time remaining (in seconds) before the message can be executed.
*/
public async estimateMessageWaitTimeSeconds(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<number> {
const resolved = await this.toCrossChainMessage(message)
const status = await this.getMessageStatus(resolved)
const resolved = await this.toCrossChainMessage(message, messageIndex)
const status = await this.getMessageStatus(resolved, messageIndex)
if (resolved.direction === MessageDirection.L1_TO_L2) {
if (
status === MessageStatus.RELAYED ||
......@@ -1012,7 +1067,7 @@ export class CrossChainMessenger {
// If the message is still within the challenge period, then we need to estimate exactly
// the amount of time left until the challenge period expires. The challenge period starts
// when the state root is published.
const stateRoot = await this.getMessageStateRoot(resolved)
const stateRoot = await this.getMessageStateRoot(resolved, messageIndex)
const challengePeriod = await this.getChallengePeriodSeconds()
const targetBlock = await this.l1Provider.getBlock(
stateRoot.batch.blockNumber
......@@ -1045,13 +1100,13 @@ export class CrossChainMessenger {
const challengePeriod =
oracleVersion === '1.0.0'
? // The ABI in the SDK does not contain FINALIZATION_PERIOD_SECONDS
// in OptimismPortal, so making an explicit call instead.
BigNumber.from(
await this.contracts.l1.OptimismPortal.provider.call({
to: this.contracts.l1.OptimismPortal.address,
data: '0xf4daa291', // FINALIZATION_PERIOD_SECONDS
})
)
// in OptimismPortal, so making an explicit call instead.
BigNumber.from(
await this.contracts.l1.OptimismPortal.provider.call({
to: this.contracts.l1.OptimismPortal.address,
data: '0xf4daa291', // FINALIZATION_PERIOD_SECONDS
})
)
: await this.contracts.l1.L2OutputOracle.FINALIZATION_PERIOD_SECONDS()
return challengePeriod.toNumber()
}
......@@ -1082,9 +1137,14 @@ export class CrossChainMessenger {
* @returns Bedrock output root.
*/
public async getMessageBedrockOutput(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<BedrockOutputData | null> {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
// Outputs are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
......@@ -1133,9 +1193,14 @@ export class CrossChainMessenger {
* @returns State root for the block in which the message was created.
*/
public async getMessageStateRoot(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<StateRoot | null> {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
// State roots are only a thing for L2 to L1 messages.
if (resolved.direction === MessageDirection.L1_TO_L2) {
......@@ -1318,14 +1383,19 @@ export class CrossChainMessenger {
* @returns Proof that can be used to finalize the message.
*/
public async getMessageProof(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<CrossChainMessageProof> {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`)
}
const stateRoot = await this.getMessageStateRoot(resolved)
const stateRoot = await this.getMessageStateRoot(resolved, messageIndex)
if (stateRoot === null) {
throw new Error(`state root for message not yet published`)
}
......@@ -1376,19 +1446,24 @@ export class CrossChainMessenger {
* @returns Proof that can be used to finalize the message.
*/
public async getBedrockMessageProof(
message: MessageLike
message: MessageLike,
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<BedrockCrossChainMessageProof> {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`can only generate proofs for L2 to L1 messages`)
}
const output = await this.getMessageBedrockOutput(resolved)
const output = await this.getMessageBedrockOutput(resolved, messageIndex)
if (output === null) {
throw new Error(`state root for message not yet published`)
}
const withdrawal = await this.toLowLevelMessage(resolved)
const withdrawal = await this.toLowLevelMessage(resolved, messageIndex)
const hash = hashLowLevelMessage(withdrawal)
const messageSlot = hashMessageHash(hash)
......@@ -1734,9 +1809,14 @@ export class CrossChainMessenger {
messageGasLimit: NumberLike,
opts?: {
overrides?: Overrides
}
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<TransactionRequest> => {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
if (resolved.direction === MessageDirection.L2_TO_L1) {
throw new Error(`cannot resend L2 to L1 message`)
}
......@@ -1780,9 +1860,14 @@ export class CrossChainMessenger {
message: MessageLike,
opts?: {
overrides?: PayableOverrides
}
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<TransactionRequest> => {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error('cannot finalize L1 to L2 message')
}
......@@ -1793,8 +1878,8 @@ export class CrossChainMessenger {
)
}
const withdrawal = await this.toLowLevelMessage(resolved)
const proof = await this.getBedrockMessageProof(resolved)
const withdrawal = await this.toLowLevelMessage(resolved, messageIndex)
const proof = await this.getBedrockMessageProof(resolved, messageIndex)
const args = [
[
......@@ -1835,15 +1920,20 @@ export class CrossChainMessenger {
message: MessageLike,
opts?: {
overrides?: PayableOverrides
}
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<TransactionRequest> => {
const resolved = await this.toCrossChainMessage(message)
const resolved = await this.toCrossChainMessage(message, messageIndex)
if (resolved.direction === MessageDirection.L1_TO_L2) {
throw new Error(`cannot finalize L1 to L2 message`)
}
if (this.bedrock) {
const withdrawal = await this.toLowLevelMessage(resolved)
const withdrawal = await this.toLowLevelMessage(resolved, messageIndex)
return this.contracts.l1.OptimismPortal.populateTransaction.finalizeWithdrawalTransaction(
[
withdrawal.messageNonce,
......@@ -1859,7 +1949,7 @@ export class CrossChainMessenger {
// L1CrossDomainMessenger relayMessage is the only method that isn't fully backwards
// compatible, so we need to use the legacy interface. When we fully upgrade to Bedrock we
// should be able to remove this code.
const proof = await this.getMessageProof(resolved)
const proof = await this.getMessageProof(resolved, messageIndex)
const legacyL1XDM = new ethers.Contract(
this.contracts.l1.L1CrossDomainMessenger.address,
getContractInterface('L1CrossDomainMessenger'),
......@@ -2116,10 +2206,14 @@ export class CrossChainMessenger {
message: MessageLike,
opts?: {
overrides?: CallOverrides
}
},
/**
* The index of the withdrawal if multiple are made with multicall
*/
messageIndex = 0
): Promise<BigNumber> => {
return this.l1Provider.estimateGas(
await this.populateTransaction.proveMessage(message, opts)
await this.populateTransaction.proveMessage(message, opts, messageIndex)
)
},
......
......@@ -565,23 +565,25 @@ describe('CrossChainMessenger', () => {
})
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(() => {
return DUMMY_MESSAGE
})
const tx = await l1Messenger.triggerSentMessageEvents(messages)
await expect(messenger.toCrossChainMessage(tx)).to.be.rejectedWith(
'expected 1 message, got 2'
const foundCrossChainMessages =
await messenger.getMessagesByTransaction(tx)
expect(await messenger.toCrossChainMessage(tx, 1)).to.deep.eq(
foundCrossChainMessages[1]
)
})
})
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([])
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:
specifier: ^0.8.2
version: link:../common-ts
'@eth-optimism/contracts-bedrock':
specifier: ^0.14.0
version: 0.14.0
specifier: 0.15.0
version: link:../contracts-bedrock
'@eth-optimism/core-utils':
specifier: ^0.12.1
version: link:../core-utils
......@@ -1482,18 +1482,6 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
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):
resolution: {integrity: sha512-vQ04wfG9kMf1Fwy3FEMqH2QZbgS0gldKhcBeBUPfO8zu68L61VI97UDXmsMQXzTsEAxK8HnokW3/gosl4/NW3w==}
peerDependencies:
......@@ -1532,23 +1520,6 @@ packages:
- utf-8-validate
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:
resolution: {integrity: sha512-/K8czydBtXXkcM9X6q29EqEkc5dN3oYenyH2a9hF7rGAApAJUpH8QBtojxOY/xQ2up5W332jqgxwp0yPiYug1g==}
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