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

Merge branch 'develop' into revert-indexer-disable

parents 122a015b 61f9ea24
...@@ -89,10 +89,8 @@ devnet-up: ...@@ -89,10 +89,8 @@ devnet-up:
@if [ ! -e op-program/bin ]; then \ @if [ ! -e op-program/bin ]; then \
make cannon-prestate; \ make cannon-prestate; \
fi fi
$(shell ./ops/scripts/newer-file.sh .devnet/allocs-l1.json ./packages/contracts-bedrock) ./ops/scripts/newer-file.sh .devnet/allocs-l1.json ./packages/contracts-bedrock \
if [ $(.SHELLSTATUS) -ne 0 ]; then \ || make devnet-allocs
make devnet-allocs; \
fi
PYTHONPATH=./bedrock-devnet python3 ./bedrock-devnet/main.py --monorepo-dir=. PYTHONPATH=./bedrock-devnet python3 ./bedrock-devnet/main.py --monorepo-dir=.
.PHONY: devnet-up .PHONY: devnet-up
......
...@@ -2,10 +2,13 @@ ...@@ -2,10 +2,13 @@
package database package database
import ( import (
"context"
"fmt" "fmt"
"github.com/ethereum-optimism/optimism/indexer/config" "github.com/ethereum-optimism/optimism/indexer/config"
_ "github.com/ethereum-optimism/optimism/indexer/database/serializers" _ "github.com/ethereum-optimism/optimism/indexer/database/serializers"
"github.com/ethereum-optimism/optimism/op-service/retry"
"github.com/pkg/errors"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -31,6 +34,8 @@ type DB struct { ...@@ -31,6 +34,8 @@ type DB struct {
} }
func NewDB(dbConfig config.DBConfig) (*DB, error) { func NewDB(dbConfig config.DBConfig) (*DB, error) {
retryStrategy := &retry.ExponentialStrategy{Min: 1000, Max: 20_000, MaxJitter: 250}
dsn := fmt.Sprintf("host=%s port=%d dbname=%s sslmode=disable", dbConfig.Host, dbConfig.Port, dbConfig.Name) dsn := fmt.Sprintf("host=%s port=%d dbname=%s sslmode=disable", dbConfig.Host, dbConfig.Port, dbConfig.Name)
if dbConfig.User != "" { if dbConfig.User != "" {
dsn += fmt.Sprintf(" user=%s", dbConfig.User) dsn += fmt.Sprintf(" user=%s", dbConfig.User)
...@@ -38,17 +43,24 @@ func NewDB(dbConfig config.DBConfig) (*DB, error) { ...@@ -38,17 +43,24 @@ func NewDB(dbConfig config.DBConfig) (*DB, error) {
if dbConfig.Password != "" { if dbConfig.Password != "" {
dsn += fmt.Sprintf(" password=%s", dbConfig.Password) dsn += fmt.Sprintf(" password=%s", dbConfig.Password)
} }
gorm, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
gormConfig := gorm.Config{
// The indexer will explicitly manage the transactions // The indexer will explicitly manage the transactions
SkipDefaultTransaction: true, SkipDefaultTransaction: true,
// We may choose to create an adapter such that the
// logger emits to the geth logger when on DEBUG mode
Logger: logger.Default.LogMode(logger.Silent), Logger: logger.Default.LogMode(logger.Silent),
}
gorm, err := retry.Do[*gorm.DB](context.Background(), 10, retryStrategy, func() (*gorm.DB, error) {
gorm, err := gorm.Open(postgres.Open(dsn), &gormConfig)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to database")
}
return gorm, nil
}) })
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, "failed to connect to database after multiple retries")
} }
db := &DB{ db := &DB{
......
...@@ -38,5 +38,6 @@ ...@@ -38,5 +38,6 @@
"SchemaRegistry", "SchemaRegistry",
"ProtocolVersions", "ProtocolVersions",
"Safe", "Safe",
"SafeProxyFactory" "SafeProxyFactory",
"DelayedVetoable"
] ]
This diff is collapsed.
// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.
package bindings
import (
"encoding/json"
"github.com/ethereum-optimism/optimism/op-bindings/solc"
)
const DelayedVetoableStorageLayoutJSON = "{\"storage\":[{\"astId\":1000,\"contract\":\"src/L1/DelayedVetoable.sol:DelayedVetoable\",\"label\":\"_delay\",\"offset\":0,\"slot\":\"0\",\"type\":\"t_uint256\"},{\"astId\":1001,\"contract\":\"src/L1/DelayedVetoable.sol:DelayedVetoable\",\"label\":\"_queuedAt\",\"offset\":0,\"slot\":\"1\",\"type\":\"t_mapping(t_bytes32,t_uint256)\"}],\"types\":{\"t_bytes32\":{\"encoding\":\"inplace\",\"label\":\"bytes32\",\"numberOfBytes\":\"32\"},\"t_mapping(t_bytes32,t_uint256)\":{\"encoding\":\"mapping\",\"label\":\"mapping(bytes32 =\u003e uint256)\",\"numberOfBytes\":\"32\",\"key\":\"t_bytes32\",\"value\":\"t_uint256\"},\"t_uint256\":{\"encoding\":\"inplace\",\"label\":\"uint256\",\"numberOfBytes\":\"32\"}}}"
var DelayedVetoableStorageLayout = new(solc.StorageLayout)
var DelayedVetoableDeployedBin = "0x608060405234801561001057600080fd5b50600436106100725760003560e01c8063b912de5d11610050578063b912de5d14610111578063d4b8399214610124578063d8bff4401461012c57610072565b806354fd4d501461007c5780635c39fcc1146100ce5780636a42b8f8146100fb575b61007a610134565b005b6100b86040518060400160405280600581526020017f312e302e3000000000000000000000000000000000000000000000000000000081525081565b6040516100c591906106a7565b60405180910390f35b6100d66104fb565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100c5565b610103610532565b6040519081526020016100c5565b61010361011f36600461071a565b610540565b6100d6610567565b6100d6610593565b361580156101425750600054155b15610298573373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000000000000000000000000000000000000000000016148015906101c357503373ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001614155b1561023d576040517f295a81c100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001660048201523360248201526044015b60405180910390fd5b7f000000000000000000000000000000000000000000000000000000000000000060008190556040519081527febf28bfb587e28dfffd9173cf71c32ba5d3f0544a0117b5539c9b274a5bba2a89060200160405180910390a1565b600080366040516102aa929190610733565b60405190819003902090503373ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000161480156103065750600081815260016020526040902054155b1561036c5760005460000361031e5761031e816105bf565b6000818152600160205260408082204290555182917f87a332a414acbc7da074543639ce7ae02ff1ea72e88379da9f261b080beb5a139161036191903690610743565b60405180910390a250565b3373ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000161480156103be575060008181526001602052604090205415155b15610406576000818152600160205260408082208290555182917fbede6852c1d97d93ff557f676de76670cd0dec861e7fe8beb13aa0ba2b0ab0409161036191903690610743565b600081815260016020526040812054900361048b576040517f295a81c100000000000000000000000000000000000000000000000000000000815273ffffffffffffffffffffffffffffffffffffffff7f0000000000000000000000000000000000000000000000000000000000000000166004820152336024820152604401610234565b60008054828252600160205260409091205442916104a891610790565b10156104e0576040517f43dc986d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b6000818152600160205260408120556104f8816105bf565b50565b60003361052757507f000000000000000000000000000000000000000000000000000000000000000090565b61052f610134565b90565b600033610527575060005490565b60003361055a575060009081526001602052604090205490565b610562610134565b919050565b60003361052757507f000000000000000000000000000000000000000000000000000000000000000090565b60003361052757507f000000000000000000000000000000000000000000000000000000000000000090565b807f4c109d85bcd0bb5c735b4be850953d652afe4cd9aa2e0b1426a65a4dcb2e12296000366040516105f2929190610743565b60405180910390a26000807f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff16600036604051610645929190610733565b6000604051808303816000865af19150503d8060008114610682576040519150601f19603f3d011682016040523d82523d6000602084013e610687565b606091505b50909250905081151560010361069f57805160208201f35b805160208201fd5b600060208083528351808285015260005b818110156106d4578581018301518582016040015282016106b8565b818111156106e6576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b60006020828403121561072c57600080fd5b5035919050565b8183823760009101908152919050565b60208152816020820152818360408301376000818301604090810191909152601f9092017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0160101919050565b600082198211156107ca577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b50019056fea164736f6c634300080f000a"
func init() {
if err := json.Unmarshal([]byte(DelayedVetoableStorageLayoutJSON), DelayedVetoableStorageLayout); err != nil {
panic(err)
}
layouts["DelayedVetoable"] = DelayedVetoableStorageLayout
deployedBytecodes["DelayedVetoable"] = DelayedVetoableDeployedBin
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -10,5 +10,17 @@ LDFLAGS := -ldflags "$(LDFLAGSSTRING)" ...@@ -10,5 +10,17 @@ LDFLAGS := -ldflags "$(LDFLAGSSTRING)"
op-bootnode: op-bootnode:
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/op-bootnode ./cmd env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/op-bootnode ./cmd
clean:
rm -f bin/op-bootnode
test:
go test -v ./...
lint: lint:
golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint --timeout 5m -e "errors.As" -e "errors.Is" ./... golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint --timeout 5m -e "errors.As" -e "errors.Is" ./...
.PHONY: \
op-bootnode \
clean \
test \
lint
...@@ -18,6 +18,7 @@ import ( ...@@ -18,6 +18,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/opio" "github.com/ethereum-optimism/optimism/op-service/opio"
) )
...@@ -69,6 +70,17 @@ func Main(cliCtx *cli.Context) error { ...@@ -69,6 +70,17 @@ func Main(cliCtx *cli.Context) error {
go p2pNode.DiscoveryProcess(ctx, logger, config, p2pConfig.TargetPeers()) go p2pNode.DiscoveryProcess(ctx, logger, config, p2pConfig.TargetPeers())
metricsCfg := opmetrics.ReadCLIConfig(cliCtx)
if metricsCfg.Enabled {
log.Info("starting metrics server", "addr", metricsCfg.ListenAddr, "port", metricsCfg.ListenPort)
go func() {
if err := m.Serve(ctx, metricsCfg.ListenAddr, metricsCfg.ListenPort); err != nil {
log.Error("error starting metrics server", err)
}
}()
m.RecordUp()
}
opio.BlockOnInterrupts() opio.BlockOnInterrupts()
return nil return nil
......
...@@ -3,21 +3,16 @@ package main ...@@ -3,21 +3,16 @@ package main
import ( import (
"os" "os"
"github.com/ethereum-optimism/optimism/op-bootnode/bootnode"
"github.com/ethereum-optimism/optimism/op-bootnode/flags"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/ethereum-optimism/optimism/op-bootnode/bootnode"
"github.com/ethereum-optimism/optimism/op-bootnode/flags"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
) )
func main() { func main() {
// Set up logger with a default INFO level in case we fail to parse flags, oplog.SetupDefaults()
// otherwise the final critical log won't show what the parsing error was.
log.Root().SetHandler(
log.LvlFilterHandler(
log.LvlInfo,
log.StreamHandler(os.Stdout, log.TerminalFormat(true)),
),
)
app := cli.NewApp() app := cli.NewApp()
app.Flags = flags.Flags app.Flags = flags.Flags
......
...@@ -8,6 +8,7 @@ import ( ...@@ -8,6 +8,7 @@ import (
"github.com/ethereum-optimism/optimism/op-node/flags" "github.com/ethereum-optimism/optimism/op-node/flags"
opservice "github.com/ethereum-optimism/optimism/op-service" opservice "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
...@@ -36,5 +37,7 @@ var Flags = []cli.Flag{ ...@@ -36,5 +37,7 @@ var Flags = []cli.Flag{
} }
func init() { func init() {
Flags = append(Flags, flags.P2pFlags...)
Flags = append(Flags, opmetrics.CLIFlags(envVarPrefix)...)
Flags = append(Flags, oplog.CLIFlags(envVarPrefix)...) Flags = append(Flags, oplog.CLIFlags(envVarPrefix)...)
} }
...@@ -24,6 +24,7 @@ var ( ...@@ -24,6 +24,7 @@ var (
cannonPreState = "./pre.json" cannonPreState = "./pre.json"
datadir = "./test_data" datadir = "./test_data"
cannonL2 = "http://example.com:9545" cannonL2 = "http://example.com:9545"
rollupRpc = "http://example.com:8555"
alphabetTrace = "abcdefghijz" alphabetTrace = "abcdefghijz"
agreeWithProposedOutput = "true" agreeWithProposedOutput = "true"
) )
...@@ -249,6 +250,25 @@ func TestDataDir(t *testing.T) { ...@@ -249,6 +250,25 @@ func TestDataDir(t *testing.T) {
}) })
} }
func TestRollupRpc(t *testing.T) {
t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) {
configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--rollup-rpc"))
})
t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) {
configForArgs(t, addRequiredArgsExcept(config.TraceTypeCannon, "--rollup-rpc"))
})
t.Run("RequiredForOutputCannonTrace", func(t *testing.T) {
verifyArgsInvalid(t, "flag rollup-rpc is required", addRequiredArgsExcept(config.TraceTypeOutputCannon, "--rollup-rpc"))
})
t.Run("Valid", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs(config.TraceTypeOutputCannon))
require.Equal(t, rollupRpc, cfg.RollupRpc)
})
}
func TestCannonL2(t *testing.T) { func TestCannonL2(t *testing.T) {
t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) {
configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-l2")) configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-l2"))
...@@ -429,6 +449,9 @@ func requiredArgs(traceType config.TraceType) map[string]string { ...@@ -429,6 +449,9 @@ func requiredArgs(traceType config.TraceType) map[string]string {
args["--cannon-prestate"] = cannonPreState args["--cannon-prestate"] = cannonPreState
args["--cannon-l2"] = cannonL2 args["--cannon-l2"] = cannonL2
} }
if traceType == config.TraceTypeOutputCannon {
args["--rollup-rpc"] = rollupRpc
}
return args return args
} }
......
...@@ -32,6 +32,7 @@ var ( ...@@ -32,6 +32,7 @@ var (
ErrCannonNetworkAndRollupConfig = errors.New("only specify one of network or rollup config path") ErrCannonNetworkAndRollupConfig = errors.New("only specify one of network or rollup config path")
ErrCannonNetworkAndL2Genesis = errors.New("only specify one of network or l2 genesis path") ErrCannonNetworkAndL2Genesis = errors.New("only specify one of network or l2 genesis path")
ErrCannonNetworkUnknown = errors.New("unknown cannon network") ErrCannonNetworkUnknown = errors.New("unknown cannon network")
ErrMissingRollupRpc = errors.New("missing rollup rpc url")
) )
type TraceType string type TraceType string
...@@ -107,6 +108,9 @@ type Config struct { ...@@ -107,6 +108,9 @@ type Config struct {
// Specific to the alphabet trace provider // Specific to the alphabet trace provider
AlphabetTrace string // String for the AlphabetTraceProvider AlphabetTrace string // String for the AlphabetTraceProvider
// Specific to the output cannon trace type
RollupRpc string
// Specific to the cannon trace provider // Specific to the cannon trace provider
CannonBin string // Path to the cannon executable to run when generating trace data CannonBin string // Path to the cannon executable to run when generating trace data
CannonServer string // Path to the op-program executable that provides the pre-image oracle server CannonServer string // Path to the op-program executable that provides the pre-image oracle server
...@@ -168,6 +172,11 @@ func (c Config) Check() error { ...@@ -168,6 +172,11 @@ func (c Config) Check() error {
if c.MaxConcurrency == 0 { if c.MaxConcurrency == 0 {
return ErrMaxConcurrencyZero return ErrMaxConcurrencyZero
} }
if c.TraceType == TraceTypeOutputCannon {
if c.RollupRpc == "" {
return ErrMissingRollupRpc
}
}
if c.TraceType == TraceTypeCannon || c.TraceType == TraceTypeOutputCannon { if c.TraceType == TraceTypeCannon || c.TraceType == TraceTypeOutputCannon {
if c.CannonBin == "" { if c.CannonBin == "" {
return ErrMissingCannonBin return ErrMissingCannonBin
......
...@@ -20,6 +20,7 @@ var ( ...@@ -20,6 +20,7 @@ var (
validCannonAbsolutPreState = "pre.json" validCannonAbsolutPreState = "pre.json"
validDatadir = "/tmp/data" validDatadir = "/tmp/data"
validCannonL2 = "http://localhost:9545" validCannonL2 = "http://localhost:9545"
validRollupRpc = "http://localhost:8555"
agreeWithProposedOutput = true agreeWithProposedOutput = true
) )
...@@ -35,6 +36,9 @@ func validConfig(traceType TraceType) Config { ...@@ -35,6 +36,9 @@ func validConfig(traceType TraceType) Config {
cfg.CannonL2 = validCannonL2 cfg.CannonL2 = validCannonL2
cfg.CannonNetwork = validCannonNetwork cfg.CannonNetwork = validCannonNetwork
} }
if traceType == TraceTypeOutputCannon {
cfg.RollupRpc = validRollupRpc
}
return cfg return cfg
} }
...@@ -125,6 +129,12 @@ func TestHttpPollInterval(t *testing.T) { ...@@ -125,6 +129,12 @@ func TestHttpPollInterval(t *testing.T) {
}) })
} }
func TestRollupRpcRequired(t *testing.T) {
config := validConfig(TraceTypeOutputCannon)
config.RollupRpc = ""
require.ErrorIs(t, config.Check(), ErrMissingRollupRpc)
}
func TestCannonL2Required(t *testing.T) { func TestCannonL2Required(t *testing.T) {
config := validConfig(TraceTypeCannon) config := validConfig(TraceTypeCannon)
config.CannonL2 = "" config.CannonL2 = ""
......
...@@ -76,6 +76,11 @@ var ( ...@@ -76,6 +76,11 @@ var (
EnvVars: prefixEnvVars("HTTP_POLL_INTERVAL"), EnvVars: prefixEnvVars("HTTP_POLL_INTERVAL"),
Value: config.DefaultPollInterval, Value: config.DefaultPollInterval,
} }
RollupRpcFlag = &cli.StringFlag{
Name: "rollup-rpc",
Usage: "HTTP provider URL for the rollup node",
EnvVars: prefixEnvVars("ROLLUP_RPC"),
}
AlphabetFlag = &cli.StringFlag{ AlphabetFlag = &cli.StringFlag{
Name: "alphabet", Name: "alphabet",
Usage: "Correct Alphabet Trace (alphabet trace type only)", Usage: "Correct Alphabet Trace (alphabet trace type only)",
...@@ -83,7 +88,10 @@ var ( ...@@ -83,7 +88,10 @@ var (
} }
CannonNetworkFlag = &cli.StringFlag{ CannonNetworkFlag = &cli.StringFlag{
Name: "cannon-network", Name: "cannon-network",
Usage: fmt.Sprintf("Predefined network selection. Available networks: %s (cannon trace type only)", strings.Join(chaincfg.AvailableNetworks(), ", ")), Usage: fmt.Sprintf(
"Predefined network selection. Available networks: %s (cannon trace type only)",
strings.Join(chaincfg.AvailableNetworks(), ", "),
),
EnvVars: prefixEnvVars("CANNON_NETWORK"), EnvVars: prefixEnvVars("CANNON_NETWORK"),
} }
CannonRollupConfigFlag = &cli.StringFlag{ CannonRollupConfigFlag = &cli.StringFlag{
...@@ -149,6 +157,7 @@ var requiredFlags = []cli.Flag{ ...@@ -149,6 +157,7 @@ var requiredFlags = []cli.Flag{
var optionalFlags = []cli.Flag{ var optionalFlags = []cli.Flag{
MaxConcurrencyFlag, MaxConcurrencyFlag,
HTTPPollInterval, HTTPPollInterval,
RollupRpcFlag,
AlphabetFlag, AlphabetFlag,
GameAllowlistFlag, GameAllowlistFlag,
CannonNetworkFlag, CannonNetworkFlag,
...@@ -221,6 +230,9 @@ func CheckRequired(ctx *cli.Context) error { ...@@ -221,6 +230,9 @@ func CheckRequired(ctx *cli.Context) error {
if err := CheckCannonFlags(ctx); err != nil { if err := CheckCannonFlags(ctx); err != nil {
return err return err
} }
if !ctx.IsSet(RollupRpcFlag.Name) {
return fmt.Errorf("flag %s is required", RollupRpcFlag.Name)
}
default: default:
return fmt.Errorf("invalid trace type. must be one of %v", config.TraceTypes) return fmt.Errorf("invalid trace type. must be one of %v", config.TraceTypes)
} }
...@@ -266,6 +278,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { ...@@ -266,6 +278,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
GameWindow: ctx.Duration(GameWindowFlag.Name), GameWindow: ctx.Duration(GameWindowFlag.Name),
MaxConcurrency: maxConcurrency, MaxConcurrency: maxConcurrency,
PollInterval: ctx.Duration(HTTPPollInterval.Name), PollInterval: ctx.Duration(HTTPPollInterval.Name),
RollupRpc: ctx.String(RollupRpcFlag.Name),
AlphabetTrace: ctx.String(AlphabetFlag.Name), AlphabetTrace: ctx.String(AlphabetFlag.Name),
CannonNetwork: ctx.String(CannonNetworkFlag.Name), CannonNetwork: ctx.String(CannonNetworkFlag.Name),
CannonRollupConfigPath: ctx.String(CannonRollupConfigFlag.Name), CannonRollupConfigPath: ctx.String(CannonRollupConfigFlag.Name),
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"sync"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/solver"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
...@@ -18,6 +19,8 @@ import ( ...@@ -18,6 +19,8 @@ import (
type Responder interface { type Responder interface {
CallResolve(ctx context.Context) (gameTypes.GameStatus, error) CallResolve(ctx context.Context) (gameTypes.GameStatus, error)
Resolve(ctx context.Context) error Resolve(ctx context.Context) error
CallResolveClaim(ctx context.Context, claimIdx uint64) error
ResolveClaim(ctx context.Context, claimIdx uint64) error
PerformAction(ctx context.Context, action types.Action) error PerformAction(ctx context.Context, action types.Action) error
} }
...@@ -112,6 +115,10 @@ func (a *Agent) shouldResolve(status gameTypes.GameStatus) bool { ...@@ -112,6 +115,10 @@ func (a *Agent) shouldResolve(status gameTypes.GameStatus) bool {
// tryResolve resolves the game if it is in a winning state // tryResolve resolves the game if it is in a winning state
// Returns true if the game is resolvable (regardless of whether it was actually resolved) // Returns true if the game is resolvable (regardless of whether it was actually resolved)
func (a *Agent) tryResolve(ctx context.Context) bool { func (a *Agent) tryResolve(ctx context.Context) bool {
if err := a.resolveClaims(ctx); err != nil {
a.log.Error("Failed to resolve claims", "err", err)
return false
}
status, err := a.responder.CallResolve(ctx) status, err := a.responder.CallResolve(ctx)
if err != nil || status == gameTypes.GameStatusInProgress { if err != nil || status == gameTypes.GameStatusInProgress {
return false return false
...@@ -126,6 +133,60 @@ func (a *Agent) tryResolve(ctx context.Context) bool { ...@@ -126,6 +133,60 @@ func (a *Agent) tryResolve(ctx context.Context) bool {
return true return true
} }
var errNoResolvableClaims = errors.New("no resolvable claims")
func (a *Agent) tryResolveClaims(ctx context.Context) error {
claims, err := a.loader.FetchClaims(ctx)
if err != nil {
return fmt.Errorf("failed to fetch claims: %w", err)
}
if len(claims) == 0 {
return errNoResolvableClaims
}
var resolvableClaims []int64
for _, claim := range claims {
a.log.Debug("checking if claim is resolvable", "claimIdx", claim.ContractIndex)
if err := a.responder.CallResolveClaim(ctx, uint64(claim.ContractIndex)); err == nil {
a.log.Info("Resolving claim", "claimIdx", claim.ContractIndex)
resolvableClaims = append(resolvableClaims, int64(claim.ContractIndex))
}
}
a.log.Info("Resolving claims", "numClaims", len(resolvableClaims))
if len(resolvableClaims) == 0 {
return errNoResolvableClaims
}
var wg sync.WaitGroup
wg.Add(len(resolvableClaims))
for _, claimIdx := range resolvableClaims {
claimIdx := claimIdx
go func() {
defer wg.Done()
err := a.responder.ResolveClaim(ctx, uint64(claimIdx))
if err != nil {
a.log.Error("Failed to resolve claim", "err", err)
}
}()
}
wg.Wait()
return nil
}
func (a *Agent) resolveClaims(ctx context.Context) error {
for {
err := a.tryResolveClaims(ctx)
switch err {
case errNoResolvableClaims:
return nil
case nil:
continue
default:
return err
}
}
}
// newGameFromContracts initializes a new game state from the state in the contract // newGameFromContracts initializes a new game state from the state in the contract
func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) { func (a *Agent) newGameFromContracts(ctx context.Context) (types.Game, error) {
claims, err := a.loader.FetchClaims(ctx) claims, err := a.loader.FetchClaims(ctx)
......
...@@ -77,7 +77,7 @@ func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) { ...@@ -77,7 +77,7 @@ func TestDoNotMakeMovesWhenGameIsResolvable(t *testing.T) {
require.NoError(t, agent.Act(ctx)) require.NoError(t, agent.Act(ctx))
require.Equal(t, 1, responder.callResolveCount, "should check if game is resolvable") require.Equal(t, 1, responder.callResolveCount, "should check if game is resolvable")
require.Zero(t, claimLoader.callCount, "should not fetch claims for resolvable game") require.Equal(t, 1, claimLoader.callCount, "should fetch claims once for resolveClaim")
if test.shouldResolve { if test.shouldResolve {
require.EqualValues(t, 1, responder.resolveCount, "should resolve winning game") require.EqualValues(t, 1, responder.resolveCount, "should resolve winning game")
...@@ -92,6 +92,7 @@ func TestLoadClaimsWhenGameNotResolvable(t *testing.T) { ...@@ -92,6 +92,7 @@ func TestLoadClaimsWhenGameNotResolvable(t *testing.T) {
// Checks that if the game isn't resolvable, that the agent continues on to start checking claims // Checks that if the game isn't resolvable, that the agent continues on to start checking claims
agent, claimLoader, responder := setupTestAgent(t, false) agent, claimLoader, responder := setupTestAgent(t, false)
responder.callResolveErr = errors.New("game is not resolvable") responder.callResolveErr = errors.New("game is not resolvable")
responder.callResolveClaimErr = errors.New("claim is not resolvable")
depth := 4 depth := 4
claimBuilder := test.NewClaimBuilder(t, depth, alphabet.NewTraceProvider("abcdefg", uint64(depth))) claimBuilder := test.NewClaimBuilder(t, depth, alphabet.NewTraceProvider("abcdefg", uint64(depth)))
...@@ -101,7 +102,9 @@ func TestLoadClaimsWhenGameNotResolvable(t *testing.T) { ...@@ -101,7 +102,9 @@ func TestLoadClaimsWhenGameNotResolvable(t *testing.T) {
require.NoError(t, agent.Act(context.Background())) require.NoError(t, agent.Act(context.Background()))
require.EqualValues(t, 1, claimLoader.callCount, "should load claims for unresolvable game") require.EqualValues(t, 2, claimLoader.callCount, "should load claims for unresolvable game")
require.EqualValues(t, responder.callResolveClaimCount, 1, "should check if claim is resolvable")
require.Zero(t, responder.resolveClaimCount, "should not send resolveClaim")
} }
func setupTestAgent(t *testing.T, agreeWithProposedOutput bool) (*Agent, *stubClaimLoader, *stubResponder) { func setupTestAgent(t *testing.T, agreeWithProposedOutput bool) (*Agent, *stubClaimLoader, *stubResponder) {
...@@ -132,6 +135,10 @@ type stubResponder struct { ...@@ -132,6 +135,10 @@ type stubResponder struct {
resolveCount int resolveCount int
resolveErr error resolveErr error
callResolveClaimCount int
callResolveClaimErr error
resolveClaimCount int
} }
func (s *stubResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) { func (s *stubResponder) CallResolve(ctx context.Context) (gameTypes.GameStatus, error) {
...@@ -144,8 +151,18 @@ func (s *stubResponder) Resolve(ctx context.Context) error { ...@@ -144,8 +151,18 @@ func (s *stubResponder) Resolve(ctx context.Context) error {
return s.resolveErr return s.resolveErr
} }
func (s *stubResponder) CallResolveClaim(ctx context.Context, clainIdx uint64) error {
s.callResolveClaimCount++
return s.callResolveClaimErr
}
func (s *stubResponder) ResolveClaim(ctx context.Context, clainIdx uint64) error {
s.resolveClaimCount++
return nil
}
func (s *stubResponder) PerformAction(ctx context.Context, response types.Action) error { func (s *stubResponder) PerformAction(ctx context.Context, response types.Action) error {
panic("Not implemented") return nil
} }
type stubUpdater struct { type stubUpdater struct {
......
...@@ -114,6 +114,10 @@ func NewGamePlayer( ...@@ -114,6 +114,10 @@ func NewGamePlayer(
}, nil }, nil
} }
func (g *GamePlayer) Status() gameTypes.GameStatus {
return g.status
}
func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus { func (g *GamePlayer) ProgressGame(ctx context.Context) gameTypes.GameStatus {
if g.status != gameTypes.GameStatusInProgress { if g.status != gameTypes.GameStatusInProgress {
// Game is already complete so don't try to perform further actions. // Game is already complete so don't try to perform further actions.
......
...@@ -94,6 +94,34 @@ func (r *FaultResponder) Resolve(ctx context.Context) error { ...@@ -94,6 +94,34 @@ func (r *FaultResponder) Resolve(ctx context.Context) error {
return r.sendTxAndWait(ctx, txData) return r.sendTxAndWait(ctx, txData)
} }
// buildResolveClaimData creates the transaction data for the ResolveClaim function.
func (r *FaultResponder) buildResolveClaimData(ctx context.Context, claimIdx uint64) ([]byte, error) {
return r.fdgAbi.Pack("resolveClaim", big.NewInt(int64(claimIdx)))
}
// CallResolveClaim determines if the resolveClaim function on the fault dispute game contract
// would succeed.
func (r *FaultResponder) CallResolveClaim(ctx context.Context, claimIdx uint64) error {
txData, err := r.buildResolveClaimData(ctx, claimIdx)
if err != nil {
return err
}
_, err = r.txMgr.Call(ctx, ethereum.CallMsg{
To: &r.fdgAddr,
Data: txData,
}, nil)
return err
}
// ResolveClaim executes a resolveClaim transaction to resolve a fault dispute game.
func (r *FaultResponder) ResolveClaim(ctx context.Context, claimIdx uint64) error {
txData, err := r.buildResolveClaimData(ctx, claimIdx)
if err != nil {
return err
}
return r.sendTxAndWait(ctx, txData)
}
func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error { func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error {
var txData []byte var txData []byte
var err error var err error
......
...@@ -73,6 +73,40 @@ func TestResolve(t *testing.T) { ...@@ -73,6 +73,40 @@ func TestResolve(t *testing.T) {
}) })
} }
func TestCallResolveClaim(t *testing.T) {
t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
mockTxMgr.callFails = true
err := responder.CallResolveClaim(context.Background(), 0)
require.ErrorIs(t, err, mockCallError)
require.Equal(t, 0, mockTxMgr.calls)
})
t.Run("Success", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
err := responder.CallResolveClaim(context.Background(), 0)
require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.calls)
})
}
func TestResolveClaim(t *testing.T) {
t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
mockTxMgr.sendFails = true
err := responder.ResolveClaim(context.Background(), 0)
require.ErrorIs(t, err, mockSendError)
require.Equal(t, 0, mockTxMgr.sends)
})
t.Run("Success", func(t *testing.T) {
responder, mockTxMgr := newTestFaultResponder(t)
err := responder.ResolveClaim(context.Background(), 0)
require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends)
})
}
// TestRespond tests the [Responder.Respond] method. // TestRespond tests the [Responder.Respond] method.
func TestPerformAction(t *testing.T) { func TestPerformAction(t *testing.T) {
t.Run("send fails", func(t *testing.T) { t.Run("send fails", func(t *testing.T) {
......
...@@ -48,7 +48,10 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t ...@@ -48,7 +48,10 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t
if game.AgreeWithClaimLevel(claim) { if game.AgreeWithClaimLevel(claim) {
return nil, nil return nil, nil
} }
step, err := s.claimSolver.AttemptStep(ctx, claim, game.AgreeWithClaimLevel(claim)) step, err := s.claimSolver.AttemptStep(ctx, game, claim)
if err == ErrStepIgnoreInvalidPath {
return nil, nil
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -63,11 +66,14 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t ...@@ -63,11 +66,14 @@ func (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim t
} }
func (s *GameSolver) calculateMove(ctx context.Context, game types.Game, claim types.Claim) (*types.Action, error) { func (s *GameSolver) calculateMove(ctx context.Context, game types.Game, claim types.Claim) (*types.Action, error) {
move, err := s.claimSolver.NextMove(ctx, claim, game.AgreeWithClaimLevel(claim)) if game.AgreeWithClaimLevel(claim) {
return nil, nil
}
move, err := s.claimSolver.NextMove(ctx, claim, game)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to calculate next move for claim index %v: %w", claim.ContractIndex, err) return nil, fmt.Errorf("failed to calculate next move for claim index %v: %w", claim.ContractIndex, err)
} }
if move == nil || game.IsDuplicate(move.ClaimData) { if move == nil || game.IsDuplicate(*move) {
return nil, nil return nil, nil
} }
return &types.Action{ return &types.Action{
......
...@@ -48,7 +48,6 @@ func TestCalculateNextActions(t *testing.T) { ...@@ -48,7 +48,6 @@ func TestCalculateNextActions(t *testing.T) {
rootClaimCorrect: true, rootClaimCorrect: true,
setupGame: func(builder *faulttest.GameBuilder) {}, setupGame: func(builder *faulttest.GameBuilder) {},
}, },
{ {
name: "DoNotPerformDuplicateMoves", name: "DoNotPerformDuplicateMoves",
agreeWithOutputRoot: true, agreeWithOutputRoot: true,
...@@ -93,16 +92,15 @@ func TestCalculateNextActions(t *testing.T) { ...@@ -93,16 +92,15 @@ func TestCalculateNextActions(t *testing.T) {
maliciousStateHash := common.Hash{0x01, 0xaa} maliciousStateHash := common.Hash{0x01, 0xaa}
// Dishonest actor counters their own claims to set up a situation with an invalid prestate // Dishonest actor counters their own claims to set up a situation with an invalid prestate
// The honest actor should attack all claims that support the root claim (disagree with the output root) // The honest actor should ignore path created by the dishonest actor, only supporting its own attack on the root claim
builder.Seq().ExpectAttack(). // This expected action is the winning move. honestMove := builder.Seq().AttackCorrect() // This expected action is the winning move.
Attack(maliciousStateHash). dishonestMove := honestMove.Attack(maliciousStateHash)
Defend(maliciousStateHash).ExpectAttack(). // The expected action by the honest actor
Attack(maliciousStateHash). dishonestMove.ExpectAttack()
Attack(maliciousStateHash).ExpectStepAttack() // The honest actor will ignore this poisoned path
dishonestMove.
// The attempt to step against our malicious leaf node will fail because the pre-state won't match our Defend(maliciousStateHash).
// malicious state hash. However, it is the very first expected action, attacking the root claim with Attack(maliciousStateHash)
// the correct hash that wins the game since it will be the left-most uncountered claim.
}, },
}, },
} }
......
...@@ -13,7 +13,6 @@ var rules = []actionRule{ ...@@ -13,7 +13,6 @@ var rules = []actionRule{
parentMustExist, parentMustExist,
onlyStepAtMaxDepth, onlyStepAtMaxDepth,
onlyMoveBeforeMaxDepth, onlyMoveBeforeMaxDepth,
onlyCounterClaimsAtDisagreeingLevels,
doNotDuplicateExistingMoves, doNotDuplicateExistingMoves,
doNotDefendRootClaim, doNotDefendRootClaim,
} }
...@@ -57,20 +56,12 @@ func onlyMoveBeforeMaxDepth(game types.Game, action types.Action) error { ...@@ -57,20 +56,12 @@ func onlyMoveBeforeMaxDepth(game types.Game, action types.Action) error {
return nil return nil
} }
func onlyCounterClaimsAtDisagreeingLevels(game types.Game, action types.Action) error {
parentClaim := game.Claims()[action.ParentIdx]
if game.AgreeWithClaimLevel(parentClaim) {
return fmt.Errorf("countering a claim at depth %v that supports our view of the root", parentClaim.Position.Depth())
}
return nil
}
func doNotDuplicateExistingMoves(game types.Game, action types.Action) error { func doNotDuplicateExistingMoves(game types.Game, action types.Action) error {
newClaimData := types.ClaimData{ newClaimData := types.ClaimData{
Value: action.Value, Value: action.Value,
Position: resultingPosition(game, action), Position: resultingPosition(game, action),
} }
if game.IsDuplicate(newClaimData) { if game.IsDuplicate(types.Claim{ClaimData: newClaimData, ParentContractIndex: action.ParentIdx}) {
return fmt.Errorf("creating duplicate claim at %v with value %v", newClaimData.Position.ToGIndex(), newClaimData.Value) return fmt.Errorf("creating duplicate claim at %v with value %v", newClaimData.Position.ToGIndex(), newClaimData.Value)
} }
return nil return nil
......
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
var ( var (
ErrStepNonLeafNode = errors.New("cannot step on non-leaf claims") ErrStepNonLeafNode = errors.New("cannot step on non-leaf claims")
ErrStepAgreedClaim = errors.New("cannot step on claims we agree with") ErrStepAgreedClaim = errors.New("cannot step on claims we agree with")
ErrStepIgnoreInvalidPath = errors.New("cannot step on claims that dispute invalid paths")
) )
// claimSolver uses a [TraceProvider] to determine the moves to make in a dispute game. // claimSolver uses a [TraceProvider] to determine the moves to make in a dispute game.
...@@ -30,10 +31,7 @@ func newClaimSolver(gameDepth int, traceProvider types.TraceProvider) *claimSolv ...@@ -30,10 +31,7 @@ func newClaimSolver(gameDepth int, traceProvider types.TraceProvider) *claimSolv
} }
// NextMove returns the next move to make given the current state of the game. // NextMove returns the next move to make given the current state of the game.
func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, agreeWithClaimLevel bool) (*types.Claim, error) { func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, game types.Game) (*types.Claim, error) {
if agreeWithClaimLevel {
return nil, nil
}
if claim.Depth() == s.gameDepth { if claim.Depth() == s.gameDepth {
return nil, types.ErrGameDepthReached return nil, types.ErrGameDepthReached
} }
...@@ -41,6 +39,24 @@ func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, agreeWith ...@@ -41,6 +39,24 @@ func (s *claimSolver) NextMove(ctx context.Context, claim types.Claim, agreeWith
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Before challenging this claim, first check that the move wasn't warranted.
// If the parent claim is on a dishonest path, then we would have moved against it anyways. So we don't move.
// Avoiding dishonest paths ensures that there's always a valid claim available to support ours during step.
if !claim.IsRoot() {
parent, err := game.GetParent(claim)
if err != nil {
return nil, err
}
agreeWithParent, err := s.agreeWithClaimPath(ctx, game, parent)
if err != nil {
return nil, err
}
if !agreeWithParent {
return nil, nil
}
}
if agree { if agree {
return s.defend(ctx, claim) return s.defend(ctx, claim)
} else { } else {
...@@ -58,13 +74,25 @@ type StepData struct { ...@@ -58,13 +74,25 @@ type StepData struct {
// AttemptStep determines what step should occur for a given leaf claim. // AttemptStep determines what step should occur for a given leaf claim.
// An error will be returned if the claim is not at the max depth. // An error will be returned if the claim is not at the max depth.
func (s *claimSolver) AttemptStep(ctx context.Context, claim types.Claim, agreeWithClaimLevel bool) (StepData, error) { // Returns ErrStepIgnoreInvalidPath if the claim disputes an invalid path
func (s *claimSolver) AttemptStep(ctx context.Context, game types.Game, claim types.Claim) (StepData, error) {
if claim.Depth() != s.gameDepth { if claim.Depth() != s.gameDepth {
return StepData{}, ErrStepNonLeafNode return StepData{}, ErrStepNonLeafNode
} }
if agreeWithClaimLevel {
return StepData{}, ErrStepAgreedClaim // Step only on claims that dispute a valid path
parent, err := game.GetParent(claim)
if err != nil {
return StepData{}, err
}
parentValid, err := s.agreeWithClaimPath(ctx, game, parent)
if err != nil {
return StepData{}, err
}
if !parentValid {
return StepData{}, ErrStepIgnoreInvalidPath
} }
claimCorrect, err := s.agreeWithClaim(ctx, claim.ClaimData) claimCorrect, err := s.agreeWithClaim(ctx, claim.ClaimData)
if err != nil { if err != nil {
return StepData{}, err return StepData{}, err
...@@ -142,3 +170,26 @@ func (s *claimSolver) traceAtPosition(ctx context.Context, p types.Position) (co ...@@ -142,3 +170,26 @@ func (s *claimSolver) traceAtPosition(ctx context.Context, p types.Position) (co
hash, err := s.trace.Get(ctx, index) hash, err := s.trace.Get(ctx, index)
return hash, err return hash, err
} }
// agreeWithClaimPath returns true if the every other claim in the path to root is correct according to the internal [TraceProvider].
func (s *claimSolver) agreeWithClaimPath(ctx context.Context, game types.Game, claim types.Claim) (bool, error) {
agree, err := s.agreeWithClaim(ctx, claim.ClaimData)
if err != nil {
return false, err
}
if !agree {
return false, nil
}
if claim.IsRoot() || claim.Parent.IsRootPosition() {
return true, nil
}
parent, err := game.GetParent(claim)
if err != nil {
return false, err
}
grandParent, err := game.GetParent(parent)
if err != nil {
return false, err
}
return s.agreeWithClaimPath(ctx, game, grandParent)
}
...@@ -79,12 +79,13 @@ func (c *ClaimBuilder) claim(idx uint64, correct bool) common.Hash { ...@@ -79,12 +79,13 @@ func (c *ClaimBuilder) claim(idx uint64, correct bool) common.Hash {
func (c *ClaimBuilder) CreateRootClaim(correct bool) types.Claim { func (c *ClaimBuilder) CreateRootClaim(correct bool) types.Claim {
value := c.claim((1<<c.maxDepth)-1, correct) value := c.claim((1<<c.maxDepth)-1, correct)
return types.Claim{ claim := types.Claim{
ClaimData: types.ClaimData{ ClaimData: types.ClaimData{
Value: value, Value: value,
Position: types.NewPosition(0, 0), Position: types.NewPosition(0, 0),
}, },
} }
return claim
} }
func (c *ClaimBuilder) CreateLeafClaim(traceIndex uint64, correct bool) types.Claim { func (c *ClaimBuilder) CreateLeafClaim(traceIndex uint64, correct bool) types.Claim {
......
...@@ -23,8 +23,12 @@ type Game interface { ...@@ -23,8 +23,12 @@ type Game interface {
// Claims returns all of the claims in the game. // Claims returns all of the claims in the game.
Claims() []Claim Claims() []Claim
// IsDuplicate returns true if the provided [Claim] already exists in the game state. // GetParent returns the parent of the provided claim.
IsDuplicate(claim ClaimData) bool GetParent(claim Claim) (Claim, error)
// IsDuplicate returns true if the provided [Claim] already exists in the game state
// referencing the same parent claim
IsDuplicate(claim Claim) bool
// AgreeWithClaimLevel returns if the game state agrees with the provided claim level. // AgreeWithClaimLevel returns if the game state agrees with the provided claim level.
AgreeWithClaimLevel(claim Claim) bool AgreeWithClaimLevel(claim Claim) bool
...@@ -32,32 +36,45 @@ type Game interface { ...@@ -32,32 +36,45 @@ type Game interface {
MaxDepth() uint64 MaxDepth() uint64
} }
type claimEntry struct {
ClaimData
ParentContractIndex int
}
type extendedClaim struct { type extendedClaim struct {
self Claim self Claim
children []ClaimData children []claimEntry
} }
// gameState is a struct that represents the state of a dispute game. // gameState is a struct that represents the state of a dispute game.
// The game state implements the [Game] interface. // The game state implements the [Game] interface.
type gameState struct { type gameState struct {
agreeWithProposedOutput bool agreeWithProposedOutput bool
root ClaimData root claimEntry
claims map[ClaimData]*extendedClaim // contractIndicies maps a contract index to it's extended claim.
// This is used to perform O(1) parent lookups.
contractIndicies map[int]*extendedClaim
// claims maps a claim entry to it's extended claim.
claims map[claimEntry]*extendedClaim
depth uint64 depth uint64
} }
// NewGameState returns a new game state. // NewGameState returns a new game state.
// The provided [Claim] is used as the root node. // The provided [Claim] is used as the root node.
func NewGameState(agreeWithProposedOutput bool, root Claim, depth uint64) *gameState { func NewGameState(agreeWithProposedOutput bool, root Claim, depth uint64) *gameState {
claims := make(map[ClaimData]*extendedClaim) claims := make(map[claimEntry]*extendedClaim)
claims[root.ClaimData] = &extendedClaim{ parents := make(map[int]*extendedClaim)
rootClaimEntry := makeClaimEntry(root)
claims[rootClaimEntry] = &extendedClaim{
self: root, self: root,
children: make([]ClaimData, 0), children: make([]claimEntry, 0),
} }
parents[root.ContractIndex] = claims[rootClaimEntry]
return &gameState{ return &gameState{
agreeWithProposedOutput: agreeWithProposedOutput, agreeWithProposedOutput: agreeWithProposedOutput,
root: root.ClaimData, root: rootClaimEntry,
claims: claims, claims: claims,
contractIndicies: parents,
depth: depth, depth: depth,
} }
} }
...@@ -87,29 +104,31 @@ func (g *gameState) PutAll(claims []Claim) error { ...@@ -87,29 +104,31 @@ func (g *gameState) PutAll(claims []Claim) error {
// Put adds a claim into the game state. // Put adds a claim into the game state.
func (g *gameState) Put(claim Claim) error { func (g *gameState) Put(claim Claim) error {
if claim.IsRoot() || g.IsDuplicate(claim.ClaimData) { if claim.IsRoot() || g.IsDuplicate(claim) {
return ErrClaimExists return ErrClaimExists
} }
parent, ok := g.claims[claim.Parent]
if !ok { parent := g.getParent(claim)
if parent == nil {
return errors.New("no parent claim") return errors.New("no parent claim")
} else {
parent.children = append(parent.children, claim.ClaimData)
} }
g.claims[claim.ClaimData] = &extendedClaim{ parent.children = append(parent.children, makeClaimEntry(claim))
claimWithExtension := &extendedClaim{
self: claim, self: claim,
children: make([]ClaimData, 0), children: make([]claimEntry, 0),
} }
g.claims[makeClaimEntry(claim)] = claimWithExtension
g.contractIndicies[claim.ContractIndex] = claimWithExtension
return nil return nil
} }
func (g *gameState) IsDuplicate(claim ClaimData) bool { func (g *gameState) IsDuplicate(claim Claim) bool {
_, ok := g.claims[claim] _, ok := g.claims[makeClaimEntry(claim)]
return ok return ok
} }
func (g *gameState) Claims() []Claim { func (g *gameState) Claims() []Claim {
queue := []ClaimData{g.root} queue := []claimEntry{g.root}
var out []Claim var out []Claim
for len(queue) > 0 { for len(queue) > 0 {
item := queue[0] item := queue[0]
...@@ -124,17 +143,31 @@ func (g *gameState) MaxDepth() uint64 { ...@@ -124,17 +143,31 @@ func (g *gameState) MaxDepth() uint64 {
return g.depth return g.depth
} }
func (g *gameState) getChildren(c ClaimData) []ClaimData { func (g *gameState) getChildren(c claimEntry) []claimEntry {
return g.claims[c].children return g.claims[c].children
} }
func (g *gameState) getParent(claim Claim) (Claim, error) { func (g *gameState) GetParent(claim Claim) (Claim, error) {
if claim.IsRoot() { parent := g.getParent(claim)
if parent == nil {
return Claim{}, ErrClaimNotFound return Claim{}, ErrClaimNotFound
} }
if parent, ok := g.claims[claim.Parent]; !ok {
return Claim{}, ErrClaimNotFound
} else {
return parent.self, nil return parent.self, nil
}
func (g *gameState) getParent(claim Claim) *extendedClaim {
if claim.IsRoot() {
return nil
}
if parent, ok := g.contractIndicies[claim.ParentContractIndex]; ok {
return parent
}
return nil
}
func makeClaimEntry(claim Claim) claimEntry {
return claimEntry{
ClaimData: claim.ClaimData,
ParentContractIndex: claim.ParentContractIndex,
} }
} }
...@@ -25,6 +25,8 @@ func createTestClaims() (Claim, Claim, Claim, Claim) { ...@@ -25,6 +25,8 @@ func createTestClaims() (Claim, Claim, Claim, Claim) {
Position: NewPosition(1, 0), Position: NewPosition(1, 0),
}, },
Parent: root.ClaimData, Parent: root.ClaimData,
ContractIndex: 1,
ParentContractIndex: 0,
} }
middle := Claim{ middle := Claim{
ClaimData: ClaimData{ ClaimData: ClaimData{
...@@ -32,6 +34,8 @@ func createTestClaims() (Claim, Claim, Claim, Claim) { ...@@ -32,6 +34,8 @@ func createTestClaims() (Claim, Claim, Claim, Claim) {
Position: NewPosition(2, 2), Position: NewPosition(2, 2),
}, },
Parent: top.ClaimData, Parent: top.ClaimData,
ContractIndex: 2,
ParentContractIndex: 1,
} }
bottom := Claim{ bottom := Claim{
...@@ -40,6 +44,8 @@ func createTestClaims() (Claim, Claim, Claim, Claim) { ...@@ -40,6 +44,8 @@ func createTestClaims() (Claim, Claim, Claim, Claim) {
Position: NewPosition(3, 4), Position: NewPosition(3, 4),
}, },
Parent: middle.ClaimData, Parent: middle.ClaimData,
ContractIndex: 3,
ParentContractIndex: 2,
} }
return root, top, middle, bottom return root, top, middle, bottom
...@@ -52,12 +58,12 @@ func TestIsDuplicate(t *testing.T) { ...@@ -52,12 +58,12 @@ func TestIsDuplicate(t *testing.T) {
require.NoError(t, g.Put(top)) require.NoError(t, g.Put(top))
// Root + Top should be duplicates // Root + Top should be duplicates
require.True(t, g.IsDuplicate(root.ClaimData)) require.True(t, g.IsDuplicate(root))
require.True(t, g.IsDuplicate(top.ClaimData)) require.True(t, g.IsDuplicate(top))
// Middle + Bottom should not be a duplicate // Middle + Bottom should not be a duplicate
require.False(t, g.IsDuplicate(middle.ClaimData)) require.False(t, g.IsDuplicate(middle))
require.False(t, g.IsDuplicate(bottom.ClaimData)) require.False(t, g.IsDuplicate(bottom))
} }
// TestGame_Put_RootAlreadyExists tests the [Game.Put] method using a [gameState] // TestGame_Put_RootAlreadyExists tests the [Game.Put] method using a [gameState]
...@@ -104,20 +110,20 @@ func TestGame_PutAll_ParentsAndChildren(t *testing.T) { ...@@ -104,20 +110,20 @@ func TestGame_PutAll_ParentsAndChildren(t *testing.T) {
g := NewGameState(false, root, testMaxDepth) g := NewGameState(false, root, testMaxDepth)
// We should not be able to get the parent of the root claim. // We should not be able to get the parent of the root claim.
parent, err := g.getParent(root) parent, err := g.GetParent(root)
require.ErrorIs(t, err, ErrClaimNotFound) require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{}) require.Equal(t, parent, Claim{})
// Put the rest of the claims in the state. // Put the rest of the claims in the state.
err = g.PutAll([]Claim{top, middle, bottom}) err = g.PutAll([]Claim{top, middle, bottom})
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(top) parent, err = g.GetParent(top)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, root) require.Equal(t, parent, root)
parent, err = g.getParent(middle) parent, err = g.GetParent(middle)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, top) require.Equal(t, parent, top)
parent, err = g.getParent(bottom) parent, err = g.GetParent(bottom)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, middle) require.Equal(t, parent, middle)
} }
...@@ -145,28 +151,28 @@ func TestGame_Put_ParentsAndChildren(t *testing.T) { ...@@ -145,28 +151,28 @@ func TestGame_Put_ParentsAndChildren(t *testing.T) {
g := NewGameState(false, root, testMaxDepth) g := NewGameState(false, root, testMaxDepth)
// We should not be able to get the parent of the root claim. // We should not be able to get the parent of the root claim.
parent, err := g.getParent(root) parent, err := g.GetParent(root)
require.ErrorIs(t, err, ErrClaimNotFound) require.ErrorIs(t, err, ErrClaimNotFound)
require.Equal(t, parent, Claim{}) require.Equal(t, parent, Claim{})
// Put + Check Top // Put + Check Top
err = g.Put(top) err = g.Put(top)
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(top) parent, err = g.GetParent(top)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, root) require.Equal(t, parent, root)
// Put + Check Top Middle // Put + Check Top Middle
err = g.Put(middle) err = g.Put(middle)
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(middle) parent, err = g.GetParent(middle)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, top) require.Equal(t, parent, top)
// Put + Check Top Bottom // Put + Check Top Bottom
err = g.Put(bottom) err = g.Put(bottom)
require.NoError(t, err) require.NoError(t, err)
parent, err = g.getParent(bottom) parent, err = g.GetParent(bottom)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, parent, middle) require.Equal(t, parent, middle)
} }
...@@ -194,27 +200,3 @@ func TestGame_ClaimPairs(t *testing.T) { ...@@ -194,27 +200,3 @@ func TestGame_ClaimPairs(t *testing.T) {
claims := g.Claims() claims := g.Claims()
require.ElementsMatch(t, expected, claims) require.ElementsMatch(t, expected, claims)
} }
func TestAgreeWithClaimLevelDisagreeWithOutput(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(false, root, testMaxDepth)
require.NoError(t, g.PutAll([]Claim{top, middle, bottom}))
require.True(t, g.AgreeWithClaimLevel(root))
require.False(t, g.AgreeWithClaimLevel(top))
require.True(t, g.AgreeWithClaimLevel(middle))
require.False(t, g.AgreeWithClaimLevel(bottom))
}
func TestAgreeWithClaimLevelAgreeWithOutput(t *testing.T) {
// Setup the game state.
root, top, middle, bottom := createTestClaims()
g := NewGameState(true, root, testMaxDepth)
require.NoError(t, g.PutAll([]Claim{top, middle, bottom}))
require.False(t, g.AgreeWithClaimLevel(root))
require.True(t, g.AgreeWithClaimLevel(top))
require.False(t, g.AgreeWithClaimLevel(middle))
require.True(t, g.AgreeWithClaimLevel(bottom))
}
...@@ -109,9 +109,14 @@ func (c *coordinator) createJob(game common.Address) (*job, error) { ...@@ -109,9 +109,14 @@ func (c *coordinator) createJob(game common.Address) (*job, error) {
return nil, fmt.Errorf("failed to create game player: %w", err) return nil, fmt.Errorf("failed to create game player: %w", err)
} }
state.player = player state.player = player
state.status = player.Status()
} }
state.inflight = true state.inflight = true
return &job{addr: game, player: state.player}, nil if state.status != types.GameStatusInProgress {
c.logger.Debug("Not rescheduling resolved game", "game", game, "status", state.status)
return nil, nil
}
return &job{addr: game, player: state.player, status: state.status}, nil
} }
func (c *coordinator) enqueueJob(ctx context.Context, j job) error { func (c *coordinator) enqueueJob(ctx context.Context, j job) error {
......
...@@ -150,7 +150,10 @@ func TestDeleteDataForResolvedGames(t *testing.T) { ...@@ -150,7 +150,10 @@ func TestDeleteDataForResolvedGames(t *testing.T) {
gameAddrs := []common.Address{gameAddr1, gameAddr2, gameAddr3} gameAddrs := []common.Address{gameAddr1, gameAddr2, gameAddr3}
require.NoError(t, c.schedule(ctx, gameAddrs)) require.NoError(t, c.schedule(ctx, gameAddrs))
require.Len(t, workQueue, len(gameAddrs), "should schedule all games") // The work queue should only contain jobs for games 1 and 2
// A resolved game should not be scheduled for an update.
// This makes the inflight game metric more robust.
require.Len(t, workQueue, 2, "should schedule all games")
// Game 1 progresses and is still in progress // Game 1 progresses and is still in progress
// Game 2 progresses and is now resolved // Game 2 progresses and is now resolved
...@@ -249,6 +252,10 @@ func (g *stubGame) ProgressGame(_ context.Context) types.GameStatus { ...@@ -249,6 +252,10 @@ func (g *stubGame) ProgressGame(_ context.Context) types.GameStatus {
return g.status return g.status
} }
func (g *stubGame) Status() types.GameStatus {
return g.status
}
type createdGames struct { type createdGames struct {
t *testing.T t *testing.T
createCompleted common.Address createCompleted common.Address
......
...@@ -15,11 +15,16 @@ type SchedulerMetricer interface { ...@@ -15,11 +15,16 @@ type SchedulerMetricer interface {
RecordGamesStatus(inProgress, defenderWon, challengerWon int) RecordGamesStatus(inProgress, defenderWon, challengerWon int)
RecordGameUpdateScheduled() RecordGameUpdateScheduled()
RecordGameUpdateCompleted() RecordGameUpdateCompleted()
IncActiveExecutors()
DecActiveExecutors()
IncIdleExecutors()
DecIdleExecutors()
} }
type Scheduler struct { type Scheduler struct {
logger log.Logger logger log.Logger
coordinator *coordinator coordinator *coordinator
m SchedulerMetricer
maxConcurrency uint maxConcurrency uint
scheduleQueue chan []common.Address scheduleQueue chan []common.Address
jobQueue chan job jobQueue chan job
...@@ -40,6 +45,7 @@ func NewScheduler(logger log.Logger, m SchedulerMetricer, disk DiskManager, maxC ...@@ -40,6 +45,7 @@ func NewScheduler(logger log.Logger, m SchedulerMetricer, disk DiskManager, maxC
return &Scheduler{ return &Scheduler{
logger: logger, logger: logger,
m: m,
coordinator: newCoordinator(logger, m, jobQueue, resultQueue, createPlayer, disk), coordinator: newCoordinator(logger, m, jobQueue, resultQueue, createPlayer, disk),
maxConcurrency: maxConcurrency, maxConcurrency: maxConcurrency,
scheduleQueue: scheduleQueue, scheduleQueue: scheduleQueue,
...@@ -48,13 +54,24 @@ func NewScheduler(logger log.Logger, m SchedulerMetricer, disk DiskManager, maxC ...@@ -48,13 +54,24 @@ func NewScheduler(logger log.Logger, m SchedulerMetricer, disk DiskManager, maxC
} }
} }
func (s *Scheduler) ThreadActive() {
s.m.IncActiveExecutors()
s.m.DecIdleExecutors()
}
func (s *Scheduler) ThreadIdle() {
s.m.IncIdleExecutors()
s.m.DecActiveExecutors()
}
func (s *Scheduler) Start(ctx context.Context) { func (s *Scheduler) Start(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
s.cancel = cancel s.cancel = cancel
for i := uint(0); i < s.maxConcurrency; i++ { for i := uint(0); i < s.maxConcurrency; i++ {
s.m.IncIdleExecutors()
s.wg.Add(1) s.wg.Add(1)
go progressGames(ctx, s.jobQueue, s.resultQueue, &s.wg) go progressGames(ctx, s.jobQueue, s.resultQueue, &s.wg, s.ThreadActive, s.ThreadIdle)
} }
s.wg.Add(1) s.wg.Add(1)
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
type GamePlayer interface { type GamePlayer interface {
ProgressGame(ctx context.Context) types.GameStatus ProgressGame(ctx context.Context) types.GameStatus
Status() types.GameStatus
} }
type DiskManager interface { type DiskManager interface {
......
...@@ -8,15 +8,17 @@ import ( ...@@ -8,15 +8,17 @@ import (
// progressGames accepts jobs from in channel, calls ProgressGame on the job.player and returns the job // progressGames accepts jobs from in channel, calls ProgressGame on the job.player and returns the job
// with updated job.resolved via the out channel. // with updated job.resolved via the out channel.
// The loop exits when the ctx is done. wg.Done() is called when the function returns. // The loop exits when the ctx is done. wg.Done() is called when the function returns.
func progressGames(ctx context.Context, in <-chan job, out chan<- job, wg *sync.WaitGroup) { func progressGames(ctx context.Context, in <-chan job, out chan<- job, wg *sync.WaitGroup, threadActive, threadIdle func()) {
defer wg.Done() defer wg.Done()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case j := <-in: case j := <-in:
threadActive()
j.status = j.player.ProgressGame(ctx) j.status = j.player.ProgressGame(ctx)
out <- j out <- j
threadIdle()
} }
} }
} }
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/ethereum-optimism/optimism/op-challenger/game/types" "github.com/ethereum-optimism/optimism/op-challenger/game/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -15,18 +16,32 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) { ...@@ -15,18 +16,32 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) {
in := make(chan job, 2) in := make(chan job, 2)
out := make(chan job, 2) out := make(chan job, 2)
ms := &metricSink{}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1) wg.Add(1)
go progressGames(ctx, in, out, &wg) go progressGames(ctx, in, out, &wg, ms.ThreadActive, ms.ThreadIdle)
in <- job{ in <- job{
player: &stubPlayer{status: types.GameStatusInProgress}, player: &stubPlayer{status: types.GameStatusInProgress},
} }
waitErr := wait.For(context.Background(), 100*time.Millisecond, func() (bool, error) {
return ms.activeCalls >= 1, nil
})
require.NoError(t, waitErr)
require.Equal(t, ms.activeCalls, 1)
require.Equal(t, ms.idleCalls, 1)
in <- job{ in <- job{
player: &stubPlayer{status: types.GameStatusDefenderWon}, player: &stubPlayer{status: types.GameStatusDefenderWon},
} }
waitErr = wait.For(context.Background(), 100*time.Millisecond, func() (bool, error) {
return ms.activeCalls >= 2, nil
})
require.NoError(t, waitErr)
require.Equal(t, ms.activeCalls, 2)
require.Equal(t, ms.idleCalls, 2)
result1 := readWithTimeout(t, out) result1 := readWithTimeout(t, out)
result2 := readWithTimeout(t, out) result2 := readWithTimeout(t, out)
...@@ -39,6 +54,19 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) { ...@@ -39,6 +54,19 @@ func TestWorkerShouldProcessJobsUntilContextDone(t *testing.T) {
wg.Wait() wg.Wait()
} }
type metricSink struct {
activeCalls int
idleCalls int
}
func (m *metricSink) ThreadActive() {
m.activeCalls++
}
func (m *metricSink) ThreadIdle() {
m.idleCalls++
}
type stubPlayer struct { type stubPlayer struct {
status types.GameStatus status types.GameStatus
} }
...@@ -47,6 +75,10 @@ func (s *stubPlayer) ProgressGame(ctx context.Context) types.GameStatus { ...@@ -47,6 +75,10 @@ func (s *stubPlayer) ProgressGame(ctx context.Context) types.GameStatus {
return s.status return s.status
} }
func (s *stubPlayer) Status() types.GameStatus {
return s.status
}
func readWithTimeout[T any](t *testing.T, ch <-chan T) T { func readWithTimeout[T any](t *testing.T, ch <-chan T) T {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
......
...@@ -29,6 +29,11 @@ type Metricer interface { ...@@ -29,6 +29,11 @@ type Metricer interface {
RecordGameUpdateScheduled() RecordGameUpdateScheduled()
RecordGameUpdateCompleted() RecordGameUpdateCompleted()
IncActiveExecutors()
DecActiveExecutors()
IncIdleExecutors()
DecIdleExecutors()
} }
type Metrics struct { type Metrics struct {
...@@ -41,6 +46,8 @@ type Metrics struct { ...@@ -41,6 +46,8 @@ type Metrics struct {
info prometheus.GaugeVec info prometheus.GaugeVec
up prometheus.Gauge up prometheus.Gauge
executors prometheus.GaugeVec
moves prometheus.Counter moves prometheus.Counter
steps prometheus.Counter steps prometheus.Counter
...@@ -75,6 +82,13 @@ func NewMetrics() *Metrics { ...@@ -75,6 +82,13 @@ func NewMetrics() *Metrics {
Name: "up", Name: "up",
Help: "1 if the op-challenger has finished starting up", Help: "1 if the op-challenger has finished starting up",
}), }),
executors: *factory.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "executors",
Help: "Number of active and idle executors",
}, []string{
"status",
}),
moves: factory.NewCounter(prometheus.CounterOpts{ moves: factory.NewCounter(prometheus.CounterOpts{
Namespace: Namespace, Namespace: Namespace,
Name: "moves", Name: "moves",
...@@ -149,6 +163,22 @@ func (m *Metrics) RecordCannonExecutionTime(t float64) { ...@@ -149,6 +163,22 @@ func (m *Metrics) RecordCannonExecutionTime(t float64) {
m.cannonExecutionTime.Observe(t) m.cannonExecutionTime.Observe(t)
} }
func (m *Metrics) IncActiveExecutors() {
m.executors.WithLabelValues("active").Inc()
}
func (m *Metrics) DecActiveExecutors() {
m.executors.WithLabelValues("active").Dec()
}
func (m *Metrics) IncIdleExecutors() {
m.executors.WithLabelValues("idle").Inc()
}
func (m *Metrics) DecIdleExecutors() {
m.executors.WithLabelValues("idle").Dec()
}
func (m *Metrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int) { func (m *Metrics) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {
m.trackedGames.WithLabelValues("in_progress").Set(float64(inProgress)) m.trackedGames.WithLabelValues("in_progress").Set(float64(inProgress))
m.trackedGames.WithLabelValues("defender_won").Set(float64(defenderWon)) m.trackedGames.WithLabelValues("defender_won").Set(float64(defenderWon))
......
...@@ -22,3 +22,8 @@ func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon ...@@ -22,3 +22,8 @@ func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon
func (*NoopMetricsImpl) RecordGameUpdateScheduled() {} func (*NoopMetricsImpl) RecordGameUpdateScheduled() {}
func (*NoopMetricsImpl) RecordGameUpdateCompleted() {} func (*NoopMetricsImpl) RecordGameUpdateCompleted() {}
func (*NoopMetricsImpl) IncActiveExecutors() {}
func (*NoopMetricsImpl) DecActiveExecutors() {}
func (*NoopMetricsImpl) IncIdleExecutors() {}
func (*NoopMetricsImpl) DecIdleExecutors() {}
...@@ -68,6 +68,12 @@ func WithAlphabet(alphabet string) Option { ...@@ -68,6 +68,12 @@ func WithAlphabet(alphabet string) Option {
} }
} }
func WithPollInterval(pollInterval time.Duration) Option {
return func(c *config.Config) {
c.PollInterval = pollInterval
}
}
func WithCannon( func WithCannon(
t *testing.T, t *testing.T,
rollupCfg *rollup.Config, rollupCfg *rollup.Config,
...@@ -98,7 +104,7 @@ func WithCannon( ...@@ -98,7 +104,7 @@ func WithCannon(
} }
func NewChallenger(t *testing.T, ctx context.Context, l1Endpoint string, name string, options ...Option) *Helper { func NewChallenger(t *testing.T, ctx context.Context, l1Endpoint string, name string, options ...Option) *Helper {
log := testlog.Logger(t, log.LvlInfo).New("role", name) log := testlog.Logger(t, log.LvlDebug).New("role", name)
log.Info("Creating challenger", "l1", l1Endpoint) log.Info("Creating challenger", "l1", l1Endpoint)
cfg := NewChallengerConfig(t, l1Endpoint, options...) cfg := NewChallengerConfig(t, l1Endpoint, options...)
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/ethereum-optimism/optimism/op-challenger/config" "github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -33,3 +34,12 @@ func (g *AlphabetGameHelper) StartChallenger(ctx context.Context, l1Endpoint str ...@@ -33,3 +34,12 @@ func (g *AlphabetGameHelper) StartChallenger(ctx context.Context, l1Endpoint str
}) })
return c return c
} }
func (g *AlphabetGameHelper) CreateHonestActor(ctx context.Context, alphabetTrace string, depth uint64) *HonestHelper {
return &HonestHelper{
t: g.t,
require: g.require,
game: &g.FaultGameHelper,
correctTrace: alphabet.NewTraceProvider(alphabetTrace, depth),
}
}
...@@ -2,16 +2,19 @@ package disputegame ...@@ -2,16 +2,19 @@ package disputegame
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -130,6 +133,10 @@ func (g *FaultGameHelper) getClaim(ctx context.Context, claimIdx int64) Contract ...@@ -130,6 +133,10 @@ func (g *FaultGameHelper) getClaim(ctx context.Context, claimIdx int64) Contract
return claimData return claimData
} }
func (g *FaultGameHelper) GetClaimUnsafe(ctx context.Context, claimIdx int64) ContractClaim {
return g.getClaim(ctx, claimIdx)
}
func (g *FaultGameHelper) WaitForClaimAtDepth(ctx context.Context, depth int) { func (g *FaultGameHelper) WaitForClaimAtDepth(ctx context.Context, depth int) {
g.waitForClaim( g.waitForClaim(
ctx, ctx,
...@@ -169,6 +176,12 @@ func (g *FaultGameHelper) Resolve(ctx context.Context) { ...@@ -169,6 +176,12 @@ func (g *FaultGameHelper) Resolve(ctx context.Context) {
g.require.NoError(err) g.require.NoError(err)
} }
func (g *FaultGameHelper) Status(ctx context.Context) Status {
status, err := g.game.Status(&bind.CallOpts{Context: ctx})
g.require.NoError(err)
return Status(status)
}
func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status) { func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status) {
g.t.Logf("Waiting for game %v to have status %v", g.addr, expected) g.t.Logf("Waiting for game %v to have status %v", g.addr, expected)
timedCtx, cancel := context.WithTimeout(ctx, time.Minute) timedCtx, cancel := context.WithTimeout(ctx, time.Minute)
...@@ -186,6 +199,46 @@ func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status ...@@ -186,6 +199,46 @@ func (g *FaultGameHelper) WaitForGameStatus(ctx context.Context, expected Status
g.require.NoErrorf(err, "wait for game status. Game state: \n%v", g.gameData(ctx)) g.require.NoErrorf(err, "wait for game status. Game state: \n%v", g.gameData(ctx))
} }
func (g *FaultGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlocks int, untilGameEnds bool) {
g.t.Logf("Waiting for game %v to have no activity for %v blocks", g.addr, numInactiveBlocks)
headCh := make(chan *gethtypes.Header, 100)
headSub, err := g.client.SubscribeNewHead(ctx, headCh)
g.require.NoError(err)
defer headSub.Unsubscribe()
var lastActiveBlock uint64
for {
if untilGameEnds && g.Status(ctx) != StatusInProgress {
break
}
select {
case head := <-headCh:
if lastActiveBlock == 0 {
lastActiveBlock = head.Number.Uint64()
continue
} else if lastActiveBlock+uint64(numInactiveBlocks) < head.Number.Uint64() {
return
}
block, err := g.client.BlockByNumber(ctx, head.Number)
g.require.NoError(err)
numActions := 0
for _, tx := range block.Transactions() {
if tx.To().Hex() == g.addr.Hex() {
numActions++
}
}
if numActions != 0 {
g.t.Logf("Game %v has %v actions in block %d. Resetting inactivity timeout", g.addr, numActions, block.NumberU64())
lastActiveBlock = head.Number.Uint64()
}
case err := <-headSub.Err():
g.require.NoError(err)
case <-ctx.Done():
g.require.Fail("Context canceled", ctx.Err())
}
}
}
// Mover is a function that either attacks or defends the claim at parentClaimIdx // Mover is a function that either attacks or defends the claim at parentClaimIdx
type Mover func(parentClaimIdx int64) type Mover func(parentClaimIdx int64)
...@@ -239,6 +292,21 @@ func (g *FaultGameHelper) ChallengeRootClaim(ctx context.Context, performMove Mo ...@@ -239,6 +292,21 @@ func (g *FaultGameHelper) ChallengeRootClaim(ctx context.Context, performMove Mo
attemptStep(maxDepth) attemptStep(maxDepth)
} }
func (g *FaultGameHelper) WaitForNewClaim(ctx context.Context, checkPoint int64) (int64, error) {
timedCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
var newClaimLen int64
err := wait.For(timedCtx, time.Second, func() (bool, error) {
actual, err := g.game.ClaimDataLen(&bind.CallOpts{Context: ctx})
if err != nil {
return false, err
}
newClaimLen = actual.Int64()
return actual.Cmp(big.NewInt(checkPoint)) > 0, nil
})
return newClaimLen, err
}
func (g *FaultGameHelper) Attack(ctx context.Context, claimIdx int64, claim common.Hash) { func (g *FaultGameHelper) Attack(ctx context.Context, claimIdx int64, claim common.Hash) {
tx, err := g.game.Attack(g.opts, big.NewInt(claimIdx), claim) tx, err := g.game.Attack(g.opts, big.NewInt(claimIdx), claim)
g.require.NoError(err, "Attack transaction did not send") g.require.NoError(err, "Attack transaction did not send")
...@@ -266,6 +334,33 @@ func (g *FaultGameHelper) StepFails(claimIdx int64, isAttack bool, stateData []b ...@@ -266,6 +334,33 @@ func (g *FaultGameHelper) StepFails(claimIdx int64, isAttack bool, stateData []b
g.require.Equal("0xfb4e40dd", errData.ErrorData(), "Revert reason should be abi encoded ValidStep()") g.require.Equal("0xfb4e40dd", errData.ErrorData(), "Revert reason should be abi encoded ValidStep()")
} }
// ResolveClaim resolves a single subgame
func (g *FaultGameHelper) ResolveClaim(ctx context.Context, claimIdx int64) {
tx, err := g.game.ResolveClaim(g.opts, big.NewInt(claimIdx))
g.require.NoError(err, "ResolveClaim transaction did not send")
_, err = wait.ForReceiptOK(ctx, g.client, tx.Hash())
g.require.NoError(err, "ResolveClaim transaction was not OK")
}
// ResolveAllClaims resolves all subgames
// This function does not resolve the game. That's the responsibility of challengers
func (g *FaultGameHelper) ResolveAllClaims(ctx context.Context) {
loader := fault.NewLoader(g.game)
claims, err := loader.FetchClaims(ctx)
g.require.NoError(err, "Failed to fetch claims")
subgames := make(map[int]bool)
for i := len(claims) - 1; i > 0; i-- {
subgames[claims[i].ParentContractIndex] = true
// Subgames containing only one node are implicitly resolved
// i.e. uncountered and claims at MAX_DEPTH
if !subgames[i] {
continue
}
g.ResolveClaim(ctx, int64(i))
}
g.ResolveClaim(ctx, 0)
}
func (g *FaultGameHelper) gameData(ctx context.Context) string { func (g *FaultGameHelper) gameData(ctx context.Context) string {
opts := &bind.CallOpts{Context: ctx} opts := &bind.CallOpts{Context: ctx}
maxDepth := int(g.MaxDepth(ctx)) maxDepth := int(g.MaxDepth(ctx))
...@@ -277,8 +372,8 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string { ...@@ -277,8 +372,8 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string {
g.require.NoErrorf(err, "Fetch claim %v", i) g.require.NoErrorf(err, "Fetch claim %v", i)
pos := types.NewPositionFromGIndex(claim.Position.Uint64()) pos := types.NewPositionFromGIndex(claim.Position.Uint64())
info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v\n", info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, Value: %v, Countered: %v, ParentIndex: %v\n",
i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.Countered) i, claim.Position.Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), common.Hash(claim.Claim).Hex(), claim.Countered, claim.ParentIndex)
} }
status, err := g.game.Status(opts) status, err := g.game.Status(opts)
g.require.NoError(err, "Load game status") g.require.NoError(err, "Load game status")
...@@ -288,3 +383,106 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string { ...@@ -288,3 +383,106 @@ func (g *FaultGameHelper) gameData(ctx context.Context) string {
func (g *FaultGameHelper) LogGameData(ctx context.Context) { func (g *FaultGameHelper) LogGameData(ctx context.Context) {
g.t.Log(g.gameData(ctx)) g.t.Log(g.gameData(ctx))
} }
type dishonestClaim struct {
ParentIndex int64
IsAttack bool
Valid bool
}
type DishonestHelper struct {
*FaultGameHelper
*HonestHelper
claims map[dishonestClaim]bool
defender bool
}
func NewDishonestHelper(g *FaultGameHelper, correctTrace *HonestHelper, defender bool) *DishonestHelper {
return &DishonestHelper{g, correctTrace, make(map[dishonestClaim]bool), defender}
}
func (t *DishonestHelper) Attack(ctx context.Context, claimIndex int64) {
c := dishonestClaim{claimIndex, true, false}
if t.claims[c] {
return
}
t.claims[c] = true
t.FaultGameHelper.Attack(ctx, claimIndex, common.Hash{byte(claimIndex)})
}
func (t *DishonestHelper) Defend(ctx context.Context, claimIndex int64) {
c := dishonestClaim{claimIndex, false, false}
if t.claims[c] {
return
}
t.claims[c] = true
t.FaultGameHelper.Defend(ctx, claimIndex, common.Hash{byte(claimIndex)})
}
func (t *DishonestHelper) AttackCorrect(ctx context.Context, claimIndex int64) {
c := dishonestClaim{claimIndex, true, true}
if t.claims[c] {
return
}
t.claims[c] = true
t.HonestHelper.Attack(ctx, claimIndex)
}
func (t *DishonestHelper) DefendCorrect(ctx context.Context, claimIndex int64) {
c := dishonestClaim{claimIndex, false, true}
if t.claims[c] {
return
}
t.claims[c] = true
t.HonestHelper.Defend(ctx, claimIndex)
}
// ExhaustDishonestClaims makes all possible significant moves (mod honest challenger's) in a game.
// It is very inefficient and should NOT be used on games with large depths
func (d *DishonestHelper) ExhaustDishonestClaims(ctx context.Context) {
depth := d.MaxDepth(ctx)
move := func(claimIndex int64, claimData ContractClaim) {
// dishonest level, valid attack
// dishonest level, invalid attack
// dishonest level, valid defense
// dishonest level, invalid defense
// honest level, invalid attack
// honest level, invalid defense
pos := types.NewPositionFromGIndex(claimData.Position.Uint64())
if int64(pos.Depth()) == depth {
return
}
d.LogGameData(ctx)
d.FaultGameHelper.t.Logf("Dishonest moves against claimIndex %d", claimIndex)
agreeWithLevel := d.defender == (pos.Depth()%2 == 0)
if !agreeWithLevel {
d.AttackCorrect(ctx, claimIndex)
if claimIndex != 0 {
d.DefendCorrect(ctx, claimIndex)
}
}
d.Attack(ctx, claimIndex)
if claimIndex != 0 {
d.Defend(ctx, claimIndex)
}
}
var numClaimsSeen int64
for {
newCount, err := d.WaitForNewClaim(ctx, numClaimsSeen)
if errors.Is(err, context.DeadlineExceeded) {
// we assume that the honest challenger has stopped responding
// There's nothing to respond to.
break
}
d.FaultGameHelper.require.NoError(err)
for i := numClaimsSeen; i < newCount; i++ {
claimData := d.getClaim(ctx, numClaimsSeen)
move(numClaimsSeen, claimData)
numClaimsSeen++
}
}
}
...@@ -3,7 +3,9 @@ package op_e2e ...@@ -3,7 +3,9 @@ package op_e2e
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame"
l2oo2 "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/l2oo" l2oo2 "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/l2oo"
...@@ -62,8 +64,12 @@ func TestMultipleCannonGames(t *testing.T) { ...@@ -62,8 +64,12 @@ func TestMultipleCannonGames(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(gameDuration) sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game1.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game1.WaitForInactivity(ctx, 10, true)
game2.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game2.WaitForInactivity(ctx, 10, true)
game1.LogGameData(ctx)
game2.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game1.Status(ctx))
require.EqualValues(t, disputegame.StatusChallengerWins, game2.Status(ctx))
// Check that the game directories are removed // Check that the game directories are removed
challenger.WaitForGameDataDeletion(ctx, game1, game2) challenger.WaitForGameDataDeletion(ctx, game1, game2)
...@@ -168,11 +174,72 @@ func TestChallengerCompleteDisputeGame(t *testing.T) { ...@@ -168,11 +174,72 @@ func TestChallengerCompleteDisputeGame(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(gameDuration) sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, test.expectedResult) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx)
require.EqualValues(t, test.expectedResult, game.Status(ctx))
}) })
} }
} }
func TestChallengerCompleteExhaustiveDisputeGame(t *testing.T) {
InitParallel(t)
testCase := func(t *testing.T, isRootCorrect bool) {
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys.cfg.L1Deployments, l1Client)
rootClaimedAlphabet := disputegame.CorrectAlphabet
if !isRootCorrect {
rootClaimedAlphabet = "abcdexyz"
}
game := disputeGameFactory.StartAlphabetGame(ctx, rootClaimedAlphabet)
require.NotNil(t, game)
gameDuration := game.GameDuration(ctx)
// Start honest challenger
game.StartChallenger(ctx, sys.NodeEndpoint("l1"), "Challenger",
challenger.WithAgreeProposedOutput(!isRootCorrect),
challenger.WithAlphabet(disputegame.CorrectAlphabet),
challenger.WithPrivKey(sys.cfg.Secrets.Alice),
// Ensures the challenger responds to all claims before test timeout
challenger.WithPollInterval(time.Millisecond*400),
)
// Start dishonest challenger
correctTrace := game.CreateHonestActor(ctx, disputegame.CorrectAlphabet, 4)
dishonestHelper := disputegame.NewDishonestHelper(&game.FaultGameHelper, correctTrace, !isRootCorrect)
dishonestHelper.ExhaustDishonestClaims(ctx)
// Wait until we've reached max depth before checking for inactivity
game.WaitForClaimAtDepth(ctx, int(game.MaxDepth(ctx)))
// Wait for 4 blocks of no challenger responses. The challenger may still be stepping on invalid claims at max depth
game.WaitForInactivity(ctx, 4, false)
sys.TimeTravelClock.AdvanceTime(gameDuration)
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
expectedStatus := disputegame.StatusChallengerWins
if isRootCorrect {
expectedStatus = disputegame.StatusDefenderWins
}
game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx)
require.EqualValues(t, expectedStatus, game.Status(ctx))
}
t.Run("RootCorrect", func(t *testing.T) {
InitParallel(t)
testCase(t, true)
})
t.Run("RootIncorrect", func(t *testing.T) {
InitParallel(t)
testCase(t, false)
})
}
func TestCannonDisputeGame(t *testing.T) { func TestCannonDisputeGame(t *testing.T) {
InitParallel(t) InitParallel(t)
...@@ -217,8 +284,9 @@ func TestCannonDisputeGame(t *testing.T) { ...@@ -217,8 +284,9 @@ func TestCannonDisputeGame(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
}) })
} }
} }
...@@ -260,8 +328,9 @@ func TestCannonDefendStep(t *testing.T) { ...@@ -260,8 +328,9 @@ func TestCannonDefendStep(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
} }
func TestCannonProposedOutputRootInvalid(t *testing.T) { func TestCannonProposedOutputRootInvalid(t *testing.T) {
...@@ -335,14 +404,14 @@ func TestCannonProposedOutputRootInvalid(t *testing.T) { ...@@ -335,14 +404,14 @@ func TestCannonProposedOutputRootInvalid(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusDefenderWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusDefenderWins, game.Status(ctx))
}) })
} }
} }
func TestCannonPoisonedPostState(t *testing.T) { func TestCannonPoisonedPostState(t *testing.T) {
t.Skip("Known failure case")
InitParallel(t) InitParallel(t)
ctx := context.Background() ctx := context.Background()
...@@ -365,9 +434,12 @@ func TestCannonPoisonedPostState(t *testing.T) { ...@@ -365,9 +434,12 @@ func TestCannonPoisonedPostState(t *testing.T) {
// Honest defense at "dishonest" level // Honest defense at "dishonest" level
correctTrace.Defend(ctx, 1) correctTrace.Defend(ctx, 1)
// Dishonest attack at "honest" level - honest move would be to defend // Dishonest attack at "honest" level - honest move would be to ignore
game.Attack(ctx, 2, common.Hash{0x03, 0xaa}) game.Attack(ctx, 2, common.Hash{0x03, 0xaa})
// Honest attack at "dishonest" level - honest move would be to ignore
correctTrace.Attack(ctx, 3)
// Start the honest challenger // Start the honest challenger
game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Honest", game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "Honest",
// Agree with the proposed output, so disagree with the root claim // Agree with the proposed output, so disagree with the root claim
...@@ -376,29 +448,40 @@ func TestCannonPoisonedPostState(t *testing.T) { ...@@ -376,29 +448,40 @@ func TestCannonPoisonedPostState(t *testing.T) {
) )
// Start dishonest challenger that posts correct claims // Start dishonest challenger that posts correct claims
game.StartChallenger(ctx, sys.RollupConfig, sys.L2GenesisCfg, l1Endpoint, l2Endpoint, "DishonestCorrect", // It participates in the subgame root the honest claim index 4
// Disagree with the proposed output, so agree with the root claim func() {
challenger.WithAgreeProposedOutput(false), claimCount := int64(5)
challenger.WithPrivKey(sys.cfg.Secrets.Mallory),
)
// Give the challengers time to progress down the full game depth
depth := game.MaxDepth(ctx) depth := game.MaxDepth(ctx)
for i := 3; i <= int(depth); i++ { for {
game.WaitForClaimAtDepth(ctx, i)
game.LogGameData(ctx) game.LogGameData(ctx)
claimCount++
// Wait for the challenger to counter
game.WaitForClaimCount(ctx, claimCount)
// Respond with our own move
correctTrace.Defend(ctx, claimCount-1)
claimCount++
game.WaitForClaimCount(ctx, claimCount)
// Defender moves last. If we're at max depth, then we're done
dishonestClaim := game.GetClaimUnsafe(ctx, claimCount-1)
pos := types.NewPositionFromGIndex(dishonestClaim.Position.Uint64())
if int64(pos.Depth()) == depth {
break
}
} }
}()
// Wait for all the leaf nodes to be countered // Wait for the challenger to drive the subgame at 4 to the leaf node, which should be countered
// Wait for the challengers to drive the game down to the leaf node which should be countered game.WaitForClaimAtMaxDepth(ctx, true)
game.WaitForAllClaimsCountered(ctx)
// Time travel past when the game will be resolvable. // Time travel past when the game will be resolvable.
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
} }
// setupDisputeGameForInvalidOutputRoot sets up an L2 chain with at least one valid output root followed by an invalid output root. // setupDisputeGameForInvalidOutputRoot sets up an L2 chain with at least one valid output root followed by an invalid output root.
...@@ -470,8 +553,9 @@ func TestCannonChallengeWithCorrectRoot(t *testing.T) { ...@@ -470,8 +553,9 @@ func TestCannonChallengeWithCorrectRoot(t *testing.T) {
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx)) sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client)) require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForGameStatus(ctx, disputegame.StatusChallengerWins) game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx) game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
} }
func startFaultDisputeSystem(t *testing.T) (*System, *ethclient.Client) { func startFaultDisputeSystem(t *testing.T) (*System, *ethclient.Client) {
......
...@@ -306,7 +306,7 @@ var optionalFlags = []cli.Flag{ ...@@ -306,7 +306,7 @@ var optionalFlags = []cli.Flag{
var Flags []cli.Flag var Flags []cli.Flag
func init() { func init() {
optionalFlags = append(optionalFlags, p2pFlags...) optionalFlags = append(optionalFlags, P2pFlags...)
optionalFlags = append(optionalFlags, oplog.CLIFlags(EnvVarPrefix)...) optionalFlags = append(optionalFlags, oplog.CLIFlags(EnvVarPrefix)...)
Flags = append(requiredFlags, optionalFlags...) Flags = append(requiredFlags, optionalFlags...)
} }
......
...@@ -308,7 +308,7 @@ var ( ...@@ -308,7 +308,7 @@ var (
// None of these flags are strictly required. // None of these flags are strictly required.
// Some are hidden if they are too technical, or not recommended. // Some are hidden if they are too technical, or not recommended.
var p2pFlags = []cli.Flag{ var P2pFlags = []cli.Flag{
DisableP2P, DisableP2P,
NoDiscovery, NoDiscovery,
P2PPrivPath, P2PPrivPath,
......
...@@ -64,6 +64,9 @@ type Config struct { ...@@ -64,6 +64,9 @@ type Config struct {
// ServerMode indicates that the program should run in pre-image server mode and wait for requests. // ServerMode indicates that the program should run in pre-image server mode and wait for requests.
// No client program is run. // No client program is run.
ServerMode bool ServerMode bool
// IsCustomChainConfig indicates that the program uses a custom chain configuration
IsCustomChainConfig bool
} }
func (c *Config) Check() error { func (c *Config) Check() error {
...@@ -117,6 +120,8 @@ func NewConfig( ...@@ -117,6 +120,8 @@ func NewConfig(
l2Claim common.Hash, l2Claim common.Hash,
l2ClaimBlockNum uint64, l2ClaimBlockNum uint64,
) *Config { ) *Config {
_, err := params.LoadOPStackChainConfig(l2Genesis.ChainID.Uint64())
isCustomConfig := err != nil
return &Config{ return &Config{
Rollup: rollupCfg, Rollup: rollupCfg,
L2ChainConfig: l2Genesis, L2ChainConfig: l2Genesis,
...@@ -126,6 +131,7 @@ func NewConfig( ...@@ -126,6 +131,7 @@ func NewConfig(
L2Claim: l2Claim, L2Claim: l2Claim,
L2ClaimBlockNumber: l2ClaimBlockNum, L2ClaimBlockNumber: l2ClaimBlockNum,
L1RPCKind: sources.RPCKindBasic, L1RPCKind: sources.RPCKindBasic,
IsCustomChainConfig: isCustomConfig,
} }
} }
...@@ -156,6 +162,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) { ...@@ -156,6 +162,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) {
} }
l2GenesisPath := ctx.String(flags.L2GenesisPath.Name) l2GenesisPath := ctx.String(flags.L2GenesisPath.Name)
var l2ChainConfig *params.ChainConfig var l2ChainConfig *params.ChainConfig
var isCustomConfig bool
if l2GenesisPath == "" { if l2GenesisPath == "" {
networkName := ctx.String(flags.Network.Name) networkName := ctx.String(flags.Network.Name)
ch := chaincfg.ChainByName(networkName) ch := chaincfg.ChainByName(networkName)
...@@ -169,6 +176,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) { ...@@ -169,6 +176,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) {
l2ChainConfig = cfg l2ChainConfig = cfg
} else { } else {
l2ChainConfig, err = loadChainConfigFromGenesis(l2GenesisPath) l2ChainConfig, err = loadChainConfigFromGenesis(l2GenesisPath)
isCustomConfig = true
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid genesis: %w", err) return nil, fmt.Errorf("invalid genesis: %w", err)
...@@ -188,6 +196,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) { ...@@ -188,6 +196,7 @@ func NewConfigFromCLI(log log.Logger, ctx *cli.Context) (*Config, error) {
L1RPCKind: sources.RPCProviderKind(ctx.String(flags.L1RPCProviderKind.Name)), L1RPCKind: sources.RPCProviderKind(ctx.String(flags.L1RPCProviderKind.Name)),
ExecCmd: ctx.String(flags.Exec.Name), ExecCmd: ctx.String(flags.Exec.Name),
ServerMode: ctx.Bool(flags.Server.Name), ServerMode: ctx.Bool(flags.Server.Name),
IsCustomChainConfig: isCustomConfig,
}, nil }, nil
} }
......
package config package config
import ( import (
"math/big"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-node/chaincfg" "github.com/ethereum-optimism/optimism/op-node/chaincfg"
"github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-program/chainconfig"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -12,7 +14,7 @@ import ( ...@@ -12,7 +14,7 @@ import (
var ( var (
validRollupConfig = chaincfg.Goerli validRollupConfig = chaincfg.Goerli
validL2Genesis = params.GoerliChainConfig validL2Genesis = chainconfig.OPGoerliChainConfig
validL1Head = common.Hash{0xaa} validL1Head = common.Hash{0xaa}
validL2Head = common.Hash{0xbb} validL2Head = common.Hash{0xbb}
validL2Claim = common.Hash{0xcc} validL2Claim = common.Hash{0xcc}
...@@ -158,6 +160,19 @@ func TestRejectExecAndServerMode(t *testing.T) { ...@@ -158,6 +160,19 @@ func TestRejectExecAndServerMode(t *testing.T) {
require.ErrorIs(t, err, ErrNoExecInServerMode) require.ErrorIs(t, err, ErrNoExecInServerMode)
} }
func TestIsCustomChainConfig(t *testing.T) {
t.Run("nonCustom", func(t *testing.T) {
cfg := validConfig()
require.Equal(t, cfg.IsCustomChainConfig, false)
})
t.Run("custom", func(t *testing.T) {
customChainConfig := &params.ChainConfig{ChainID: big.NewInt(0x1212121212)}
cfg := NewConfig(validRollupConfig, customChainConfig, validL1Head, validL2Head, validL2OutputRoot, validL2Claim, validL2ClaimBlockNum)
require.Equal(t, cfg.IsCustomChainConfig, true)
})
}
func validConfig() *Config { func validConfig() *Config {
cfg := NewConfig(validRollupConfig, validL2Genesis, validL1Head, validL2Head, validL2OutputRoot, validL2Claim, validL2ClaimBlockNum) cfg := NewConfig(validRollupConfig, validL2Genesis, validL1Head, validL2Head, validL2OutputRoot, validL2Claim, validL2ClaimBlockNum)
cfg.DataDir = "/tmp/configTest" cfg.DataDir = "/tmp/configTest"
......
...@@ -38,7 +38,15 @@ func (s *LocalPreimageSource) Get(key common.Hash) ([]byte, error) { ...@@ -38,7 +38,15 @@ func (s *LocalPreimageSource) Get(key common.Hash) ([]byte, error) {
case l2ClaimBlockNumberKey: case l2ClaimBlockNumberKey:
return binary.BigEndian.AppendUint64(nil, s.config.L2ClaimBlockNumber), nil return binary.BigEndian.AppendUint64(nil, s.config.L2ClaimBlockNumber), nil
case l2ChainIDKey: case l2ChainIDKey:
return binary.BigEndian.AppendUint64(nil, client.CustomChainIDIndicator), nil // The CustomChainIDIndicator informs the client to rely on the L2ChainConfigKey to
// read the chain config. Otherwise, it'll attempt to read a non-existent hardcoded chain config
var chainID uint64
if s.config.IsCustomChainConfig {
chainID = client.CustomChainIDIndicator
} else {
chainID = s.config.L2ChainConfig.ChainID.Uint64()
}
return binary.BigEndian.AppendUint64(nil, chainID), nil
case l2ChainConfigKey: case l2ChainConfigKey:
return json.Marshal(s.config.L2ChainConfig) return json.Marshal(s.config.L2ChainConfig)
case rollupKey: case rollupKey:
......
...@@ -7,7 +7,6 @@ import ( ...@@ -7,7 +7,6 @@ import (
"github.com/ethereum-optimism/optimism/op-node/chaincfg" "github.com/ethereum-optimism/optimism/op-node/chaincfg"
preimage "github.com/ethereum-optimism/optimism/op-preimage" preimage "github.com/ethereum-optimism/optimism/op-preimage"
"github.com/ethereum-optimism/optimism/op-program/client"
"github.com/ethereum-optimism/optimism/op-program/host/config" "github.com/ethereum-optimism/optimism/op-program/host/config"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
...@@ -33,7 +32,7 @@ func TestLocalPreimageSource(t *testing.T) { ...@@ -33,7 +32,7 @@ func TestLocalPreimageSource(t *testing.T) {
{"L2OutputRoot", l2OutputRootKey, cfg.L2OutputRoot.Bytes()}, {"L2OutputRoot", l2OutputRootKey, cfg.L2OutputRoot.Bytes()},
{"L2Claim", l2ClaimKey, cfg.L2Claim.Bytes()}, {"L2Claim", l2ClaimKey, cfg.L2Claim.Bytes()},
{"L2ClaimBlockNumber", l2ClaimBlockNumberKey, binary.BigEndian.AppendUint64(nil, cfg.L2ClaimBlockNumber)}, {"L2ClaimBlockNumber", l2ClaimBlockNumberKey, binary.BigEndian.AppendUint64(nil, cfg.L2ClaimBlockNumber)},
{"L2ChainID", l2ChainIDKey, binary.BigEndian.AppendUint64(nil, client.CustomChainIDIndicator)}, {"L2ChainID", l2ChainIDKey, binary.BigEndian.AppendUint64(nil, cfg.L2ChainConfig.ChainID.Uint64())},
{"Rollup", rollupKey, asJson(t, cfg.Rollup)}, {"Rollup", rollupKey, asJson(t, cfg.Rollup)},
{"ChainConfig", l2ChainConfigKey, asJson(t, cfg.L2ChainConfig)}, {"ChainConfig", l2ChainConfigKey, asJson(t, cfg.L2ChainConfig)},
{"Unknown", preimage.LocalIndexKey(1000).PreimageKey(), nil}, {"Unknown", preimage.LocalIndexKey(1000).PreimageKey(), nil},
......
...@@ -46,6 +46,9 @@ CrossDomainOwnable3_Test:test_transferOwnership_zeroAddress_reverts() (gas: 1208 ...@@ -46,6 +46,9 @@ CrossDomainOwnable3_Test:test_transferOwnership_zeroAddress_reverts() (gas: 1208
CrossDomainOwnableThroughPortal_Test:test_depositTransaction_crossDomainOwner_succeeds() (gas: 81417) CrossDomainOwnableThroughPortal_Test:test_depositTransaction_crossDomainOwner_succeeds() (gas: 81417)
CrossDomainOwnable_Test:test_onlyOwner_notOwner_reverts() (gas: 10597) CrossDomainOwnable_Test:test_onlyOwner_notOwner_reverts() (gas: 10597)
CrossDomainOwnable_Test:test_onlyOwner_succeeds() (gas: 34883) CrossDomainOwnable_Test:test_onlyOwner_succeeds() (gas: 34883)
DelayedVetoable_Getters_Test:test_getters() (gas: 24466)
DelayedVetoable_Getters_TestFail:test_getters_notZeroAddress_reverts() (gas: 31166)
DelayedVetoable_HandleCall_TestFail:test_handleCall_unauthorizedInitiation_reverts() (gas: 20234)
DeleteOutput:test_script_succeeds() (gas: 3100) DeleteOutput:test_script_succeeds() (gas: 3100)
DeployerWhitelist_Test:test_owner_succeeds() (gas: 7582) DeployerWhitelist_Test:test_owner_succeeds() (gas: 7582)
DeployerWhitelist_Test:test_storageSlots_succeeds() (gas: 33395) DeployerWhitelist_Test:test_storageSlots_succeeds() (gas: 33395)
...@@ -88,39 +91,45 @@ FaucetTest:test_nonAdmin_drip_fails() (gas: 262520) ...@@ -88,39 +91,45 @@ FaucetTest:test_nonAdmin_drip_fails() (gas: 262520)
FaucetTest:test_receive_succeeds() (gas: 17401) FaucetTest:test_receive_succeeds() (gas: 17401)
FaucetTest:test_withdraw_nonAdmin_reverts() (gas: 13145) FaucetTest:test_withdraw_nonAdmin_reverts() (gas: 13145)
FaucetTest:test_withdraw_succeeds() (gas: 78359) FaucetTest:test_withdraw_succeeds() (gas: 78359)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 499197) FaultDisputeGame_ResolvesCorrectly_CorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 660411)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 506057) FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 667293)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 502738) FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 663974)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 505955) FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 667169)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 505224) FaultDisputeGame_ResolvesCorrectly_CorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 666460)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 497962) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 653092)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 504822) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 658598)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 501503) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 655943)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 502720) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 656899)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 501989) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 656332)
FaultDisputeGame_Test:test_addLocalData_static_succeeds() (gas: 640504) FaultDisputeGame_Test:test_addLocalData_static_succeeds() (gas: 640567)
FaultDisputeGame_Test:test_createdAt_succeeds() (gas: 10342) FaultDisputeGame_Test:test_createdAt_succeeds() (gas: 10342)
FaultDisputeGame_Test:test_extraData_succeeds() (gas: 32377) FaultDisputeGame_Test:test_extraData_succeeds() (gas: 32355)
FaultDisputeGame_Test:test_gameData_succeeds() (gas: 32804) FaultDisputeGame_Test:test_gameData_succeeds() (gas: 32782)
FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8309) FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8265)
FaultDisputeGame_Test:test_initialize_correctData_succeeds() (gas: 57628) FaultDisputeGame_Test:test_initialize_correctData_succeeds() (gas: 57739)
FaultDisputeGame_Test:test_initialize_firstOutput_reverts() (gas: 210629) FaultDisputeGame_Test:test_initialize_firstOutput_reverts() (gas: 210563)
FaultDisputeGame_Test:test_initialize_l1HeadTooOld_reverts() (gas: 228390) FaultDisputeGame_Test:test_initialize_l1HeadTooOld_reverts() (gas: 228368)
FaultDisputeGame_Test:test_move_clockCorrectness_succeeds() (gas: 415971) FaultDisputeGame_Test:test_move_clockCorrectness_succeeds() (gas: 594268)
FaultDisputeGame_Test:test_move_clockTimeExceeded_reverts() (gas: 23197) FaultDisputeGame_Test:test_move_clockTimeExceeded_reverts() (gas: 23175)
FaultDisputeGame_Test:test_move_defendRoot_reverts() (gas: 13344) FaultDisputeGame_Test:test_move_defendRoot_reverts() (gas: 13366)
FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 102898) FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 147389)
FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 407913) FaultDisputeGame_Test:test_move_duplicateClaimsDifferentSubgames_succeeds() (gas: 556885)
FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 585897)
FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 11002) FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 11002)
FaultDisputeGame_Test:test_move_nonExistentParent_reverts() (gas: 24710) FaultDisputeGame_Test:test_move_nonExistentParent_reverts() (gas: 24666)
FaultDisputeGame_Test:test_move_simpleAttack_succeeds() (gas: 107384) FaultDisputeGame_Test:test_move_simpleAttack_succeeds() (gas: 151959)
FaultDisputeGame_Test:test_resolve_challengeContested_succeeds() (gas: 224949) FaultDisputeGame_Test:test_resolve_challengeContested_succeeds() (gas: 269413)
FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9686) FaultDisputeGame_Test:test_resolve_claimAlreadyResolved_reverts() (gas: 272356)
FaultDisputeGame_Test:test_resolve_rootContested_succeeds() (gas: 109879) FaultDisputeGame_Test:test_resolve_claimAtMaxDepthAlreadyResolved_reverts() (gas: 586672)
FaultDisputeGame_Test:test_resolve_rootUncontestedClockNotExpired_succeeds() (gas: 21421) FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9732)
FaultDisputeGame_Test:test_resolve_rootUncontested_succeeds() (gas: 27279) FaultDisputeGame_Test:test_resolve_outOfOrderResolution_reverts() (gas: 309037)
FaultDisputeGame_Test:test_resolve_teamDeathmatch_succeeds() (gas: 395658) FaultDisputeGame_Test:test_resolve_rootContested_succeeds() (gas: 139044)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8276) FaultDisputeGame_Test:test_resolve_rootUncontestedButUnresolved_reverts() (gas: 15883)
FaultDisputeGame_Test:test_resolve_rootUncontestedClockNotExpired_succeeds() (gas: 18406)
FaultDisputeGame_Test:test_resolve_rootUncontested_succeeds() (gas: 51409)
FaultDisputeGame_Test:test_resolve_stepReached_succeeds() (gas: 498476)
FaultDisputeGame_Test:test_resolve_teamDeathmatch_succeeds() (gas: 443373)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8232)
FeeVault_Test:test_constructor_succeeds() (gas: 18185) FeeVault_Test:test_constructor_succeeds() (gas: 18185)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 354421) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 354421)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2952628) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2952628)
......
{ {
"src/EAS/EAS.sol": "0x1acb25751a1206eb859cc5fcf934da2f84cfb907b8e8951d86fc4e43c53a7303", "src/EAS/EAS.sol": "0x1acb25751a1206eb859cc5fcf934da2f84cfb907b8e8951d86fc4e43c53a7303",
"src/EAS/SchemaRegistry.sol": "0x305f3afed2e337cd70aac70fc202e6503b947b0a31e0d4e18c49486eeb635bb5", "src/EAS/SchemaRegistry.sol": "0x305f3afed2e337cd70aac70fc202e6503b947b0a31e0d4e18c49486eeb635bb5",
"src/L1/DelayedVetoable.sol": "0x276c6276292095e6aa37a70008cf4e0d1cbcc020dbc9107459bbc72ab5ed744f",
"src/L1/L1CrossDomainMessenger.sol": "0x14f5991022705b8bd3bf931e138a528cc9c9e90d0f1ec398efd5079224d61b3b", "src/L1/L1CrossDomainMessenger.sol": "0x14f5991022705b8bd3bf931e138a528cc9c9e90d0f1ec398efd5079224d61b3b",
"src/L1/L1ERC721Bridge.sol": "0x3e0e3d2f4c151e41585850f06e6452cdda86348debfe6ff16e364a839266450b", "src/L1/L1ERC721Bridge.sol": "0x3e0e3d2f4c151e41585850f06e6452cdda86348debfe6ff16e364a839266450b",
"src/L1/L1StandardBridge.sol": "0x12e227c6054660a83b92d823a7447db96a7d476b7a94e0f1807772d400329880", "src/L1/L1StandardBridge.sol": "0x12e227c6054660a83b92d823a7447db96a7d476b7a94e0f1807772d400329880",
...@@ -17,7 +18,7 @@ ...@@ -17,7 +18,7 @@
"src/L2/L2StandardBridge.sol": "0xfe01bcb1ddc947b9b8a7093d0971854b9fa8d49da5bd933a3dd106167907f882", "src/L2/L2StandardBridge.sol": "0xfe01bcb1ddc947b9b8a7093d0971854b9fa8d49da5bd933a3dd106167907f882",
"src/L2/L2ToL1MessagePasser.sol": "0xafc710b4d320ef450586d96a61cbd58cac814cb3b0c4fdc280eace3efdcdf321", "src/L2/L2ToL1MessagePasser.sol": "0xafc710b4d320ef450586d96a61cbd58cac814cb3b0c4fdc280eace3efdcdf321",
"src/L2/SequencerFeeVault.sol": "0xc2f733c1128d06ad60bf1e1d98c8f684a4825b11875ccdf2376ede33f5aad4e6", "src/L2/SequencerFeeVault.sol": "0xc2f733c1128d06ad60bf1e1d98c8f684a4825b11875ccdf2376ede33f5aad4e6",
"src/dispute/FaultDisputeGame.sol": "0x7b8462c29d003e96a73491c644001e1a9034bcc45c5be2a7bac3caf80d521635", "src/dispute/FaultDisputeGame.sol": "0x76e7c16431faa32e2074e6abdfe3e86f5ec90b4ac8a6b662edba8c3ce791ad80",
"src/legacy/DeployerWhitelist.sol": "0xf2129ec3da75307ba8e21bc943c332bb04704642e6e263149b5c8ee92dbcb7a8", "src/legacy/DeployerWhitelist.sol": "0xf2129ec3da75307ba8e21bc943c332bb04704642e6e263149b5c8ee92dbcb7a8",
"src/legacy/L1BlockNumber.sol": "0x30aae1fc85103476af0226b6e98c71c01feebbdc35d93401390b1ad438a37be6", "src/legacy/L1BlockNumber.sol": "0x30aae1fc85103476af0226b6e98c71c01feebbdc35d93401390b1ad438a37be6",
"src/legacy/LegacyMessagePasser.sol": "0x5c08b0a663cc49d30e4e38540f6aefab19ef287c3ecd31c8d8c3decd5f5bd497", "src/legacy/LegacyMessagePasser.sol": "0x5c08b0a663cc49d30e4e38540f6aefab19ef287c3ecd31c8d8c3decd5f5bd497",
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { ISemver } from "src/universal/ISemver.sol";
/// @title DelayedVetoable
/// @notice This contract enables a delay before a call is forwarded to a target contract, and during the delay period
/// the call can be vetoed by the authorized vetoer.
/// This contract does not support value transfers, only data is forwarded.
/// Additionally, this contract cannot be used to forward calls with data beginning with the function selector
/// of the queuedAt(bytes32) function. This is because of input validation checks which solidity performs at
/// runtime on functions which take an argument.
contract DelayedVetoable is ISemver {
/// @notice Error for when the delay has already been set.
error AlreadyDelayed();
/// @notice Error for when attempting to forward too early.
error ForwardingEarly();
/// @notice Error for the target is not set.
error TargetUnitialized();
/// @notice Error for unauthorized calls.
error Unauthorized(address expected, address actual);
/// @notice An event that is emitted when the delay is activated.
/// @param delay The delay that was activated.
event DelayActivated(uint256 delay);
/// @notice An event that is emitted when a call is initiated.
/// @param callHash The hash of the call data.
/// @param data The data of the initiated call.
event Initiated(bytes32 indexed callHash, bytes data);
/// @notice An event that is emitted each time a call is forwarded.
/// @param callHash The hash of the call data.
/// @param data The data forwarded to the target.
event Forwarded(bytes32 indexed callHash, bytes data);
/// @notice An event that is emitted each time a call is vetoed.
/// @param callHash The hash of the call data.
/// @param data The data forwarded to the target.
event Vetoed(bytes32 indexed callHash, bytes data);
/// @notice The address that all calls are forwarded to after the delay.
address internal immutable TARGET;
/// @notice The address that can veto a call.
address internal immutable VETOER;
/// @notice The address that can initiate a call.
address internal immutable INITIATOR;
/// @notice The delay which will be set after the initial system deployment is completed.
uint256 internal immutable OPERATING_DELAY;
/// @notice The current amount of time to wait before forwarding a call.
uint256 internal _delay;
/// @notice The time that a call was initiated.
mapping(bytes32 => uint256) internal _queuedAt;
/// @notice A modifier that reverts if not called by the vetoer or by address(0) to allow
/// eth_call to interact with this proxy without needing to use low-level storage
/// inspection. We assume that nobody is able to trigger calls from address(0) during
/// normal EVM execution.
modifier readOrHandle() {
if (msg.sender == address(0)) {
_;
} else {
// This WILL halt the call frame on completion.
_handleCall();
}
}
/// @notice Semantic version.
/// @custom:semver 1.0.0
string public constant version = "1.0.0";
/// @notice Sets the target admin during contract deployment.
/// @param vetoer_ Address of the vetoer.
/// @param initiator_ Address of the initiator.
/// @param target_ Address of the target.
/// @param operatingDelay_ Time to delay when the system is operational.
constructor(address vetoer_, address initiator_, address target_, uint256 operatingDelay_) {
// Note that the _delay value is not set here. Having an initial delay of 0 is helpful
// during the deployment of a new system.
VETOER = vetoer_;
INITIATOR = initiator_;
TARGET = target_;
OPERATING_DELAY = operatingDelay_;
}
/// @notice Gets the initiator
/// @return initiator_ Initiator address.
function initiator() external virtual readOrHandle returns (address initiator_) {
initiator_ = INITIATOR;
}
//// @notice Queries the vetoer address.
/// @return vetoer_ Vetoer address.
function vetoer() external virtual readOrHandle returns (address vetoer_) {
vetoer_ = VETOER;
}
//// @notice Queries the target address.
/// @return target_ Target address.
function target() external readOrHandle returns (address target_) {
target_ = TARGET;
}
/// @notice Gets the delay
/// @return delay_ Delay address.
function delay() external readOrHandle returns (uint256 delay_) {
delay_ = _delay;
}
/// @notice Gets entries in the _queuedAt mapping.
/// @param callHash The hash of the call data.
/// @return queuedAt_ The time the callHash was recorded.
function queuedAt(bytes32 callHash) external readOrHandle returns (uint256 queuedAt_) {
queuedAt_ = _queuedAt[callHash];
}
/// @notice Used for all calls that pass data to the contract.
fallback() external {
_handleCall();
}
/// @notice Receives all calls other than those made by the vetoer.
/// This enables transparent initiation and forwarding of calls to the target and avoids
/// the need for additional layers of abi encoding.
function _handleCall() internal {
// The initiator and vetoer activate the delay by passing in null data.
if (msg.data.length == 0 && _delay == 0) {
if (msg.sender != INITIATOR && msg.sender != VETOER) {
revert Unauthorized(INITIATOR, msg.sender);
}
_delay = OPERATING_DELAY;
emit DelayActivated(_delay);
return;
}
bytes32 callHash = keccak256(msg.data);
// Case 1: The initiator is calling the contract to initiate a call.
if (msg.sender == INITIATOR && _queuedAt[callHash] == 0) {
if (_delay == 0) {
// This forward function will halt the call frame on completion.
_forwardAndHalt(callHash);
}
_queuedAt[callHash] = block.timestamp;
emit Initiated(callHash, msg.data);
return;
}
// Case 2: The vetoer is calling the contract to veto a call.
// Note: The vetoer retains the ability to veto even after the delay has passed. This makes censoring the vetoer
// more costly, as there is no time limit after which their transaction can be included.
if (msg.sender == VETOER && _queuedAt[callHash] != 0) {
delete _queuedAt[callHash];
emit Vetoed(callHash, msg.data);
return;
}
// Case 3: The call is from an unpermissioned actor. We'll forward the call if the delay has
// passed.
if (_queuedAt[callHash] == 0) {
// The call has not been initiated, so we'll treat this is an unauthorized initiation attempt.
revert Unauthorized(INITIATOR, msg.sender);
}
if (_queuedAt[callHash] + _delay < block.timestamp) {
// Not enough time has passed, so we'll revert.
revert ForwardingEarly();
}
// Delete the call to prevent replays
delete _queuedAt[callHash];
_forwardAndHalt(callHash);
}
/// @notice Forwards the call to the target and halts the call frame.
function _forwardAndHalt(bytes32 callHash) internal {
// Forward the call
emit Forwarded(callHash, msg.data);
(bool success, bytes memory returndata) = TARGET.call(msg.data);
if (success == true) {
assembly {
return(add(returndata, 0x20), mload(returndata))
}
} else {
assembly {
revert(add(returndata, 0x20), mload(returndata))
}
}
}
}
...@@ -75,6 +75,12 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -75,6 +75,12 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @notice An internal mapping to allow for constant-time lookups of existing claims. /// @notice An internal mapping to allow for constant-time lookups of existing claims.
mapping(ClaimHash => bool) internal claims; mapping(ClaimHash => bool) internal claims;
/// @notice An internal mapping of subgames rooted at a claim index to other claim indices in the subgame.
mapping(uint256 => uint256[]) internal subgames;
/// @notice Indicates whether the subgame rooted at the root claim has been resolved.
bool internal subgameAtRootResolved;
/// @param _gameType The type ID of the game. /// @param _gameType The type ID of the game.
/// @param _absolutePrestate The absolute prestate of the instruction trace. /// @param _absolutePrestate The absolute prestate of the instruction trace.
/// @param _maxGameDepth The maximum depth of bisection. /// @param _maxGameDepth The maximum depth of bisection.
...@@ -232,9 +238,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -232,9 +238,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// Construct the next clock with the new duration and the current block timestamp. // Construct the next clock with the new duration and the current block timestamp.
Clock nextClock = LibClock.wrap(nextDuration, Timestamp.wrap(uint64(block.timestamp))); Clock nextClock = LibClock.wrap(nextDuration, Timestamp.wrap(uint64(block.timestamp)));
// INVARIANT: A claim may only exist at a given position once. Multiple claims may exist // INVARIANT: There cannot be multiple identical claims with identical moves on the same challengeIndex. Multiple
// at the same position, however they must have different values. // claims
ClaimHash claimHash = _claim.hashClaimPos(nextPosition); // at the same position may dispute the same challengeIndex. However, the must have different values.
ClaimHash claimHash = _claim.hashClaimPos(nextPosition, _challengeIndex);
if (claims[claimHash]) revert ClaimAlreadyExists(); if (claims[claimHash]) revert ClaimAlreadyExists();
claims[claimHash] = true; claims[claimHash] = true;
...@@ -252,6 +259,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -252,6 +259,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// Set the parent claim as countered. // Set the parent claim as countered.
claimData[_challengeIndex].countered = true; claimData[_challengeIndex].countered = true;
// Update the subgame rooted at the parent claim.
subgames[_challengeIndex].push(claimData.length - 1);
// Emit the appropriate event for the attack or defense. // Emit the appropriate event for the attack or defense.
emit Move(_challengeIndex, _claim, msg.sender); emit Move(_challengeIndex, _claim, msg.sender);
} }
...@@ -348,67 +358,64 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -348,67 +358,64 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// INVARIANT: Resolution cannot occur unless the game is currently in progress. // INVARIANT: Resolution cannot occur unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
// Search for the left-most dangling non-bottom node // INVARIANT: Resolution cannot occur unless the absolute root subgame has been resolved.
// The most recent claim is always a dangling, non-bottom node so we start with that if (!subgameAtRootResolved) revert OutOfOrderResolution();
uint256 leftMostIndex = claimData.length - 1;
uint256 leftMostTraceIndex = type(uint128).max;
for (uint256 i = leftMostIndex; i < type(uint64).max;) {
// Fetch the claim at the current index.
ClaimData storage claim = claimData[i];
// Decrement the loop counter; If it underflows, we've reached the root status_ = claimData[0].countered ? GameStatus.CHALLENGER_WINS : GameStatus.DEFENDER_WINS;
// claim and can stop searching. emit Resolved(status = status_);
unchecked {
--i;
} }
// INVARIANT: A claim can never be considered as the leftMostIndex or leftMostTraceIndex /// @inheritdoc IFaultDisputeGame
// if it has been countered. function resolveClaim(uint256 _claimIndex) external payable {
if (claim.countered) continue; // INVARIANT: Resolution cannot occur unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
// 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 = claim.position.traceIndex(MAX_GAME_DEPTH);
if (traceIndex < leftMostTraceIndex) {
leftMostTraceIndex = traceIndex;
unchecked {
leftMostIndex = i + 1;
}
}
}
// Create a reference to the left most uncontested claim and its parent. ClaimData storage parent = claimData[_claimIndex];
ClaimData storage leftMostUncontested = claimData[leftMostIndex];
// INVARIANT: The game may never be resolved unless the clock of the left-most uncontested // INVARIANT: Cannot resolve a subgame unless the clock of its root has expired
// claim's parent has expired. If the left-most uncontested claim is the root
// claim, it is uncountered, and we check if 3.5 days has passed since its
// creation.
uint256 parentIndex = leftMostUncontested.parentIndex;
Clock opposingClock = parentIndex == type(uint32).max ? leftMostUncontested.clock : claimData[parentIndex].clock;
if ( if (
Duration.unwrap(opposingClock.duration()) + (block.timestamp - Timestamp.unwrap(opposingClock.timestamp())) Duration.unwrap(parent.clock.duration()) + (block.timestamp - Timestamp.unwrap(parent.clock.timestamp()))
<= Duration.unwrap(GAME_DURATION) >> 1 <= Duration.unwrap(GAME_DURATION) >> 1
) { ) {
revert ClockNotExpired(); revert ClockNotExpired();
} }
// If the left-most dangling node is at an even depth, the defender wins. uint256[] storage challengeIndices = subgames[_claimIndex];
// Otherwise, the challenger wins and the root claim is deemed invalid.
if ( // INVARIANT: Cannot resolve subgames twice
leftMostUncontested // Uncontested claims are resolved implicitly unless they are the root claim
.position if (_claimIndex == 0 && subgameAtRootResolved) revert ClaimAlreadyResolved();
// slither-disable-next-line weak-prng if (challengeIndices.length == 0 && _claimIndex != 0) revert ClaimAlreadyResolved();
.depth() % 2 == 0 && leftMostTraceIndex != type(uint128).max
) { // Assume parent is honest until proven otherwise
status_ = GameStatus.DEFENDER_WINS; bool countered = false;
} else {
status_ = GameStatus.CHALLENGER_WINS; for (uint256 i = 0; i < challengeIndices.length; ++i) {
uint256 challengeIndex = challengeIndices[i];
// INVARIANT: Cannot resolve a subgame containing an unresolved claim
if (subgames[challengeIndex].length != 0) revert OutOfOrderResolution();
ClaimData storage claim = claimData[challengeIndex];
// Ignore false claims
if (!claim.countered) {
countered = true;
break;
}
} }
// Update the game status // Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
emit Resolved(status = status_); // resolveClaim will not need to traverse this subgame.
parent.countered = countered;
// Resolved subgames have no entries
delete subgames[_claimIndex];
// Indicate the game is ready to be resolved
if (_claimIndex == 0) {
subgameAtRootResolved = true;
}
} }
/// @inheritdoc IDisputeGame /// @inheritdoc IDisputeGame
......
...@@ -73,6 +73,14 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -73,6 +73,14 @@ interface IFaultDisputeGame is IDisputeGame {
/// @param _partOffset The offset of the data to post. /// @param _partOffset The offset of the data to post.
function addLocalData(uint256 _ident, uint256 _partOffset) external; function addLocalData(uint256 _ident, uint256 _partOffset) external;
/// @notice Resolves the subgame rooted at the given claim index.
/// @dev This function must be called bottom-up in the DAG
/// A subgame is a tree of claims that has a maximum depth of 1.
/// A subgame root claims is valid if, and only if, all of its child claims are invalid.
/// At the deepest level in the DAG, a claim is invalid if there's a successful step against it.
/// @param _claimIndex The index of the subgame root claim to resolve.
function resolveClaim(uint256 _claimIndex) external payable;
/// @notice An L1 block hash that contains the disputed output root, fetched from the /// @notice An L1 block hash that contains the disputed output root, fetched from the
/// `BlockOracle` and verified by referencing the timestamp associated with the /// `BlockOracle` and verified by referencing the timestamp associated with the
/// first L2 Output Proposal in the `L2OutputOracle` that contains the disputed /// first L2 Output Proposal in the `L2OutputOracle` that contains the disputed
......
...@@ -9,11 +9,20 @@ library LibHashing { ...@@ -9,11 +9,20 @@ library LibHashing {
/// @notice Hashes a claim and a position together. /// @notice Hashes a claim and a position together.
/// @param _claim A Claim type. /// @param _claim A Claim type.
/// @param _position The position of `claim`. /// @param _position The position of `claim`.
/// @return claimHash_ A hash of abi.encodePacked(claim, position); /// @param _challengeIndex The index of the claim being moved against.
function hashClaimPos(Claim _claim, Position _position) internal pure returns (ClaimHash claimHash_) { /// @return claimHash_ A hash of abi.encodePacked(claim, position|challengeIndex);
function hashClaimPos(
Claim _claim,
Position _position,
uint256 _challengeIndex
)
internal
pure
returns (ClaimHash claimHash_)
{
assembly { assembly {
mstore(0x00, _claim) mstore(0x00, _claim)
mstore(0x20, _position) mstore(0x20, or(shl(128, _position), and(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, _challengeIndex)))
claimHash_ := keccak256(0x00, 0x40) claimHash_ := keccak256(0x00, 0x40)
} }
} }
......
...@@ -66,6 +66,12 @@ error L1HeadTooOld(); ...@@ -66,6 +66,12 @@ error L1HeadTooOld();
/// @notice Thrown when an invalid local identifier is passed to the `addLocalData` function. /// @notice Thrown when an invalid local identifier is passed to the `addLocalData` function.
error InvalidLocalIdent(); error InvalidLocalIdent();
/// @notice Thrown when resolving claims out of order.
error OutOfOrderResolution();
/// @notice Thrown when resolving a claim that has already been resolved.
error ClaimAlreadyResolved();
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `AttestationDisputeGame` Errors // // `AttestationDisputeGame` Errors //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { CommonTest } from "./CommonTest.t.sol";
import { DelayedVetoable } from "src/L1/DelayedVetoable.sol";
contract DelayedVetoable_Init is CommonTest {
error Unauthorized(address expected, address actual);
error ForwardingEarly();
event Initiated(bytes32 indexed callHash, bytes data);
event Forwarded(bytes32 indexed callHash, bytes data);
event Vetoed(bytes32 indexed callHash, bytes data);
address target = address(0xabba);
address initiator = alice;
address vetoer = bob;
uint256 operatingDelay = 14 days;
DelayedVetoable delayedVetoable;
function setUp() public override {
super.setUp();
delayedVetoable = new DelayedVetoable({
initiator_: alice,
vetoer_: bob,
target_: address(target),
operatingDelay_: operatingDelay
});
// Most tests will use the operating delay, so we call as the initiator with null data
// to set the delay. For tests that need to use the initial zero delay, we'll modify the
// value in storage.
vm.prank(initiator);
(bool success,) = address(delayedVetoable).call(hex"");
}
/// @dev This function is used to prevent initiating the delay unintentionally.
/// It should only be used on tests prior to the delay being activated.
/// @param data The data to be used in the call.
function assumeNonzeroData(bytes memory data) internal pure {
vm.assume(data.length > 0);
}
/// @dev This function is used to ensure that the data does not clash with the queuedAt function selector.
/// @param data The data to be used in the call.
function assumeNoClash(bytes calldata data) internal pure {
if (data.length >= 4) {
vm.assume(bytes4(data[0:4]) != bytes4(keccak256("queuedAt(bytes32)")));
}
}
}
contract DelayedVetoable_Getters_Test is DelayedVetoable_Init {
/// @dev The getters return the expected values when called by the zero address.
function test_getters() external {
vm.startPrank(address(0));
assertEq(delayedVetoable.initiator(), initiator);
assertEq(delayedVetoable.vetoer(), vetoer);
assertEq(delayedVetoable.target(), target);
assertEq(delayedVetoable.delay(), operatingDelay);
assertEq(delayedVetoable.queuedAt(keccak256(abi.encode(0))), 0);
}
}
contract DelayedVetoable_Getters_TestFail is DelayedVetoable_Init {
/// @dev Check that getter calls from unauthorized entities will revert.
function test_getters_notZeroAddress_reverts() external {
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, initiator, address(this)));
delayedVetoable.initiator();
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, initiator, address(this)));
delayedVetoable.vetoer();
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, initiator, address(this)));
delayedVetoable.target();
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, initiator, address(this)));
delayedVetoable.delay();
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, initiator, address(this)));
delayedVetoable.queuedAt(keccak256(abi.encode(0)));
}
}
contract DelayedVetoable_HandleCall_Test is DelayedVetoable_Init {
/// @dev A call can be initiated by the initiator.
function testFuzz_handleCall_initiation_succeeds(bytes calldata data) external {
assumeNoClash(data);
vm.expectEmit(true, false, false, true, address(delayedVetoable));
emit Initiated(keccak256(data), data);
vm.prank(initiator);
(bool success,) = address(delayedVetoable).call(data);
assertTrue(success);
}
/// @dev The delay is inititially set to zero and the call is immediately forwarded.
function testFuzz_handleCall_initialForwardingImmediately_succeeds(
bytes calldata inData,
bytes calldata outData
)
external
{
assumeNonzeroData(inData);
assumeNoClash(inData);
// Reset the delay to zero
vm.store(address(delayedVetoable), bytes32(uint256(0)), bytes32(uint256(0)));
vm.mockCall(target, inData, outData);
vm.expectEmit(true, false, false, true, address(delayedVetoable));
vm.expectCall({ callee: target, data: inData });
emit Forwarded(keccak256(inData), inData);
vm.prank(initiator);
(bool success, bytes memory returnData) = address(delayedVetoable).call(inData);
assertTrue(success);
assertEq(returnData, outData);
// Check that the callHash is not stored for future forwarding
bytes32 callHash = keccak256(inData);
vm.prank(address(0));
assertEq(delayedVetoable.queuedAt(callHash), 0);
}
/// @dev Calls are not forwarded until the delay has passed.
function testFuzz_handleCall_forwardingWithDelay_succeeds(bytes calldata data) external {
assumeNonzeroData(data);
assumeNoClash(data);
vm.prank(initiator);
(bool success,) = address(delayedVetoable).call(data);
// Check that the call is in the _queuedAt mapping
bytes32 callHash = keccak256(data);
vm.prank(address(0));
assertEq(delayedVetoable.queuedAt(callHash), block.timestamp);
vm.warp(block.timestamp + operatingDelay);
vm.expectEmit(true, false, false, true, address(delayedVetoable));
emit Forwarded(keccak256(data), data);
vm.expectCall({ callee: target, data: data });
(success,) = address(delayedVetoable).call(data);
assertTrue(success);
}
}
contract DelayedVetoable_HandleCall_TestFail is DelayedVetoable_Init {
/// @dev Only the initiator can initiate a call.
function test_handleCall_unauthorizedInitiation_reverts() external {
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, initiator, address(this)));
(bool success,) = address(delayedVetoable).call(NON_ZERO_DATA);
}
/// @dev The call cannot be forwarded until the delay has passed.
function testFuzz_handleCall_forwardingTooSoon_reverts(bytes calldata data) external {
assumeNoClash(data);
vm.prank(initiator);
(bool success,) = address(delayedVetoable).call(data);
vm.expectRevert(abi.encodeWithSelector(ForwardingEarly.selector));
(success,) = address(delayedVetoable).call(data);
}
/// @dev The call cannot be forwarded a second time.
function testFuzz_handleCall_forwardingTwice_reverts(bytes calldata data) external {
assumeNoClash(data);
// Initiate the call
vm.prank(initiator);
(bool success,) = address(delayedVetoable).call(data);
vm.warp(block.timestamp + operatingDelay);
vm.expectEmit(true, false, false, true, address(delayedVetoable));
emit Forwarded(keccak256(data), data);
// Forward the call
vm.expectCall({ callee: target, data: data });
(success,) = address(delayedVetoable).call(data);
assertTrue(success);
// Attempt to foward the same call again.
vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, initiator, address(this)));
(success,) = address(delayedVetoable).call(data);
assertTrue(success);
}
/// @dev If the target reverts, it is bubbled up.
function testFuzz_handleCall_forwardingTargetReverts_reverts(
bytes calldata inData,
bytes calldata outData
)
external
{
assumeNoClash(inData);
// Initiate the call
vm.prank(initiator);
(bool success,) = address(delayedVetoable).call(inData);
vm.warp(block.timestamp + operatingDelay);
vm.expectEmit(true, false, false, true, address(delayedVetoable));
emit Forwarded(keccak256(inData), inData);
vm.mockCallRevert(target, inData, outData);
// Forward the call
vm.expectRevert(outData);
(bool success2,) = address(delayedVetoable).call(inData);
}
/// @dev A test documenting the single instance in which the contract is not 'transparent' to the initiator.
function testFuzz_handleCall_queuedAtClash_reverts(bytes memory outData) external {
// This will get us calldata with the same function selector as the queuedAt function, but
// with the incorrect input data length.
bytes memory inData = abi.encodePacked(keccak256("queuedAt(bytes32)"));
// Reset the delay to zero
vm.store(address(delayedVetoable), bytes32(uint256(0)), bytes32(uint256(0)));
vm.prank(initiator);
vm.expectRevert(outData);
(bool success,) = address(delayedVetoable).call(inData);
}
}
This diff is collapsed.
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