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

Merge branch 'develop' into aj/e2e-withdrawal-timing

parents e85c4837 6c643be4
---
'@eth-optimism/sdk': minor
---
Add support for claiming multicall3 withdrawals
......@@ -685,6 +685,10 @@ jobs:
image: ubuntu-2204:2022.07.1
steps:
- checkout
- run:
name: Install pnpm package manager
command: |
npm i pnpm --global
- run:
name: Lint check
command: |
......
......@@ -10,6 +10,7 @@ import (
)
var remapTypeRe = regexp.MustCompile(`^(t_[\w_]+\([\w]+\))([\d]+)(_[\w]+)?$`)
var remapAstIdStorage = regexp.MustCompile(`(t_(struct|userDefinedValueType))\(([\w]+)\)([\d]+)_storage`)
// typeRemapping represents a mapping between an a type generated by solc
// and a canonicalized type. This is because solc inserts the ast id into
......@@ -63,8 +64,11 @@ func CanonicalizeASTIDs(in *solc.StorageLayout) *solc.StorageLayout {
// The storage types include the size when its a fixed size.
// This is subject to breaking in the future if a type with
// an ast id is added in a fixed storage type
if strings.Contains(oldType, "storage") {
// an ast id is added in a fixed storage type. We don't want
// to skip a type with `_storage` in it if it has a subtype
// with an ast id or it has an astid itself.
skip := len(remapAstIdStorage.FindAllStringSubmatch(oldType, -1)) == 0
if strings.Contains(oldType, "storage") && skip {
continue
}
......
This diff is collapsed.
......@@ -9,7 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/solc"
)
const L2OutputOracleStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"_initialized\",\"offset\":0,\"slot\":\"0\",\"type\":\"t_uint8\"},{\"astId\":1001,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"_initializing\",\"offset\":1,\"slot\":\"0\",\"type\":\"t_bool\"},{\"astId\":1002,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"startingBlockNumber\",\"offset\":0,\"slot\":\"1\",\"type\":\"t_uint256\"},{\"astId\":1003,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"startingTimestamp\",\"offset\":0,\"slot\":\"2\",\"type\":\"t_uint256\"},{\"astId\":1004,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"l2Outputs\",\"offset\":0,\"slot\":\"3\",\"type\":\"t_array(t_struct(OutputProposal)42701_storage)dyn_storage\"}],\"types\":{\"t_array(t_struct(OutputProposal)42701_storage)dyn_storage\":{\"encoding\":\"dynamic_array\",\"label\":\"struct Types.OutputProposal[]\",\"numberOfBytes\":\"32\",\"base\":\"t_struct(OutputProposal)42701_storage\"},\"t_bool\":{\"encoding\":\"inplace\",\"label\":\"bool\",\"numberOfBytes\":\"1\"},\"t_bytes32\":{\"encoding\":\"inplace\",\"label\":\"bytes32\",\"numberOfBytes\":\"32\"},\"t_struct(OutputProposal)42701_storage\":{\"encoding\":\"inplace\",\"label\":\"struct Types.OutputProposal\",\"numberOfBytes\":\"64\"},\"t_uint128\":{\"encoding\":\"inplace\",\"label\":\"uint128\",\"numberOfBytes\":\"16\"},\"t_uint256\":{\"encoding\":\"inplace\",\"label\":\"uint256\",\"numberOfBytes\":\"32\"},\"t_uint8\":{\"encoding\":\"inplace\",\"label\":\"uint8\",\"numberOfBytes\":\"1\"}}}"
const L2OutputOracleStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"_initialized\",\"offset\":0,\"slot\":\"0\",\"type\":\"t_uint8\"},{\"astId\":1001,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"_initializing\",\"offset\":1,\"slot\":\"0\",\"type\":\"t_bool\"},{\"astId\":1002,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"startingBlockNumber\",\"offset\":0,\"slot\":\"1\",\"type\":\"t_uint256\"},{\"astId\":1003,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"startingTimestamp\",\"offset\":0,\"slot\":\"2\",\"type\":\"t_uint256\"},{\"astId\":1004,\"contract\":\"contracts/L1/L2OutputOracle.sol:L2OutputOracle\",\"label\":\"l2Outputs\",\"offset\":0,\"slot\":\"3\",\"type\":\"t_array(t_struct(OutputProposal)1005_storage)dyn_storage\"}],\"types\":{\"t_array(t_struct(OutputProposal)1005_storage)dyn_storage\":{\"encoding\":\"dynamic_array\",\"label\":\"struct Types.OutputProposal[]\",\"numberOfBytes\":\"32\",\"base\":\"t_struct(OutputProposal)1005_storage\"},\"t_bool\":{\"encoding\":\"inplace\",\"label\":\"bool\",\"numberOfBytes\":\"1\"},\"t_bytes32\":{\"encoding\":\"inplace\",\"label\":\"bytes32\",\"numberOfBytes\":\"32\"},\"t_struct(OutputProposal)1005_storage\":{\"encoding\":\"inplace\",\"label\":\"struct Types.OutputProposal\",\"numberOfBytes\":\"64\"},\"t_uint128\":{\"encoding\":\"inplace\",\"label\":\"uint128\",\"numberOfBytes\":\"16\"},\"t_uint256\":{\"encoding\":\"inplace\",\"label\":\"uint256\",\"numberOfBytes\":\"32\"},\"t_uint8\":{\"encoding\":\"inplace\",\"label\":\"uint8\",\"numberOfBytes\":\"1\"}}}"
var L2OutputOracleStorageLayout = new(solc.StorageLayout)
......
......@@ -9,7 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/solc"
)
const OptimismPortalStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"_initialized\",\"offset\":0,\"slot\":\"0\",\"type\":\"t_uint8\"},{\"astId\":1001,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"_initializing\",\"offset\":1,\"slot\":\"0\",\"type\":\"t_bool\"},{\"astId\":1002,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"params\",\"offset\":0,\"slot\":\"1\",\"type\":\"t_struct(ResourceParams)34941_storage\"},{\"astId\":1003,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"__gap\",\"offset\":0,\"slot\":\"2\",\"type\":\"t_array(t_uint256)48_storage\"},{\"astId\":1004,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"l2Sender\",\"offset\":0,\"slot\":\"50\",\"type\":\"t_address\"},{\"astId\":1005,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"finalizedWithdrawals\",\"offset\":0,\"slot\":\"51\",\"type\":\"t_mapping(t_bytes32,t_bool)\"},{\"astId\":1006,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"provenWithdrawals\",\"offset\":0,\"slot\":\"52\",\"type\":\"t_mapping(t_bytes32,t_struct(ProvenWithdrawal)34280_storage)\"},{\"astId\":1007,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"paused\",\"offset\":0,\"slot\":\"53\",\"type\":\"t_bool\"}],\"types\":{\"t_address\":{\"encoding\":\"inplace\",\"label\":\"address\",\"numberOfBytes\":\"20\"},\"t_array(t_uint256)48_storage\":{\"encoding\":\"inplace\",\"label\":\"uint256[48]\",\"numberOfBytes\":\"1536\",\"base\":\"t_uint256\"},\"t_bool\":{\"encoding\":\"inplace\",\"label\":\"bool\",\"numberOfBytes\":\"1\"},\"t_bytes32\":{\"encoding\":\"inplace\",\"label\":\"bytes32\",\"numberOfBytes\":\"32\"},\"t_mapping(t_bytes32,t_bool)\":{\"encoding\":\"mapping\",\"label\":\"mapping(bytes32 =\u003e bool)\",\"numberOfBytes\":\"32\",\"key\":\"t_bytes32\",\"value\":\"t_bool\"},\"t_mapping(t_bytes32,t_struct(ProvenWithdrawal)34280_storage)\":{\"encoding\":\"mapping\",\"label\":\"mapping(bytes32 =\u003e struct OptimismPortal.ProvenWithdrawal)\",\"numberOfBytes\":\"32\",\"key\":\"t_bytes32\",\"value\":\"t_struct(ProvenWithdrawal)34280_storage\"},\"t_struct(ProvenWithdrawal)34280_storage\":{\"encoding\":\"inplace\",\"label\":\"struct OptimismPortal.ProvenWithdrawal\",\"numberOfBytes\":\"64\"},\"t_struct(ResourceParams)34941_storage\":{\"encoding\":\"inplace\",\"label\":\"struct ResourceMetering.ResourceParams\",\"numberOfBytes\":\"32\"},\"t_uint128\":{\"encoding\":\"inplace\",\"label\":\"uint128\",\"numberOfBytes\":\"16\"},\"t_uint256\":{\"encoding\":\"inplace\",\"label\":\"uint256\",\"numberOfBytes\":\"32\"},\"t_uint64\":{\"encoding\":\"inplace\",\"label\":\"uint64\",\"numberOfBytes\":\"8\"},\"t_uint8\":{\"encoding\":\"inplace\",\"label\":\"uint8\",\"numberOfBytes\":\"1\"}}}"
const OptimismPortalStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"_initialized\",\"offset\":0,\"slot\":\"0\",\"type\":\"t_uint8\"},{\"astId\":1001,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"_initializing\",\"offset\":1,\"slot\":\"0\",\"type\":\"t_bool\"},{\"astId\":1002,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"params\",\"offset\":0,\"slot\":\"1\",\"type\":\"t_struct(ResourceParams)1009_storage\"},{\"astId\":1003,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"__gap\",\"offset\":0,\"slot\":\"2\",\"type\":\"t_array(t_uint256)48_storage\"},{\"astId\":1004,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"l2Sender\",\"offset\":0,\"slot\":\"50\",\"type\":\"t_address\"},{\"astId\":1005,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"finalizedWithdrawals\",\"offset\":0,\"slot\":\"51\",\"type\":\"t_mapping(t_bytes32,t_bool)\"},{\"astId\":1006,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"provenWithdrawals\",\"offset\":0,\"slot\":\"52\",\"type\":\"t_mapping(t_bytes32,t_struct(ProvenWithdrawal)1008_storage)\"},{\"astId\":1007,\"contract\":\"contracts/L1/OptimismPortal.sol:OptimismPortal\",\"label\":\"paused\",\"offset\":0,\"slot\":\"53\",\"type\":\"t_bool\"}],\"types\":{\"t_address\":{\"encoding\":\"inplace\",\"label\":\"address\",\"numberOfBytes\":\"20\"},\"t_array(t_uint256)48_storage\":{\"encoding\":\"inplace\",\"label\":\"uint256[48]\",\"numberOfBytes\":\"1536\",\"base\":\"t_uint256\"},\"t_bool\":{\"encoding\":\"inplace\",\"label\":\"bool\",\"numberOfBytes\":\"1\"},\"t_bytes32\":{\"encoding\":\"inplace\",\"label\":\"bytes32\",\"numberOfBytes\":\"32\"},\"t_mapping(t_bytes32,t_bool)\":{\"encoding\":\"mapping\",\"label\":\"mapping(bytes32 =\u003e bool)\",\"numberOfBytes\":\"32\",\"key\":\"t_bytes32\",\"value\":\"t_bool\"},\"t_mapping(t_bytes32,t_struct(ProvenWithdrawal)1008_storage)\":{\"encoding\":\"mapping\",\"label\":\"mapping(bytes32 =\u003e struct OptimismPortal.ProvenWithdrawal)\",\"numberOfBytes\":\"32\",\"key\":\"t_bytes32\",\"value\":\"t_struct(ProvenWithdrawal)1008_storage\"},\"t_struct(ProvenWithdrawal)1008_storage\":{\"encoding\":\"inplace\",\"label\":\"struct OptimismPortal.ProvenWithdrawal\",\"numberOfBytes\":\"64\"},\"t_struct(ResourceParams)1009_storage\":{\"encoding\":\"inplace\",\"label\":\"struct ResourceMetering.ResourceParams\",\"numberOfBytes\":\"32\"},\"t_uint128\":{\"encoding\":\"inplace\",\"label\":\"uint128\",\"numberOfBytes\":\"16\"},\"t_uint256\":{\"encoding\":\"inplace\",\"label\":\"uint256\",\"numberOfBytes\":\"32\"},\"t_uint64\":{\"encoding\":\"inplace\",\"label\":\"uint64\",\"numberOfBytes\":\"8\"},\"t_uint8\":{\"encoding\":\"inplace\",\"label\":\"uint8\",\"numberOfBytes\":\"1\"}}}"
var OptimismPortalStorageLayout = new(solc.StorageLayout)
......
......@@ -9,7 +9,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/solc"
)
const SystemConfigStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"_initialized\",\"offset\":0,\"slot\":\"0\",\"type\":\"t_uint8\"},{\"astId\":1001,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"_initializing\",\"offset\":1,\"slot\":\"0\",\"type\":\"t_bool\"},{\"astId\":1002,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"__gap\",\"offset\":0,\"slot\":\"1\",\"type\":\"t_array(t_uint256)50_storage\"},{\"astId\":1003,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"_owner\",\"offset\":0,\"slot\":\"51\",\"type\":\"t_address\"},{\"astId\":1004,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"__gap\",\"offset\":0,\"slot\":\"52\",\"type\":\"t_array(t_uint256)49_storage\"},{\"astId\":1005,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"overhead\",\"offset\":0,\"slot\":\"101\",\"type\":\"t_uint256\"},{\"astId\":1006,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"scalar\",\"offset\":0,\"slot\":\"102\",\"type\":\"t_uint256\"},{\"astId\":1007,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"batcherHash\",\"offset\":0,\"slot\":\"103\",\"type\":\"t_bytes32\"},{\"astId\":1008,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"gasLimit\",\"offset\":0,\"slot\":\"104\",\"type\":\"t_uint64\"},{\"astId\":1009,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"_resourceConfig\",\"offset\":0,\"slot\":\"105\",\"type\":\"t_struct(ResourceConfig)34954_storage\"}],\"types\":{\"t_address\":{\"encoding\":\"inplace\",\"label\":\"address\",\"numberOfBytes\":\"20\"},\"t_array(t_uint256)49_storage\":{\"encoding\":\"inplace\",\"label\":\"uint256[49]\",\"numberOfBytes\":\"1568\",\"base\":\"t_uint256\"},\"t_array(t_uint256)50_storage\":{\"encoding\":\"inplace\",\"label\":\"uint256[50]\",\"numberOfBytes\":\"1600\",\"base\":\"t_uint256\"},\"t_bool\":{\"encoding\":\"inplace\",\"label\":\"bool\",\"numberOfBytes\":\"1\"},\"t_bytes32\":{\"encoding\":\"inplace\",\"label\":\"bytes32\",\"numberOfBytes\":\"32\"},\"t_struct(ResourceConfig)34954_storage\":{\"encoding\":\"inplace\",\"label\":\"struct ResourceMetering.ResourceConfig\",\"numberOfBytes\":\"32\"},\"t_uint128\":{\"encoding\":\"inplace\",\"label\":\"uint128\",\"numberOfBytes\":\"16\"},\"t_uint256\":{\"encoding\":\"inplace\",\"label\":\"uint256\",\"numberOfBytes\":\"32\"},\"t_uint32\":{\"encoding\":\"inplace\",\"label\":\"uint32\",\"numberOfBytes\":\"4\"},\"t_uint64\":{\"encoding\":\"inplace\",\"label\":\"uint64\",\"numberOfBytes\":\"8\"},\"t_uint8\":{\"encoding\":\"inplace\",\"label\":\"uint8\",\"numberOfBytes\":\"1\"}}}"
const SystemConfigStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"_initialized\",\"offset\":0,\"slot\":\"0\",\"type\":\"t_uint8\"},{\"astId\":1001,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"_initializing\",\"offset\":1,\"slot\":\"0\",\"type\":\"t_bool\"},{\"astId\":1002,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"__gap\",\"offset\":0,\"slot\":\"1\",\"type\":\"t_array(t_uint256)50_storage\"},{\"astId\":1003,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"_owner\",\"offset\":0,\"slot\":\"51\",\"type\":\"t_address\"},{\"astId\":1004,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"__gap\",\"offset\":0,\"slot\":\"52\",\"type\":\"t_array(t_uint256)49_storage\"},{\"astId\":1005,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"overhead\",\"offset\":0,\"slot\":\"101\",\"type\":\"t_uint256\"},{\"astId\":1006,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"scalar\",\"offset\":0,\"slot\":\"102\",\"type\":\"t_uint256\"},{\"astId\":1007,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"batcherHash\",\"offset\":0,\"slot\":\"103\",\"type\":\"t_bytes32\"},{\"astId\":1008,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"gasLimit\",\"offset\":0,\"slot\":\"104\",\"type\":\"t_uint64\"},{\"astId\":1009,\"contract\":\"contracts/L1/SystemConfig.sol:SystemConfig\",\"label\":\"_resourceConfig\",\"offset\":0,\"slot\":\"105\",\"type\":\"t_struct(ResourceConfig)1010_storage\"}],\"types\":{\"t_address\":{\"encoding\":\"inplace\",\"label\":\"address\",\"numberOfBytes\":\"20\"},\"t_array(t_uint256)49_storage\":{\"encoding\":\"inplace\",\"label\":\"uint256[49]\",\"numberOfBytes\":\"1568\",\"base\":\"t_uint256\"},\"t_array(t_uint256)50_storage\":{\"encoding\":\"inplace\",\"label\":\"uint256[50]\",\"numberOfBytes\":\"1600\",\"base\":\"t_uint256\"},\"t_bool\":{\"encoding\":\"inplace\",\"label\":\"bool\",\"numberOfBytes\":\"1\"},\"t_bytes32\":{\"encoding\":\"inplace\",\"label\":\"bytes32\",\"numberOfBytes\":\"32\"},\"t_struct(ResourceConfig)1010_storage\":{\"encoding\":\"inplace\",\"label\":\"struct ResourceMetering.ResourceConfig\",\"numberOfBytes\":\"32\"},\"t_uint128\":{\"encoding\":\"inplace\",\"label\":\"uint128\",\"numberOfBytes\":\"16\"},\"t_uint256\":{\"encoding\":\"inplace\",\"label\":\"uint256\",\"numberOfBytes\":\"32\"},\"t_uint32\":{\"encoding\":\"inplace\",\"label\":\"uint32\",\"numberOfBytes\":\"4\"},\"t_uint64\":{\"encoding\":\"inplace\",\"label\":\"uint64\",\"numberOfBytes\":\"8\"},\"t_uint8\":{\"encoding\":\"inplace\",\"label\":\"uint8\",\"numberOfBytes\":\"1\"}}}"
var SystemConfigStorageLayout = new(solc.StorageLayout)
......
......@@ -25,6 +25,14 @@ type Game interface {
// 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)
}
type extendedClaim struct {
......@@ -38,11 +46,12 @@ type extendedClaim struct {
type gameState struct {
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 {
func NewGameState(root Claim, depth uint64) *gameState {
claims := make(map[ClaimData]*extendedClaim)
claims[root.ClaimData] = &extendedClaim{
self: root,
......@@ -52,6 +61,7 @@ func NewGameState(root Claim) *gameState {
return &gameState{
root: root.ClaimData,
claims: claims,
depth: depth,
}
}
......@@ -115,3 +125,45 @@ func (g *gameState) getParent(claim Claim) (Claim, error) {
return parent.self, nil
}
}
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")
}
// 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
}
return g.preStateClaim(claim)
}
// 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) 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)
}
// 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
}
}
......@@ -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,71 +37,83 @@ 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))
}
// 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) {
// Setup the game state.
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.
top, _, _ := createTestClaims()
g := NewGameState(top)
root, _, _, _ := createTestClaims()
g := NewGameState(root, testMaxDepth)
// Try to put the root claim into the game state again.
err := g.PutAll([]Claim{top})
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) {
// Setup the game state.
top, middle, _ := createTestClaims()
g := NewGameState(top)
root, top, middle, bottom := createTestClaims()
g := NewGameState(root, testMaxDepth)
// Put the next claim into state.
err := g.PutAll([]Claim{middle})
err := g.PutAll([]Claim{top, middle})
require.NoError(t, err)
// Try to put the root claim into the game state again.
err = g.PutAll([]Claim{middle})
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.
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 rest of the claims in the state.
err = g.PutAll([]Claim{middle, bottom})
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)
......@@ -100,24 +122,12 @@ func TestGame_PutAll_ParentsAndChildren(t *testing.T) {
require.Equal(t, parent, middle)
}
// TestGame_Put_RootAlreadyExists tests the [Game.Put] method using a [gameState]
// instance errors when the root claim already exists in state.
func TestGame_Put_RootAlreadyExists(t *testing.T) {
// Setup the game state.
top, _, _ := createTestClaims()
g := NewGameState(top)
// Try to put the root claim into the game state again.
err := g.Put(top)
require.ErrorIs(t, err, ErrClaimExists)
}
// 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)
......@@ -131,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)
......@@ -159,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.
......@@ -171,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)
}
......@@ -47,6 +47,14 @@ 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
......
......@@ -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
......
......@@ -32,30 +32,32 @@ DisputeGameFactory_SetImplementation_Test:test_setImplementation_notOwner_revert
DisputeGameFactory_SetImplementation_Test:test_setImplementation_succeeds() (gas: 44243)
DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_notOwner_reverts() (gas: 15950)
DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_succeeds() (gas: 18642)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 498826)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 500705)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot:test_resolvesCorrectly_succeeds() (gas: 488169)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 497701)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 499580)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot:test_resolvesCorrectly_succeeds() (gas: 487044)
FaultDisputeGame_Test:test_defendRoot_invalidMove_reverts() (gas: 13294)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 502174)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 504053)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot:test_resolvesCorrectly_succeeds() (gas: 491517)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 500937)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 502816)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot:test_resolvesCorrectly_succeeds() (gas: 490280)
FaultDisputeGame_Test:test_extraData_succeeds() (gas: 17426)
FaultDisputeGame_Test:test_gameData_succeeds() (gas: 17917)
FaultDisputeGame_Test:test_gameStart_succeeds() (gas: 10315)
FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8282)
FaultDisputeGame_Test:test_initialRootClaimData_succeeds() (gas: 17691)
FaultDisputeGame_Test:test_initialRootClaimData_succeeds() (gas: 17669)
FaultDisputeGame_Test:test_move_clockCorrectness_succeeds() (gas: 415981)
FaultDisputeGame_Test:test_move_clockTimeExceeded_reverts() (gas: 26387)
FaultDisputeGame_Test:test_move_defendRoot_reverts() (gas: 13360)
FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 103230)
FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 408100)
FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 10923)
FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 10968)
FaultDisputeGame_Test:test_move_nonExistentParent_reverts() (gas: 24655)
FaultDisputeGame_Test:test_resolve_challengeContested_succeeds() (gas: 221201)
FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9679)
FaultDisputeGame_Test:test_resolve_rootContested_succeeds() (gas: 106068)
FaultDisputeGame_Test:test_resolve_rootUncontested_succeeds() (gas: 23692)
FaultDisputeGame_Test:test_resolve_teamDeathmatch_succeeds() (gas: 391875)
FaultDisputeGame_Test:test_move_simpleAttack_succeeds() (gas: 107344)
FaultDisputeGame_Test:test_resolve_challengeContested_succeeds() (gas: 224789)
FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9657)
FaultDisputeGame_Test:test_resolve_rootContested_succeeds() (gas: 109754)
FaultDisputeGame_Test:test_resolve_rootUncontestedClockNotExpired_succeeds() (gas: 21422)
FaultDisputeGame_Test:test_resolve_rootUncontested_succeeds() (gas: 27256)
FaultDisputeGame_Test:test_resolve_teamDeathmatch_succeeds() (gas: 395447)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8181)
FaultDisputeGame_Test:test_simpleAttack_succeeds() (gas: 107388)
FeeVault_Test:test_constructor_succeeds() (gas: 18185)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 352113)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2950320)
......
......@@ -34,7 +34,6 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
IBigStepper public immutable VM;
/// @notice The duration of the game.
/// @dev TODO: Account for resolution buffer. (?)
Duration internal constant GAME_DURATION = Duration.wrap(7 days);
/// @notice The root claim's position is always at gindex 1.
......@@ -275,10 +274,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @inheritdoc IDisputeGame
function resolve() external returns (GameStatus status_) {
// TODO: Do not allow resolution before clocks run out.
// If the game is not in progress, it cannot be resolved.
if (status != GameStatus.IN_PROGRESS) {
// If the game is not in progress, it cannot be resolved.
revert GameNotInProgress();
}
......@@ -298,7 +295,6 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// If the claim is not a dangling node above the bottom of the tree,
// we can skip over it. These nodes are not relevant to the game resolution.
Position claimPos = claim.position;
if (claim.countered) {
continue;
}
......@@ -306,7 +302,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// If the claim is a dangling node, we can check if it is the left-most
// dangling node we've come across so far. If it is, we can update the
// left-most trace index.
uint256 traceIndex = claimPos.traceIndex(MAX_GAME_DEPTH);
uint256 traceIndex = claim.position.traceIndex(MAX_GAME_DEPTH);
if (traceIndex < leftMostTraceIndex) {
leftMostTraceIndex = traceIndex;
unchecked {
......@@ -315,12 +311,29 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
}
}
// Create a reference to the left most uncontested claim and its parent.
ClaimData storage leftMostUncontested = claimData[leftMostIndex];
// If the left most uncontested claim's parent has not expired their clock, the game
// cannot be resolved. If the left most uncontested claim is the root, no nodes qualified,
// and we check if 3.5 days has passed since the root claim's creation.
uint256 parentIndex = leftMostUncontested.parentIndex;
Clock opposingClock = parentIndex == type(uint32).max
? leftMostUncontested.clock
: claimData[parentIndex].clock;
if (
Duration.unwrap(opposingClock.duration()) +
(block.timestamp - Timestamp.unwrap(opposingClock.timestamp())) <=
Duration.unwrap(GAME_DURATION) >> 1
) {
revert ClockNotExpired();
}
// If the left-most dangling node is at an even depth, the defender wins.
// Otherwise, the challenger wins and the root claim is deemed invalid.
if (
// slither-disable-next-line weak-prng
claimData[leftMostIndex].position.depth() % 2 == 0 &&
leftMostTraceIndex != type(uint128).max
leftMostUncontested.position.depth() % 2 == 0 && leftMostTraceIndex != type(uint128).max
) {
status_ = GameStatus.DEFENDER_WINS;
} else {
......
......@@ -39,6 +39,9 @@ error GameNotInProgress();
/// @notice Thrown when a move is attempted to be made after the clock has timed out.
error ClockTimeExceeded();
/// @notice Thrown when the game is attempted to be resolved too early.
error ClockNotExpired();
/// @notice Thrown when a move is attempted to be made at or greater than the max depth of the game.
error GameDepthExceeded();
......
......@@ -130,7 +130,7 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
}
/// @dev Tests that an attempt to defend the root claim reverts with the `CannotDefendRootClaim` error.
function test_defendRoot_invalidMove_reverts() public {
function test_move_defendRoot_reverts() public {
vm.expectRevert(CannotDefendRootClaim.selector);
gameProxy.defend(0, Claim.wrap(bytes32(uint256(5))));
}
......@@ -175,6 +175,49 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
}
/// @notice Static unit test for the correctness of the chess clock incrementation.
function test_move_clockCorrectness_succeeds() public {
(, , , , Clock clock) = gameProxy.claimData(0);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))))
);
Claim claim = Claim.wrap(bytes32(uint256(5)));
vm.warp(block.timestamp + 15);
gameProxy.attack(0, claim);
(, , , , clock) = gameProxy.claimData(1);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(15), Timestamp.wrap(uint64(block.timestamp))))
);
vm.warp(block.timestamp + 10);
gameProxy.attack(1, claim);
(, , , , clock) = gameProxy.claimData(2);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(10), Timestamp.wrap(uint64(block.timestamp))))
);
vm.warp(block.timestamp + 10);
gameProxy.attack(2, claim);
(, , , , clock) = gameProxy.claimData(3);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(25), Timestamp.wrap(uint64(block.timestamp))))
);
vm.warp(block.timestamp + 10);
gameProxy.attack(3, claim);
(, , , , clock) = gameProxy.claimData(4);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(20), Timestamp.wrap(uint64(block.timestamp))))
);
}
/// @dev Tests that an identical claim cannot be made twice. The duplicate claim attempt should
/// revert with the `ClaimAlreadyExists` error.
function test_move_duplicateClaim_reverts() public {
......@@ -189,7 +232,7 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
}
/// @dev Static unit test for the correctness of an opening attack.
function test_simpleAttack_succeeds() public {
function test_move_simpleAttack_succeeds() public {
// Warp ahead 5 seconds.
vm.warp(block.timestamp + 5);
......@@ -237,11 +280,19 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
/// @dev Static unit test for the correctness an uncontested root resolution.
function test_resolve_rootUncontested_succeeds() public {
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
GameStatus status = gameProxy.resolve();
assertEq(uint8(status), uint8(GameStatus.DEFENDER_WINS));
assertEq(uint8(gameProxy.status()), uint8(GameStatus.DEFENDER_WINS));
}
/// @dev Static unit test for the correctness an uncontested root resolution.
function test_resolve_rootUncontestedClockNotExpired_succeeds() public {
vm.warp(block.timestamp + 3 days + 12 hours);
vm.expectRevert(ClockNotExpired.selector);
gameProxy.resolve();
}
/// @dev Static unit test asserting that resolve reverts when the game state is
/// not in progress.
function test_resolve_notInProgress_reverts() public {
......@@ -263,6 +314,8 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
function test_resolve_rootContested_succeeds() public {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
GameStatus status = gameProxy.resolve();
assertEq(uint8(status), uint8(GameStatus.CHALLENGER_WINS));
assertEq(uint8(gameProxy.status()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -273,6 +326,8 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
gameProxy.defend(1, Claim.wrap(bytes32(uint256(6))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
GameStatus status = gameProxy.resolve();
assertEq(uint8(status), uint8(GameStatus.DEFENDER_WINS));
assertEq(uint8(gameProxy.status()), uint8(GameStatus.DEFENDER_WINS));
......@@ -285,6 +340,8 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.defend(1, Claim.wrap(bytes32(uint256(6))));
gameProxy.defend(1, Claim.wrap(bytes32(uint256(7))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
GameStatus status = gameProxy.resolve();
assertEq(uint8(status), uint8(GameStatus.CHALLENGER_WINS));
assertEq(uint8(gameProxy.status()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -499,6 +556,9 @@ contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -517,6 +577,9 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
......@@ -535,6 +598,9 @@ contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2 is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -553,6 +619,9 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot2 is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
......@@ -571,6 +640,9 @@ contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3 is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
......@@ -589,6 +661,9 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot3 is OneVsOne_Arena {
// Play the game until a step is forced.
challenger.play(0);
// Warp ahead to expire the other player's clock.
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
......
This diff is collapsed.
......@@ -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`
)
})
})
......
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