diff --git a/op-challenger/cmd/main_test.go b/op-challenger/cmd/main_test.go index cd0eefb4c65..8dee487e0ed 100644 --- a/op-challenger/cmd/main_test.go +++ b/op-challenger/cmd/main_test.go @@ -29,6 +29,12 @@ var ( datadir = "./test_data" cannonL2 = "http://example.com:9545" rollupRpc = "http://example.com:8555" + asteriscNetwork = "op-mainnet" + asteriscBin = "./bin/astersic" + asteriscServer = "./bin/op-program" + asteriscPreState = "./pre.json" + asteriscL2 = "http://example.com:9545" + otherAsteriscNetwork = "op-goerli" ) func TestLogLevel(t *testing.T) { @@ -111,14 +117,18 @@ func TestMultipleTraceTypes(t *testing.T) { t.Run("WithAllOptions", func(t *testing.T) { argsMap := requiredArgs(config.TraceTypeCannon) addRequiredOutputArgs(argsMap) + // Add Asterisc required flags + addRequiredAsteriscArgs(argsMap) args := toArgList(argsMap) // Add extra trace types (cannon is already specified) args = append(args, "--trace-type", config.TraceTypeAlphabet.String()) args = append(args, "--trace-type", config.TraceTypePermissioned.String()) + args = append(args, + "--trace-type", config.TraceTypeAsterisc.String()) cfg := configForArgs(t, args) - require.Equal(t, []config.TraceType{config.TraceTypeCannon, config.TraceTypeAlphabet, config.TraceTypePermissioned}, cfg.TraceTypes) + require.Equal(t, []config.TraceType{config.TraceTypeCannon, config.TraceTypeAlphabet, config.TraceTypePermissioned, config.TraceTypeAsterisc}, cfg.TraceTypes) }) t.Run("WithSomeOptions", func(t *testing.T) { argsMap := requiredArgs(config.TraceTypeCannon) @@ -243,6 +253,165 @@ func TestPollInterval(t *testing.T) { }) } +func TestAsteriscRequiredArgs(t *testing.T) { + for _, traceType := range []config.TraceType{config.TraceTypeAsterisc} { + traceType := traceType + t.Run(fmt.Sprintf("TestAsteriscBin-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--asterisc-bin")) + }) + + t.Run("Required", func(t *testing.T) { + verifyArgsInvalid(t, "flag asterisc-bin is required", addRequiredArgsExcept(traceType, "--asterisc-bin")) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--asterisc-bin", "--asterisc-bin=./asterisc")) + require.Equal(t, "./asterisc", cfg.AsteriscBin) + }) + }) + + t.Run(fmt.Sprintf("TestAsteriscServer-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--asterisc-server")) + }) + + t.Run("Required", func(t *testing.T) { + verifyArgsInvalid(t, "flag asterisc-server is required", addRequiredArgsExcept(traceType, "--asterisc-server")) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--asterisc-server", "--asterisc-server=./op-program")) + require.Equal(t, "./op-program", cfg.AsteriscServer) + }) + }) + + t.Run(fmt.Sprintf("TestAstersicAbsolutePrestate-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--asterisc-prestate")) + }) + + t.Run("Required", func(t *testing.T) { + verifyArgsInvalid(t, "flag asterisc-prestate is required", addRequiredArgsExcept(traceType, "--asterisc-prestate")) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--asterisc-prestate", "--asterisc-prestate=./pre.json")) + require.Equal(t, "./pre.json", cfg.AsteriscAbsolutePreState) + }) + }) + + t.Run(fmt.Sprintf("TestAsteriscL2-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--asterisc-l2")) + }) + + t.Run("RequiredForAsteriscTrace", func(t *testing.T) { + verifyArgsInvalid(t, "flag asterisc-l2 is required", addRequiredArgsExcept(traceType, "--asterisc-l2")) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgs(traceType)) + require.Equal(t, asteriscL2, cfg.AsteriscL2) + }) + }) + + t.Run(fmt.Sprintf("TestAsteriscSnapshotFreq-%v", traceType), func(t *testing.T) { + t.Run("UsesDefault", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgs(traceType)) + require.Equal(t, config.DefaultAsteriscSnapshotFreq, cfg.AsteriscSnapshotFreq) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgs(traceType, "--asterisc-snapshot-freq=1234")) + require.Equal(t, uint(1234), cfg.AsteriscSnapshotFreq) + }) + + t.Run("Invalid", func(t *testing.T) { + verifyArgsInvalid(t, "invalid value \"abc\" for flag -asterisc-snapshot-freq", + addRequiredArgs(traceType, "--asterisc-snapshot-freq=abc")) + }) + }) + + t.Run(fmt.Sprintf("TestAsteriscInfoFreq-%v", traceType), func(t *testing.T) { + t.Run("UsesDefault", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgs(traceType)) + require.Equal(t, config.DefaultAsteriscInfoFreq, cfg.AsteriscInfoFreq) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgs(traceType, "--asterisc-info-freq=1234")) + require.Equal(t, uint(1234), cfg.AsteriscInfoFreq) + }) + + t.Run("Invalid", func(t *testing.T) { + verifyArgsInvalid(t, "invalid value \"abc\" for flag -asterisc-info-freq", + addRequiredArgs(traceType, "--asterisc-info-freq=abc")) + }) + }) + + t.Run(fmt.Sprintf("TestRequireEitherAsteriscNetworkOrRollupAndGenesis-%v", traceType), func(t *testing.T) { + verifyArgsInvalid( + t, + "flag asterisc-network or asterisc-rollup-config and asterisc-l2-genesis is required", + addRequiredArgsExcept(traceType, "--asterisc-network")) + verifyArgsInvalid( + t, + "flag asterisc-network or asterisc-rollup-config and asterisc-l2-genesis is required", + addRequiredArgsExcept(traceType, "--asterisc-network", "--asterisc-rollup-config=rollup.json")) + verifyArgsInvalid( + t, + "flag asterisc-network or asterisc-rollup-config and asterisc-l2-genesis is required", + addRequiredArgsExcept(traceType, "--asterisc-network", "--asterisc-l2-genesis=gensis.json")) + }) + + t.Run(fmt.Sprintf("TestMustNotSpecifyNetworkAndRollup-%v", traceType), func(t *testing.T) { + verifyArgsInvalid( + t, + "flag asterisc-network can not be used with asterisc-rollup-config and asterisc-l2-genesis", + addRequiredArgsExcept(traceType, "--asterisc-network", + "--asterisc-network", asteriscNetwork, "--asterisc-rollup-config=rollup.json")) + }) + + t.Run(fmt.Sprintf("TestAsteriscNetwork-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--asterisc-network")) + }) + + t.Run("NotRequiredWhenRollupAndGenesIsSpecified", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(traceType, "--asterisc-network", + "--asterisc-rollup-config=rollup.json", "--asterisc-l2-genesis=genesis.json")) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--asterisc-network", "--asterisc-network", otherAsteriscNetwork)) + require.Equal(t, otherAsteriscNetwork, cfg.AsteriscNetwork) + }) + }) + + t.Run(fmt.Sprintf("TestAsteriscRollupConfig-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--asterisc-rollup-config")) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--asterisc-network", "--asterisc-rollup-config=rollup.json", "--asterisc-l2-genesis=genesis.json")) + require.Equal(t, "rollup.json", cfg.AsteriscRollupConfigPath) + }) + }) + + t.Run(fmt.Sprintf("TestAsteriscL2Genesis-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--asterisc-l2-genesis")) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--asterisc-network", "--asterisc-rollup-config=rollup.json", "--asterisc-l2-genesis=genesis.json")) + require.Equal(t, "genesis.json", cfg.AsteriscL2GenesisPath) + }) + }) + } +} func TestCannonRequiredArgs(t *testing.T) { for _, traceType := range []config.TraceType{config.TraceTypeCannon, config.TraceTypePermissioned} { traceType := traceType @@ -560,6 +729,8 @@ func requiredArgs(traceType config.TraceType) map[string]string { switch traceType { case config.TraceTypeCannon, config.TraceTypePermissioned: addRequiredCannonArgs(args) + case config.TraceTypeAsterisc: + addRequiredAsteriscArgs(args) case config.TraceTypeAlphabet: addRequiredOutputArgs(args) } @@ -575,6 +746,15 @@ func addRequiredCannonArgs(args map[string]string) { addRequiredOutputArgs(args) } +func addRequiredAsteriscArgs(args map[string]string) { + args["--asterisc-network"] = asteriscNetwork + args["--asterisc-bin"] = asteriscBin + args["--asterisc-server"] = asteriscServer + args["--asterisc-prestate"] = asteriscPreState + args["--asterisc-l2"] = asteriscL2 + addRequiredOutputArgs(args) +} + func addRequiredOutputArgs(args map[string]string) { args["--rollup-rpc"] = rollupRpc } diff --git a/op-challenger/config/config.go b/op-challenger/config/config.go index d13c716b803..ece11c8a4dc 100644 --- a/op-challenger/config/config.go +++ b/op-challenger/config/config.go @@ -35,16 +35,30 @@ var ( ErrCannonNetworkUnknown = errors.New("unknown cannon network") ErrMissingRollupRpc = errors.New("missing rollup rpc url") ) +var ( + ErrMissingAsteriscL2 = errors.New("missing asterisc L2") + ErrMissingAsteriscBin = errors.New("missing asterisc bin") + ErrMissingAsteriscServer = errors.New("missing asterisc server") + ErrMissingAsteriscAbsolutePreState = errors.New("missing asterisc absolute pre-state") + ErrMissingAsteriscSnapshotFreq = errors.New("missing asterisc snapshot freq") + ErrMissingAsteriscInfoFreq = errors.New("missing asterisc info freq") + ErrMissingAsteriscRollupConfig = errors.New("missing asterisc network or rollup config path") + ErrMissingAsteriscL2Genesis = errors.New("missing asterisc network or l2 genesis path") + ErrAsteriscNetworkAndRollupConfig = errors.New("only specify one of network or rollup config path") + ErrAsteriscNetworkAndL2Genesis = errors.New("only specify one of network or l2 genesis path") + ErrAsteriscNetworkUnknown = errors.New("unknown asterisc network") +) type TraceType string const ( TraceTypeAlphabet TraceType = "alphabet" TraceTypeCannon TraceType = "cannon" + TraceTypeAsterisc TraceType = "asterisc" TraceTypePermissioned TraceType = "permissioned" ) -var TraceTypes = []TraceType{TraceTypeAlphabet, TraceTypeCannon, TraceTypePermissioned} +var TraceTypes = []TraceType{TraceTypeAlphabet, TraceTypeCannon, TraceTypePermissioned, TraceTypeAsterisc} func (t TraceType) String() string { return string(t) @@ -74,9 +88,11 @@ func ValidTraceType(value TraceType) bool { } const ( - DefaultPollInterval = time.Second * 12 - DefaultCannonSnapshotFreq = uint(1_000_000_000) - DefaultCannonInfoFreq = uint(10_000_000) + DefaultPollInterval = time.Second * 12 + DefaultCannonSnapshotFreq = uint(1_000_000_000) + DefaultCannonInfoFreq = uint(10_000_000) + DefaultAsteriscSnapshotFreq = uint(1_000_000_000) + DefaultAsteriscInfoFreq = uint(10_000_000) // DefaultGameWindow is the default maximum time duration in the past // that the challenger will look for games to progress. // The default value is 15 days, which is an 8 day resolution buffer @@ -105,8 +121,7 @@ type Config struct { TraceTypes []TraceType // Type of traces supported - // Specific to the output cannon trace type - RollupRpc string + RollupRpc string // L2 Rollup RPC Url // Specific to the cannon trace provider CannonBin string // Path to the cannon executable to run when generating trace data @@ -119,6 +134,17 @@ type Config struct { CannonSnapshotFreq uint // Frequency of snapshots to create when executing cannon (in VM instructions) CannonInfoFreq uint // Frequency of cannon progress log messages (in VM instructions) + // Specific to the asterisc trace provider + AsteriscBin string // Path to the asterisc executable to run when generating trace data + AsteriscServer string // Path to the op-program executable that provides the pre-image oracle server + AsteriscAbsolutePreState string // File to load the absolute pre-state for Asterisc traces from + AsteriscNetwork string + AsteriscRollupConfigPath string + AsteriscL2GenesisPath string + AsteriscL2 string // L2 RPC Url + AsteriscSnapshotFreq uint // Frequency of snapshots to create when executing asterisc (in VM instructions) + AsteriscInfoFreq uint // Frequency of asterisc progress log messages (in VM instructions) + MaxPendingTx uint64 // Maximum number of pending transactions (0 == no limit) TxMgrConfig txmgr.CLIConfig @@ -150,9 +176,11 @@ func NewConfig( Datadir: datadir, - CannonSnapshotFreq: DefaultCannonSnapshotFreq, - CannonInfoFreq: DefaultCannonInfoFreq, - GameWindow: DefaultGameWindow, + CannonSnapshotFreq: DefaultCannonSnapshotFreq, + CannonInfoFreq: DefaultCannonInfoFreq, + AsteriscSnapshotFreq: DefaultAsteriscSnapshotFreq, + AsteriscInfoFreq: DefaultAsteriscInfoFreq, + GameWindow: DefaultGameWindow, } } @@ -220,6 +248,44 @@ func (c Config) Check() error { return ErrMissingCannonInfoFreq } } + if c.TraceTypeEnabled(TraceTypeAsterisc) { + if c.AsteriscBin == "" { + return ErrMissingAsteriscBin + } + if c.AsteriscServer == "" { + return ErrMissingAsteriscServer + } + if c.AsteriscNetwork == "" { + if c.AsteriscRollupConfigPath == "" { + return ErrMissingAsteriscRollupConfig + } + if c.AsteriscL2GenesisPath == "" { + return ErrMissingAsteriscL2Genesis + } + } else { + if c.AsteriscRollupConfigPath != "" { + return ErrAsteriscNetworkAndRollupConfig + } + if c.AsteriscL2GenesisPath != "" { + return ErrAsteriscNetworkAndL2Genesis + } + if ch := chaincfg.ChainByName(c.AsteriscNetwork); ch == nil { + return fmt.Errorf("%w: %v", ErrAsteriscNetworkUnknown, c.AsteriscNetwork) + } + } + if c.AsteriscAbsolutePreState == "" { + return ErrMissingAsteriscAbsolutePreState + } + if c.AsteriscL2 == "" { + return ErrMissingAsteriscL2 + } + if c.AsteriscSnapshotFreq == 0 { + return ErrMissingAsteriscSnapshotFreq + } + if c.AsteriscInfoFreq == 0 { + return ErrMissingAsteriscInfoFreq + } + } if err := c.TxMgrConfig.Check(); err != nil { return err } diff --git a/op-challenger/config/config_test.go b/op-challenger/config/config_test.go index 01210557c60..20614548248 100644 --- a/op-challenger/config/config_test.go +++ b/op-challenger/config/config_test.go @@ -24,6 +24,14 @@ var ( validRollupRpc = "http://localhost:8555" ) +var ( + validAsteriscBin = "./bin/asterisc" + validAsteriscOpProgramBin = "./bin/op-program" + validAsteriscNetwork = "mainnet" + validAsteriscAbsolutPreState = "pre.json" + validAsteriscL2 = "http://localhost:9545" +) + var cannonTraceTypes = []TraceType{TraceTypeCannon, TraceTypePermissioned} func validConfig(traceType TraceType) Config { @@ -35,6 +43,13 @@ func validConfig(traceType TraceType) Config { cfg.CannonL2 = validCannonL2 cfg.CannonNetwork = validCannonNetwork } + if traceType == TraceTypeAsterisc { + cfg.AsteriscBin = validAsteriscBin + cfg.AsteriscServer = validAsteriscOpProgramBin + cfg.AsteriscAbsolutePreState = validAsteriscAbsolutPreState + cfg.AsteriscL2 = validAsteriscL2 + cfg.AsteriscNetwork = validAsteriscNetwork + } cfg.RollupRpc = validRollupRpc return cfg } @@ -209,7 +224,7 @@ func TestRollupRpcRequired(t *testing.T) { } } -func TestRequireConfigForMultipleTraceTypes(t *testing.T) { +func TestRequireConfigForMultipleTraceTypesForCannon(t *testing.T) { cfg := validConfig(TraceTypeCannon) cfg.TraceTypes = []TraceType{TraceTypeCannon, TraceTypeAlphabet} // Set all required options and check its valid @@ -225,3 +240,20 @@ func TestRequireConfigForMultipleTraceTypes(t *testing.T) { cfg.RollupRpc = "" require.ErrorIs(t, cfg.Check(), ErrMissingRollupRpc) } + +func TestRequireConfigForMultipleTraceTypesForAsterisc(t *testing.T) { + cfg := validConfig(TraceTypeAsterisc) + cfg.TraceTypes = []TraceType{TraceTypeAsterisc, TraceTypeAlphabet} + // Set all required options and check its valid + cfg.RollupRpc = validRollupRpc + require.NoError(t, cfg.Check()) + + // Require asterisc specific args + cfg.AsteriscL2 = "" + require.ErrorIs(t, cfg.Check(), ErrMissingAsteriscL2) + cfg.AsteriscL2 = validAsteriscL2 + + // Require output asterisc specific args + cfg.RollupRpc = "" + require.ErrorIs(t, cfg.Check(), ErrMissingRollupRpc) +} diff --git a/op-challenger/flags/flags.go b/op-challenger/flags/flags.go index 718cd5c00b2..ec8f47fbd5a 100644 --- a/op-challenger/flags/flags.go +++ b/op-challenger/flags/flags.go @@ -138,6 +138,56 @@ var ( EnvVars: prefixEnvVars("CANNON_INFO_FREQ"), Value: config.DefaultCannonInfoFreq, } + AsteriscNetworkFlag = &cli.StringFlag{ + Name: "asterisc-network", + Usage: fmt.Sprintf( + "Predefined network selection. Available networks: %s (asterisc trace type only)", + strings.Join(chaincfg.AvailableNetworks(), ", "), + ), + EnvVars: prefixEnvVars("ASTERISC_NETWORK"), + } + AsteriscRollupConfigFlag = &cli.StringFlag{ + Name: "asterisc-rollup-config", + Usage: "Rollup chain parameters (asterisc trace type only)", + EnvVars: prefixEnvVars("ASTERISC_ROLLUP_CONFIG"), + } + AsteriscL2GenesisFlag = &cli.StringFlag{ + Name: "asterisc-l2-genesis", + Usage: "Path to the op-geth genesis file (asterisc trace type only)", + EnvVars: prefixEnvVars("ASTERISC_L2_GENESIS"), + } + AsteriscBinFlag = &cli.StringFlag{ + Name: "asterisc-bin", + Usage: "Path to asterisc executable to use when generating trace data (asterisc trace type only)", + EnvVars: prefixEnvVars("ASTERISC_BIN"), + } + AsteriscServerFlag = &cli.StringFlag{ + Name: "asterisc-server", + Usage: "Path to executable to use as pre-image oracle server when generating trace data (asterisc trace type only)", + EnvVars: prefixEnvVars("ASTERISC_SERVER"), + } + AsteriscPreStateFlag = &cli.StringFlag{ + Name: "asterisc-prestate", + Usage: "Path to absolute prestate to use when generating trace data (asterisc trace type only)", + EnvVars: prefixEnvVars("ASTERISC_PRESTATE"), + } + AsteriscL2Flag = &cli.StringFlag{ + Name: "asterisc-l2", + Usage: "L2 Address of L2 JSON-RPC endpoint to use (eth and debug namespace required) (asterisc trace type only)", + EnvVars: prefixEnvVars("ASTERISC_L2"), + } + AsteriscSnapshotFreqFlag = &cli.UintFlag{ + Name: "asterisc-snapshot-freq", + Usage: "Frequency of asterisc snapshots to generate in VM steps (asterisc trace type only)", + EnvVars: prefixEnvVars("ASTERISC_SNAPSHOT_FREQ"), + Value: config.DefaultAsteriscSnapshotFreq, + } + AsteriscInfoFreqFlag = &cli.UintFlag{ + Name: "asterisc-info-freq", + Usage: "Frequency of asterisc info log messages to generate in VM steps (asterisc trace type only)", + EnvVars: prefixEnvVars("ASTERISC_INFO_FREQ"), + Value: config.DefaultAsteriscInfoFreq, + } GameWindowFlag = &cli.DurationFlag{ Name: "game-window", Usage: "The time window which the challenger will look for games to progress and claim bonds. " + @@ -184,6 +234,15 @@ var optionalFlags = []cli.Flag{ CannonL2Flag, CannonSnapshotFreqFlag, CannonInfoFreqFlag, + AsteriscNetworkFlag, + AsteriscRollupConfigFlag, + AsteriscL2GenesisFlag, + AsteriscBinFlag, + AsteriscServerFlag, + AsteriscPreStateFlag, + AsteriscL2Flag, + AsteriscSnapshotFreqFlag, + AsteriscInfoFreqFlag, GameWindowFlag, SelectiveClaimResolutionFlag, UnsafeAllowInvalidPrestate, @@ -227,6 +286,32 @@ func CheckCannonFlags(ctx *cli.Context) error { return nil } +func CheckAsteriscFlags(ctx *cli.Context) error { + if !ctx.IsSet(AsteriscNetworkFlag.Name) && + !(ctx.IsSet(AsteriscRollupConfigFlag.Name) && ctx.IsSet(AsteriscL2GenesisFlag.Name)) { + return fmt.Errorf("flag %v or %v and %v is required", + AsteriscNetworkFlag.Name, AsteriscRollupConfigFlag.Name, AsteriscL2GenesisFlag.Name) + } + if ctx.IsSet(AsteriscNetworkFlag.Name) && + (ctx.IsSet(AsteriscRollupConfigFlag.Name) || ctx.IsSet(AsteriscL2GenesisFlag.Name)) { + return fmt.Errorf("flag %v can not be used with %v and %v", + AsteriscNetworkFlag.Name, AsteriscRollupConfigFlag.Name, AsteriscL2GenesisFlag.Name) + } + if !ctx.IsSet(AsteriscBinFlag.Name) { + return fmt.Errorf("flag %s is required", AsteriscBinFlag.Name) + } + if !ctx.IsSet(AsteriscServerFlag.Name) { + return fmt.Errorf("flag %s is required", AsteriscServerFlag.Name) + } + if !ctx.IsSet(AsteriscPreStateFlag.Name) { + return fmt.Errorf("flag %s is required", AsteriscPreStateFlag.Name) + } + if !ctx.IsSet(AsteriscL2Flag.Name) { + return fmt.Errorf("flag %s is required", AsteriscL2Flag.Name) + } + return nil +} + func CheckRequired(ctx *cli.Context, traceTypes []config.TraceType) error { for _, f := range requiredFlags { if !ctx.IsSet(f.Names()[0]) { @@ -239,6 +324,10 @@ func CheckRequired(ctx *cli.Context, traceTypes []config.TraceType) error { if err := CheckCannonFlags(ctx); err != nil { return err } + case config.TraceTypeAsterisc: + if err := CheckAsteriscFlags(ctx); err != nil { + return err + } case config.TraceTypeAlphabet: default: return fmt.Errorf("invalid trace type. must be one of %v", config.TraceTypes) @@ -326,6 +415,15 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { CannonL2: ctx.String(CannonL2Flag.Name), CannonSnapshotFreq: ctx.Uint(CannonSnapshotFreqFlag.Name), CannonInfoFreq: ctx.Uint(CannonInfoFreqFlag.Name), + AsteriscNetwork: ctx.String(AsteriscNetworkFlag.Name), + AsteriscRollupConfigPath: ctx.String(AsteriscRollupConfigFlag.Name), + AsteriscL2GenesisPath: ctx.String(AsteriscL2GenesisFlag.Name), + AsteriscBin: ctx.String(AsteriscBinFlag.Name), + AsteriscServer: ctx.String(AsteriscServerFlag.Name), + AsteriscAbsolutePreState: ctx.String(AsteriscPreStateFlag.Name), + AsteriscL2: ctx.String(AsteriscL2Flag.Name), + AsteriscSnapshotFreq: ctx.Uint(AsteriscSnapshotFreqFlag.Name), + AsteriscInfoFreq: ctx.Uint(AsteriscInfoFreqFlag.Name), TxMgrConfig: txMgrConfig, MetricsConfig: metricsConfig, PprofConfig: pprofConfig, diff --git a/op-challenger/game/fault/register.go b/op-challenger/game/fault/register.go index 1117dfb641e..444b7d49efd 100644 --- a/op-challenger/game/fault/register.go +++ b/op-challenger/game/fault/register.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum-optimism/optimism/op-challenger/game/fault/claims" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/alphabet" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/asterisc" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" @@ -58,7 +59,7 @@ func RegisterGameTypes( ) (CloseFunc, error) { var closer CloseFunc var l2Client *ethclient.Client - if cfg.TraceTypeEnabled(config.TraceTypeCannon) || cfg.TraceTypeEnabled(config.TraceTypePermissioned) { + if cfg.TraceTypeEnabled(config.TraceTypeCannon) || cfg.TraceTypeEnabled(config.TraceTypePermissioned) || cfg.TraceTypeEnabled(config.TraceTypeAsterisc) { l2, err := ethclient.DialContext(ctx, cfg.CannonL2) if err != nil { return nil, fmt.Errorf("dial l2 client %v: %w", cfg.CannonL2, err) @@ -78,6 +79,11 @@ func RegisterGameTypes( return nil, fmt.Errorf("failed to register permissioned cannon game type: %w", err) } } + if cfg.TraceTypeEnabled(config.TraceTypeAsterisc) { + if err := registerAsterisc(faultTypes.AsteriscGameType, registry, oracles, ctx, systemClock, l1Clock, logger, m, cfg, syncValidator, rollupClient, txSender, gameFactory, caller, l2Client, l1HeaderSource, selective, claimants); err != nil { + return nil, fmt.Errorf("failed to register asterisc game type: %w", err) + } + } if cfg.TraceTypeEnabled(config.TraceTypeAlphabet) { if err := registerAlphabet(registry, oracles, ctx, systemClock, l1Clock, logger, m, syncValidator, rollupClient, txSender, gameFactory, caller, l1HeaderSource, selective, claimants); err != nil { return nil, fmt.Errorf("failed to register alphabet game type: %w", err) @@ -167,6 +173,74 @@ func registerOracle(ctx context.Context, m metrics.Metricer, oracles OracleRegis return nil } +func registerAsterisc( + gameType uint32, + registry Registry, + oracles OracleRegistry, + ctx context.Context, + systemClock clock.Clock, + l1Clock faultTypes.ClockReader, + logger log.Logger, + m metrics.Metricer, + cfg *config.Config, + syncValidator SyncValidator, + rollupClient outputs.OutputRollupClient, + txSender TxSender, + gameFactory *contracts.DisputeGameFactoryContract, + caller *batching.MultiCaller, + l2Client cannon.L2HeaderSource, + l1HeaderSource L1HeaderSource, + selective bool, + claimants []common.Address, +) error { + asteriscPrestateProvider := asterisc.NewPrestateProvider(cfg.AsteriscAbsolutePreState) + playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) { + contract, err := contracts.NewFaultDisputeGameContract(m, game.Proxy, caller) + if err != nil { + return nil, err + } + oracle, err := contract.GetOracle(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load oracle for game %v: %w", game.Proxy, err) + } + oracles.RegisterOracle(oracle) + prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx) + if err != nil { + return nil, err + } + splitDepth, err := contract.GetSplitDepth(ctx) + if err != nil { + return nil, fmt.Errorf("failed to load split depth: %w", err) + } + l1HeadID, err := loadL1Head(contract, ctx, l1HeaderSource) + if err != nil { + return nil, err + } + prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock) + creator := func(ctx context.Context, logger log.Logger, gameDepth faultTypes.Depth, dir string) (faultTypes.TraceAccessor, error) { + accessor, err := outputs.NewOutputAsteriscTraceAccessor(logger, m, cfg, l2Client, prestateProvider, rollupClient, dir, l1HeadID, splitDepth, prestateBlock, poststateBlock) + if err != nil { + return nil, err + } + return accessor, nil + } + prestateValidator := NewPrestateValidator("asterisc", contract.GetAbsolutePrestateHash, asteriscPrestateProvider) + genesisValidator := NewPrestateValidator("output root", contract.GetStartingRootHash, prestateProvider) + return NewGamePlayer(ctx, systemClock, l1Clock, logger, m, dir, game.Proxy, txSender, contract, syncValidator, []Validator{prestateValidator, genesisValidator}, creator, l1HeaderSource, selective, claimants) + } + err := registerOracle(ctx, m, oracles, gameFactory, caller, gameType) + if err != nil { + return err + } + registry.RegisterGameType(gameType, playerCreator) + + contractCreator := func(game types.GameMetadata) (claims.BondContract, error) { + return contracts.NewFaultDisputeGameContract(m, game.Proxy, caller) + } + registry.RegisterBondContract(gameType, contractCreator) + return nil +} + func registerCannon( gameType uint32, registry Registry, diff --git a/op-challenger/game/fault/trace/asterisc/asterisc_memory.go b/op-challenger/game/fault/trace/asterisc/asterisc_memory.go new file mode 100644 index 00000000000..04e2d0a0c87 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/asterisc_memory.go @@ -0,0 +1,137 @@ +package asterisc + +import ( + "encoding/json" + "fmt" + "math/bits" + "sort" + + "github.com/ethereum/go-ethereum/crypto" +) + +// Duplicated fromhttps://github.com/ethereum-optimism/asterisc/blob/4c4705809b4adbb854d265f76ace719b08c732e6/rvgo/fast/memory.go + +type Memory struct { + // generalized index -> merkle root or nil if invalidated + nodes map[uint64]*[32]byte + + // pageIndex -> cached page + pages map[uint64]*CachedPage + + // Note: since we don't de-alloc pages, we don't do ref-counting. + // Once a page exists, it doesn't leave memory + + // two caches: we often read instructions from one page, and do memory things with another page. + // this prevents map lookups each instruction + lastPageKeys [2]uint64 + lastPage [2]*CachedPage +} + +type pageEntry struct { + Index uint64 `json:"index"` + Data *Page `json:"data"` +} + +func NewMemory() *Memory { + return &Memory{ + nodes: make(map[uint64]*[32]byte), + pages: make(map[uint64]*CachedPage), + lastPageKeys: [2]uint64{^uint64(0), ^uint64(0)}, // default to invalid keys, to not match any pages + } +} + +func HashPair(left, right [32]byte) [32]byte { + out := crypto.Keccak256Hash(left[:], right[:]) + //fmt.Printf("0x%x 0x%x -> 0x%x\n", left, right, out) + return out +} + +var zeroHashes = func() [256][32]byte { + // empty parts of the tree are all zero. Precompute the hash of each full-zero range sub-tree level. + var out [256][32]byte + for i := 1; i < 256; i++ { + out[i] = HashPair(out[i-1], out[i-1]) + } + return out +}() + +func (m *Memory) MarshalJSON() ([]byte, error) { + pages := make([]pageEntry, 0, len(m.pages)) + for k, p := range m.pages { + pages = append(pages, pageEntry{ + Index: k, + Data: p.Data, + }) + } + sort.Slice(pages, func(i, j int) bool { + return pages[i].Index < pages[j].Index + }) + return json.Marshal(pages) +} + +func (m *Memory) UnmarshalJSON(data []byte) error { + var pages []pageEntry + if err := json.Unmarshal(data, &pages); err != nil { + return err + } + m.nodes = make(map[uint64]*[32]byte) + m.pages = make(map[uint64]*CachedPage) + m.lastPageKeys = [2]uint64{^uint64(0), ^uint64(0)} + m.lastPage = [2]*CachedPage{nil, nil} + for i, p := range pages { + if _, ok := m.pages[p.Index]; ok { + return fmt.Errorf("cannot load duplicate page, entry %d, page index %d", i, p.Index) + } + m.AllocPage(p.Index).Data = p.Data + } + return nil +} + +func (m *Memory) AllocPage(pageIndex uint64) *CachedPage { + p := &CachedPage{Data: new(Page)} + m.pages[pageIndex] = p + // make nodes to root + k := (1 << PageKeySize) | uint64(pageIndex) + for k > 0 { + m.nodes[k] = nil + k >>= 1 + } + return p +} + +func (m *Memory) MerkleRoot() [32]byte { + return m.MerkleizeSubtree(1) +} + +func (m *Memory) MerkleizeSubtree(gindex uint64) [32]byte { + l := uint64(bits.Len64(gindex)) + if l > ProofLen { + panic("gindex too deep") + } + if l > PageKeySize { + depthIntoPage := l - 1 - PageKeySize + pageIndex := (gindex >> depthIntoPage) & PageKeyMask + if p, ok := m.pages[uint64(pageIndex)]; ok { + pageGindex := (1 << depthIntoPage) | (gindex & ((1 << depthIntoPage) - 1)) + return p.MerkleizeSubtree(pageGindex) + } else { + return zeroHashes[64-5+1-l] // page does not exist + } + } + if l > PageKeySize+1 { + panic("cannot jump into intermediate node of page") + } + n, ok := m.nodes[gindex] + if !ok { + // if the node doesn't exist, the whole sub-tree is zeroed + return zeroHashes[64-5+1-l] + } + if n != nil { + return *n + } + left := m.MerkleizeSubtree(gindex << 1) + right := m.MerkleizeSubtree((gindex << 1) | 1) + r := HashPair(left, right) + m.nodes[gindex] = &r + return r +} diff --git a/op-challenger/game/fault/trace/asterisc/asterisc_page.go b/op-challenger/game/fault/trace/asterisc/asterisc_page.go new file mode 100644 index 00000000000..80c32bc183e --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/asterisc_page.go @@ -0,0 +1,82 @@ +package asterisc + +import ( + "encoding/hex" + "fmt" + + "github.com/ethereum/go-ethereum/crypto" +) + +// Duplicated from https://github.com/ethereum-optimism/asterisc/blob/4c4705809b4adbb854d265f76ace719b08c732e6/rvgo/fast/page.go + +const ( + PageAddrSize = 12 + PageKeySize = 64 - PageAddrSize + PageSize = 1 << PageAddrSize + PageAddrMask = PageSize - 1 + MaxPageCount = 1 << PageKeySize + PageKeyMask = MaxPageCount - 1 + ProofLen = 64 - 4 +) + +type Page [PageSize]byte + +func (p *Page) MarshalText() ([]byte, error) { + dst := make([]byte, hex.EncodedLen(len(p))) + hex.Encode(dst, p[:]) + return dst, nil +} + +func (p *Page) UnmarshalText(dat []byte) error { + if len(dat) != PageSize*2 { + return fmt.Errorf("expected %d hex chars, but got %d", PageSize*2, len(dat)) + } + _, err := hex.Decode(p[:], dat) + return err +} + +type CachedPage struct { + Data *Page + // intermediate nodes only + Cache [PageSize / 32][32]byte + // true if the intermediate node is valid + Ok [PageSize / 32]bool +} + +func (p *CachedPage) MerkleRoot() [32]byte { + // hash the bottom layer + for i := uint64(0); i < PageSize; i += 64 { + j := PageSize/32/2 + i/64 + if p.Ok[j] { + continue + } + p.Cache[j] = crypto.Keccak256Hash(p.Data[i : i+64]) + //fmt.Printf("0x%x 0x%x -> 0x%x\n", p.Data[i:i+32], p.Data[i+32:i+64], p.Cache[j]) + p.Ok[j] = true + } + + // hash the cache layers + for i := PageSize/32 - 2; i > 0; i -= 2 { + j := i >> 1 + if p.Ok[j] { + continue + } + p.Cache[j] = HashPair(p.Cache[i], p.Cache[i+1]) + p.Ok[j] = true + } + + return p.Cache[1] +} + +func (p *CachedPage) MerkleizeSubtree(gindex uint64) [32]byte { + _ = p.MerkleRoot() // fill cache + if gindex >= PageSize/32 { + if gindex >= PageSize/32*2 { + panic("gindex too deep") + } + // it's pointing to a bottom node + nodeIndex := gindex & (PageAddrMask >> 5) + return *(*[32]byte)(p.Data[nodeIndex*32 : nodeIndex*32+32]) + } + return p.Cache[gindex] +} diff --git a/op-challenger/game/fault/trace/asterisc/asterisc_state.go b/op-challenger/game/fault/trace/asterisc/asterisc_state.go new file mode 100644 index 00000000000..3d6856c3195 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/asterisc_state.go @@ -0,0 +1,113 @@ +package asterisc + +import ( + "encoding/binary" + "encoding/json" + "fmt" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm" + "github.com/ethereum-optimism/optimism/op-service/ioutil" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" +) + +// Duplicated from https://github.com/ethereum-optimism/asterisc/blob/4c4705809b4adbb854d265f76ace719b08c732e6/rvgo/fast/state.go +type VMState struct { + Memory *Memory `json:"memory"` + + PreimageKey [32]byte `json:"preimageKey"` + PreimageOffset uint64 `json:"preimageOffset"` + + PC uint64 `json:"pc"` + + //0xF14: mhartid - riscv tests use this. Always hart 0, no parallelism supported + //CSR [4096]uint64 // 12 bit addressing space + + ExitCode uint8 `json:"exit"` + Exited bool `json:"exited"` + + Step uint64 `json:"step"` + + Heap uint64 `json:"heap"` // for mmap to keep allocating new anon memory + + LoadReservation uint64 `json:"loadReservation"` + + Registers [32]uint64 `json:"registers"` + + // LastHint is optional metadata, and not part of the VM state itself. + // It is used to remember the last pre-image hint, + // so a VM can start from any state without fetching prior pre-images, + // and instead just repeat the last hint on setup, + // to make sure pre-image requests can be served. + // The first 4 bytes are a uin32 length prefix. + // Warning: the hint MAY NOT BE COMPLETE. I.e. this is buffered, + // and should only be read when len(LastHint) > 4 && uint32(LastHint[:4]) >= len(LastHint[4:]) + LastHint hexutil.Bytes `json:"lastHint,omitempty"` +} + +type StateWitness []byte + +func (state *VMState) EncodeWitness() StateWitness { + out := make([]byte, 0) + memRoot := state.Memory.MerkleRoot() + out = append(out, memRoot[:]...) + out = append(out, state.PreimageKey[:]...) + out = binary.BigEndian.AppendUint64(out, state.PreimageOffset) + out = binary.BigEndian.AppendUint64(out, state.PC) + out = append(out, state.ExitCode) + if state.Exited { + out = append(out, 1) + } else { + out = append(out, 0) + } + out = binary.BigEndian.AppendUint64(out, state.Step) + out = binary.BigEndian.AppendUint64(out, state.Heap) + out = binary.BigEndian.AppendUint64(out, state.LoadReservation) + for _, r := range state.Registers { + out = binary.BigEndian.AppendUint64(out, r) + } + return out +} + +func vmStatus(exited bool, exitCode uint8) uint8 { + if !exited { + return mipsevm.VMStatusUnfinished + } + switch exitCode { + case 0: + return mipsevm.VMStatusValid + case 1: + return mipsevm.VMStatusInvalid + default: + return mipsevm.VMStatusPanic + } +} + +func (sw StateWitness) StateHash() (common.Hash, error) { + offset := 32 + 32 + 8 + 8 // mem-root, preimage-key, preimage-offset, PC + if len(sw) <= offset+1 { + return common.Hash{}, fmt.Errorf("state must at least be %d bytes, but got %d", offset, len(sw)) + } + + hash := crypto.Keccak256Hash(sw) + exitCode := sw[offset] + exited := sw[offset+1] + status := vmStatus(exited == 1, exitCode) + hash[0] = status + return hash, nil +} + +func parseState(path string) (*VMState, error) { + file, err := ioutil.OpenDecompressed(path) + if err != nil { + return nil, fmt.Errorf("cannot open state file (%v): %w", path, err) + } + defer file.Close() + var state VMState + err = json.NewDecoder(file).Decode(&state) + if err != nil { + return nil, fmt.Errorf("invalid asterisc VM state (%v): %w", path, err) + } + return &state, nil +} diff --git a/op-challenger/game/fault/trace/asterisc/asterisc_state_test.go b/op-challenger/game/fault/trace/asterisc/asterisc_state_test.go new file mode 100644 index 00000000000..1ca30e0d7b3 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/asterisc_state_test.go @@ -0,0 +1,49 @@ +package asterisc + +import ( + "compress/gzip" + _ "embed" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +//go:embed test_data/state.json +var testState []byte + +func TestLoadState(t *testing.T) { + t.Run("Uncompressed", func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "state.json") + require.NoError(t, os.WriteFile(path, testState, 0644)) + + state, err := parseState(path) + require.NoError(t, err) + + var expected VMState + require.NoError(t, json.Unmarshal(testState, &expected)) + require.Equal(t, &expected, state) + }) + + t.Run("Gzipped", func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "state.json.gz") + f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + require.NoError(t, err) + defer f.Close() + writer := gzip.NewWriter(f) + _, err = writer.Write(testState) + require.NoError(t, err) + require.NoError(t, writer.Close()) + + state, err := parseState(path) + require.NoError(t, err) + + var expected VMState + require.NoError(t, json.Unmarshal(testState, &expected)) + require.Equal(t, &expected, state) + }) +} diff --git a/op-challenger/game/fault/trace/asterisc/executor.go b/op-challenger/game/fault/trace/asterisc/executor.go new file mode 100644 index 00000000000..94a05731f60 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/executor.go @@ -0,0 +1,198 @@ +package asterisc + +import ( + "context" + "errors" + "fmt" + "math" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum/go-ethereum/log" +) + +const ( + snapsDir = "snapshots" + preimagesDir = "preimages" + finalState = "final.json.gz" +) + +var snapshotNameRegexp = regexp.MustCompile(`^[0-9]+\.json.gz$`) + +type snapshotSelect func(logger log.Logger, dir string, absolutePreState string, i uint64) (string, error) +type cmdExecutor func(ctx context.Context, l log.Logger, binary string, args ...string) error + +type Executor struct { + logger log.Logger + metrics AsteriscMetricer + l1 string + l1Beacon string + l2 string + inputs cannon.LocalGameInputs + asterisc string + server string + network string + rollupConfig string + l2Genesis string + absolutePreState string + snapshotFreq uint + infoFreq uint + selectSnapshot snapshotSelect + cmdExecutor cmdExecutor +} + +func NewExecutor(logger log.Logger, m AsteriscMetricer, cfg *config.Config, inputs cannon.LocalGameInputs) *Executor { + return &Executor{ + logger: logger, + metrics: m, + l1: cfg.L1EthRpc, + l1Beacon: cfg.L1Beacon, + l2: cfg.AsteriscL2, + inputs: inputs, + asterisc: cfg.AsteriscBin, + server: cfg.AsteriscServer, + network: cfg.AsteriscNetwork, + rollupConfig: cfg.AsteriscRollupConfigPath, + l2Genesis: cfg.AsteriscL2GenesisPath, + absolutePreState: cfg.AsteriscAbsolutePreState, + snapshotFreq: cfg.AsteriscSnapshotFreq, + infoFreq: cfg.AsteriscInfoFreq, + selectSnapshot: findStartingSnapshot, + cmdExecutor: runCmd, + } +} + +// GenerateProof executes asterisc to generate a proof at the specified trace index. +// The proof is stored at the specified directory. +func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) error { + return e.generateProof(ctx, dir, i, i) +} + +// generateProofOrUntilPreimageRead executes asterisc to generate a proof at the specified trace index, +// or until a non-local preimage read is encountered if untilPreimageRead is true. +// The proof is stored at the specified directory. +func (e *Executor) generateProof(ctx context.Context, dir string, begin uint64, end uint64, extraAsteriscArgs ...string) error { + snapshotDir := filepath.Join(dir, snapsDir) + start, err := e.selectSnapshot(e.logger, snapshotDir, e.absolutePreState, begin) + if err != nil { + return fmt.Errorf("find starting snapshot: %w", err) + } + proofDir := filepath.Join(dir, proofsDir) + dataDir := preimageDir(dir) + lastGeneratedState := filepath.Join(dir, finalState) + args := []string{ + "run", + "--input", start, + "--output", lastGeneratedState, + "--meta", "", + "--info-at", "%" + strconv.FormatUint(uint64(e.infoFreq), 10), + "--proof-at", "=" + strconv.FormatUint(end, 10), + "--proof-fmt", filepath.Join(proofDir, "%d.json.gz"), + "--snapshot-at", "%" + strconv.FormatUint(uint64(e.snapshotFreq), 10), + "--snapshot-fmt", filepath.Join(snapshotDir, "%d.json.gz"), + } + if end < math.MaxUint64 { + args = append(args, "--stop-at", "="+strconv.FormatUint(end+1, 10)) + } + args = append(args, extraAsteriscArgs...) + args = append(args, + "--", + e.server, "--server", + "--l1", e.l1, + "--l1.beacon", e.l1Beacon, + "--l2", e.l2, + "--datadir", dataDir, + "--l1.head", e.inputs.L1Head.Hex(), + "--l2.head", e.inputs.L2Head.Hex(), + "--l2.outputroot", e.inputs.L2OutputRoot.Hex(), + "--l2.claim", e.inputs.L2Claim.Hex(), + "--l2.blocknumber", e.inputs.L2BlockNumber.Text(10), + ) + if e.network != "" { + args = append(args, "--network", e.network) + } + if e.rollupConfig != "" { + args = append(args, "--rollup.config", e.rollupConfig) + } + if e.l2Genesis != "" { + args = append(args, "--l2.genesis", e.l2Genesis) + } + + if err := os.MkdirAll(snapshotDir, 0755); err != nil { + return fmt.Errorf("could not create snapshot directory %v: %w", snapshotDir, err) + } + if err := os.MkdirAll(dataDir, 0755); err != nil { + return fmt.Errorf("could not create preimage cache directory %v: %w", dataDir, err) + } + if err := os.MkdirAll(proofDir, 0755); err != nil { + return fmt.Errorf("could not create proofs directory %v: %w", proofDir, err) + } + e.logger.Info("Generating trace", "proof", end, "cmd", e.asterisc, "args", strings.Join(args, ", ")) + execStart := time.Now() + err = e.cmdExecutor(ctx, e.logger.New("proof", end), e.asterisc, args...) + e.metrics.RecordAsteriscExecutionTime(time.Since(execStart).Seconds()) + return err +} + +func preimageDir(dir string) string { + return filepath.Join(dir, preimagesDir) +} + +func runCmd(ctx context.Context, l log.Logger, binary string, args ...string) error { + cmd := exec.CommandContext(ctx, binary, args...) + stdOut := oplog.NewWriter(l, log.LevelInfo) + defer stdOut.Close() + // Keep stdErr at info level because cannon uses stderr for progress messages + stdErr := oplog.NewWriter(l, log.LevelInfo) + defer stdErr.Close() + cmd.Stdout = stdOut + cmd.Stderr = stdErr + return cmd.Run() +} + +// findStartingSnapshot finds the closest snapshot before the specified traceIndex in snapDir. +// If no suitable snapshot can be found it returns absolutePreState. +func findStartingSnapshot(logger log.Logger, snapDir string, absolutePreState string, traceIndex uint64) (string, error) { + // Find the closest snapshot to start from + entries, err := os.ReadDir(snapDir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return absolutePreState, nil + } + return "", fmt.Errorf("list snapshots in %v: %w", snapDir, err) + } + bestSnap := uint64(0) + for _, entry := range entries { + if entry.IsDir() { + logger.Warn("Unexpected directory in snapshots dir", "parent", snapDir, "child", entry.Name()) + continue + } + name := entry.Name() + if !snapshotNameRegexp.MatchString(name) { + logger.Warn("Unexpected file in snapshots dir", "parent", snapDir, "child", entry.Name()) + continue + } + index, err := strconv.ParseUint(name[0:len(name)-len(".json.gz")], 10, 64) + if err != nil { + logger.Error("Unable to parse trace index of snapshot file", "parent", snapDir, "child", entry.Name()) + continue + } + if index > bestSnap && index < traceIndex { + bestSnap = index + } + } + if bestSnap == 0 { + return absolutePreState, nil + } + startFrom := fmt.Sprintf("%v/%v.json.gz", snapDir, bestSnap) + + return startFrom, nil +} diff --git a/op-challenger/game/fault/trace/asterisc/executor_test.go b/op-challenger/game/fault/trace/asterisc/executor_test.go new file mode 100644 index 00000000000..5b05e4dd11d --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/executor_test.go @@ -0,0 +1,227 @@ +package asterisc + +import ( + "context" + "fmt" + "math" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" + "github.com/ethereum-optimism/optimism/op-challenger/metrics" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +const execTestAsteriscPrestate = "/foo/pre.json" + +func TestGenerateProof(t *testing.T) { + input := "starting.json" + tempDir := t.TempDir() + dir := filepath.Join(tempDir, "gameDir") + cfg := config.NewConfig(common.Address{0xbb}, "http://localhost:8888", "http://localhost:9000", tempDir, config.TraceTypeAsterisc) + cfg.AsteriscAbsolutePreState = "pre.json" + cfg.AsteriscBin = "./bin/asterisc" + cfg.AsteriscServer = "./bin/op-program" + cfg.AsteriscL2 = "http://localhost:9999" + cfg.AsteriscSnapshotFreq = 500 + cfg.AsteriscInfoFreq = 900 + + inputs := cannon.LocalGameInputs{ + L1Head: common.Hash{0x11}, + L2Head: common.Hash{0x22}, + L2OutputRoot: common.Hash{0x33}, + L2Claim: common.Hash{0x44}, + L2BlockNumber: big.NewInt(3333), + } + captureExec := func(t *testing.T, cfg config.Config, proofAt uint64) (string, string, map[string]string) { + m := &asteriscDurationMetrics{} + executor := NewExecutor(testlog.Logger(t, log.LevelInfo), m, &cfg, inputs) + executor.selectSnapshot = func(logger log.Logger, dir string, absolutePreState string, i uint64) (string, error) { + return input, nil + } + var binary string + var subcommand string + args := make(map[string]string) + executor.cmdExecutor = func(ctx context.Context, l log.Logger, b string, a ...string) error { + binary = b + subcommand = a[0] + for i := 1; i < len(a); { + if a[i] == "--" { + // Skip over the divider between asterisc and server program + i += 1 + continue + } + args[a[i]] = a[i+1] + i += 2 + } + return nil + } + err := executor.GenerateProof(context.Background(), dir, proofAt) + require.NoError(t, err) + require.Equal(t, 1, m.executionTimeRecordCount, "Should record asterisc execution time") + return binary, subcommand, args + } + + t.Run("Network", func(t *testing.T) { + cfg.AsteriscNetwork = "mainnet" + cfg.AsteriscRollupConfigPath = "" + cfg.AsteriscL2GenesisPath = "" + binary, subcommand, args := captureExec(t, cfg, 150_000_000) + require.DirExists(t, filepath.Join(dir, preimagesDir)) + require.DirExists(t, filepath.Join(dir, proofsDir)) + require.DirExists(t, filepath.Join(dir, snapsDir)) + require.Equal(t, cfg.AsteriscBin, binary) + require.Equal(t, "run", subcommand) + require.Equal(t, input, args["--input"]) + require.Contains(t, args, "--meta") + require.Equal(t, "", args["--meta"]) + require.Equal(t, filepath.Join(dir, finalState), args["--output"]) + require.Equal(t, "=150000000", args["--proof-at"]) + require.Equal(t, "=150000001", args["--stop-at"]) + require.Equal(t, "%500", args["--snapshot-at"]) + require.Equal(t, "%900", args["--info-at"]) + // Slight quirk of how we pair off args + // The server binary winds up as the key and the first arg --server as the value which has no value + // Then everything else pairs off correctly again + require.Equal(t, "--server", args[cfg.AsteriscServer]) + require.Equal(t, cfg.L1EthRpc, args["--l1"]) + require.Equal(t, cfg.L1Beacon, args["--l1.beacon"]) + require.Equal(t, cfg.AsteriscL2, args["--l2"]) + require.Equal(t, filepath.Join(dir, preimagesDir), args["--datadir"]) + require.Equal(t, filepath.Join(dir, proofsDir, "%d.json.gz"), args["--proof-fmt"]) + require.Equal(t, filepath.Join(dir, snapsDir, "%d.json.gz"), args["--snapshot-fmt"]) + require.Equal(t, cfg.AsteriscNetwork, args["--network"]) + require.NotContains(t, args, "--rollup.config") + require.NotContains(t, args, "--l2.genesis") + + // Local game inputs + require.Equal(t, inputs.L1Head.Hex(), args["--l1.head"]) + require.Equal(t, inputs.L2Head.Hex(), args["--l2.head"]) + require.Equal(t, inputs.L2OutputRoot.Hex(), args["--l2.outputroot"]) + require.Equal(t, inputs.L2Claim.Hex(), args["--l2.claim"]) + require.Equal(t, "3333", args["--l2.blocknumber"]) + }) + + t.Run("RollupAndGenesis", func(t *testing.T) { + cfg.AsteriscNetwork = "" + cfg.AsteriscRollupConfigPath = "rollup.json" + cfg.AsteriscL2GenesisPath = "genesis.json" + _, _, args := captureExec(t, cfg, 150_000_000) + require.NotContains(t, args, "--network") + require.Equal(t, cfg.AsteriscRollupConfigPath, args["--rollup.config"]) + require.Equal(t, cfg.AsteriscL2GenesisPath, args["--l2.genesis"]) + }) + + t.Run("NoStopAtWhenProofIsMaxUInt", func(t *testing.T) { + cfg.AsteriscNetwork = "mainnet" + cfg.AsteriscRollupConfigPath = "rollup.json" + cfg.AsteriscL2GenesisPath = "genesis.json" + _, _, args := captureExec(t, cfg, math.MaxUint64) + // stop-at would need to be one more than the proof step which would overflow back to 0 + // so expect that it will be omitted. We'll ultimately want asterisc to execute until the program exits. + require.NotContains(t, args, "--stop-at") + }) +} + +func TestRunCmdLogsOutput(t *testing.T) { + bin := "/bin/echo" + if _, err := os.Stat(bin); err != nil { + t.Skip(bin, " not available", err) + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + logger, logs := testlog.CaptureLogger(t, log.LevelInfo) + err := runCmd(ctx, logger, bin, "Hello World") + require.NoError(t, err) + levelFilter := testlog.NewLevelFilter(log.LevelInfo) + msgFilter := testlog.NewMessageFilter("Hello World") + require.NotNil(t, logs.FindLog(levelFilter, msgFilter)) +} + +func TestFindStartingSnapshot(t *testing.T) { + logger := testlog.Logger(t, log.LevelInfo) + + withSnapshots := func(t *testing.T, files ...string) string { + dir := t.TempDir() + for _, file := range files { + require.NoError(t, os.WriteFile(fmt.Sprintf("%v/%v", dir, file), nil, 0o644)) + } + return dir + } + + t.Run("UsePrestateWhenSnapshotsDirDoesNotExist", func(t *testing.T) { + dir := t.TempDir() + snapshot, err := findStartingSnapshot(logger, filepath.Join(dir, "doesNotExist"), execTestAsteriscPrestate, 1200) + require.NoError(t, err) + require.Equal(t, execTestAsteriscPrestate, snapshot) + }) + + t.Run("UsePrestateWhenSnapshotsDirEmpty", func(t *testing.T) { + dir := withSnapshots(t) + snapshot, err := findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 1200) + require.NoError(t, err) + require.Equal(t, execTestAsteriscPrestate, snapshot) + }) + + t.Run("UsePrestateWhenNoSnapshotBeforeTraceIndex", func(t *testing.T) { + dir := withSnapshots(t, "100.json", "200.json") + snapshot, err := findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 99) + require.NoError(t, err) + require.Equal(t, execTestAsteriscPrestate, snapshot) + + snapshot, err = findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 100) + require.NoError(t, err) + require.Equal(t, execTestAsteriscPrestate, snapshot) + }) + + t.Run("UseClosestAvailableSnapshot", func(t *testing.T) { + dir := withSnapshots(t, "100.json.gz", "123.json.gz", "250.json.gz") + + snapshot, err := findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 101) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, "100.json.gz"), snapshot) + + snapshot, err = findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 123) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, "100.json.gz"), snapshot) + + snapshot, err = findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 124) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, "123.json.gz"), snapshot) + + snapshot, err = findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 256) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, "250.json.gz"), snapshot) + }) + + t.Run("IgnoreDirectories", func(t *testing.T) { + dir := withSnapshots(t, "100.json.gz") + require.NoError(t, os.Mkdir(filepath.Join(dir, "120.json.gz"), 0o777)) + snapshot, err := findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 150) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, "100.json.gz"), snapshot) + }) + + t.Run("IgnoreUnexpectedFiles", func(t *testing.T) { + dir := withSnapshots(t, ".file", "100.json.gz", "foo", "bar.json.gz") + snapshot, err := findStartingSnapshot(logger, dir, execTestAsteriscPrestate, 150) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, "100.json.gz"), snapshot) + }) +} + +type asteriscDurationMetrics struct { + metrics.NoopMetricsImpl + executionTimeRecordCount int +} + +func (c *asteriscDurationMetrics) RecordAsteriscExecutionTime(_ float64) { + c.executionTimeRecordCount++ +} diff --git a/op-challenger/game/fault/trace/asterisc/prestate.go b/op-challenger/game/fault/trace/asterisc/prestate.go new file mode 100644 index 00000000000..81fb699f1f2 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/prestate.go @@ -0,0 +1,45 @@ +package asterisc + +import ( + "context" + "fmt" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum/go-ethereum/common" +) + +var _ types.PrestateProvider = (*AsteriscPreStateProvider)(nil) + +type AsteriscPreStateProvider struct { + prestate string + + prestateCommitment common.Hash +} + +func NewPrestateProvider(prestate string) *AsteriscPreStateProvider { + return &AsteriscPreStateProvider{prestate: prestate} +} + +func (p *AsteriscPreStateProvider) absolutePreState() ([]byte, error) { + state, err := parseState(p.prestate) + if err != nil { + return nil, fmt.Errorf("cannot load absolute pre-state: %w", err) + } + return state.EncodeWitness(), nil +} + +func (p *AsteriscPreStateProvider) AbsolutePreStateCommitment(_ context.Context) (common.Hash, error) { + if p.prestateCommitment != (common.Hash{}) { + return p.prestateCommitment, nil + } + state, err := p.absolutePreState() + if err != nil { + return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err) + } + hash, err := StateWitness(state).StateHash() + if err != nil { + return common.Hash{}, fmt.Errorf("cannot hash absolute pre-state: %w", err) + } + p.prestateCommitment = hash + return hash, nil +} diff --git a/op-challenger/game/fault/trace/asterisc/prestate_test.go b/op-challenger/game/fault/trace/asterisc/prestate_test.go new file mode 100644 index 00000000000..74dc3e243ef --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/prestate_test.go @@ -0,0 +1,82 @@ +package asterisc + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func newAsteriscPrestateProvider(dataDir string, prestate string) *AsteriscPreStateProvider { + return &AsteriscPreStateProvider{ + prestate: filepath.Join(dataDir, prestate), + } +} + +func TestAbsolutePreStateCommitment(t *testing.T) { + dataDir := t.TempDir() + + prestate := "state.json" + + t.Run("StateUnavailable", func(t *testing.T) { + provider := newAsteriscPrestateProvider("/dir/does/not/exist", prestate) + _, err := provider.AbsolutePreStateCommitment(context.Background()) + require.ErrorIs(t, err, os.ErrNotExist) + }) + + t.Run("InvalidStateFile", func(t *testing.T) { + setupPreState(t, dataDir, "invalid.json") + provider := newAsteriscPrestateProvider(dataDir, prestate) + _, err := provider.AbsolutePreStateCommitment(context.Background()) + require.ErrorContains(t, err, "invalid asterisc VM state") + }) + + t.Run("ExpectedAbsolutePreState", func(t *testing.T) { + setupPreState(t, dataDir, "state.json") + provider := newAsteriscPrestateProvider(dataDir, prestate) + actual, err := provider.AbsolutePreStateCommitment(context.Background()) + require.NoError(t, err) + state := VMState{ + Memory: NewMemory(), + PreimageKey: common.HexToHash("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), + PreimageOffset: 0, + PC: 0, + ExitCode: 0, + Exited: false, + Step: 0, + Heap: 0, + LoadReservation: 0, + Registers: [32]uint64{}, + } + expected, err := state.EncodeWitness().StateHash() + require.NoError(t, err) + require.Equal(t, expected, actual) + }) + + t.Run("CacheAbsolutePreState", func(t *testing.T) { + setupPreState(t, dataDir, "state.json") + provider := newAsteriscPrestateProvider(dataDir, prestate) + first, err := provider.AbsolutePreStateCommitment(context.Background()) + require.NoError(t, err) + + // Remove the prestate from disk + require.NoError(t, os.Remove(provider.prestate)) + + // Value should still be available from cache + cached, err := provider.AbsolutePreStateCommitment(context.Background()) + require.NoError(t, err) + require.Equal(t, first, cached) + }) +} + +func setupPreState(t *testing.T, dataDir string, filename string) { + srcDir := filepath.Join("test_data") + path := filepath.Join(srcDir, filename) + file, err := testData.ReadFile(path) + require.NoErrorf(t, err, "reading %v", path) + err = os.WriteFile(filepath.Join(dataDir, "state.json"), file, 0o644) + require.NoErrorf(t, err, "writing %v", path) +} diff --git a/op-challenger/game/fault/trace/asterisc/provider.go b/op-challenger/game/fault/trace/asterisc/provider.go new file mode 100644 index 00000000000..e96385c7f7f --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/provider.go @@ -0,0 +1,303 @@ +package asterisc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "os" + "path/filepath" + "strconv" + + "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + preimage "github.com/ethereum-optimism/optimism/op-preimage" + "github.com/ethereum-optimism/optimism/op-program/host/kvstore" + "github.com/ethereum-optimism/optimism/op-service/ioutil" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/log" +) + +const ( + proofsDir = "proofs" + diskStateCache = "state.json.gz" +) + +type AsteriscMetricer interface { + RecordAsteriscExecutionTime(t float64) +} + +type ProofGenerator interface { + // GenerateProof executes asterisc to generate a proof at the specified trace index in dataDir. + GenerateProof(ctx context.Context, dataDir string, proofAt uint64) error +} + +type AsteriscTraceProvider struct { + logger log.Logger + dir string + prestate string + generator ProofGenerator + gameDepth types.Depth + preimageLoader *cannon.PreimageLoader + + // lastStep stores the last step in the actual trace if known. 0 indicates unknown. + // Cached as an optimisation to avoid repeatedly attempting to execute beyond the end of the trace. + lastStep uint64 +} + +func NewTraceProvider(logger log.Logger, m AsteriscMetricer, cfg *config.Config, localInputs cannon.LocalGameInputs, dir string, gameDepth types.Depth) *AsteriscTraceProvider { + return &AsteriscTraceProvider{ + logger: logger, + dir: dir, + prestate: cfg.AsteriscAbsolutePreState, + generator: NewExecutor(logger, m, cfg, localInputs), + gameDepth: gameDepth, + preimageLoader: cannon.NewPreimageLoader(kvstore.NewDiskKV(preimageDir(dir)).Get), + } +} + +func (p *AsteriscTraceProvider) SetMaxDepth(gameDepth types.Depth) { + p.gameDepth = gameDepth +} + +func (p *AsteriscTraceProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) { + traceIndex := pos.TraceIndex(p.gameDepth) + if !traceIndex.IsUint64() { + return common.Hash{}, errors.New("trace index out of bounds") + } + proof, err := p.loadProof(ctx, traceIndex.Uint64()) + if err != nil { + return common.Hash{}, err + } + value := proof.ClaimValue + + if value == (common.Hash{}) { + return common.Hash{}, errors.New("proof missing post hash") + } + return value, nil +} + +func (p *AsteriscTraceProvider) GetStepData(ctx context.Context, pos types.Position) ([]byte, []byte, *types.PreimageOracleData, error) { + traceIndex := pos.TraceIndex(p.gameDepth) + if !traceIndex.IsUint64() { + return nil, nil, nil, errors.New("trace index out of bounds") + } + proof, err := p.loadProof(ctx, traceIndex.Uint64()) + if err != nil { + return nil, nil, nil, err + } + value := ([]byte)(proof.StateData) + if len(value) == 0 { + return nil, nil, nil, errors.New("proof missing state data") + } + data := ([]byte)(proof.ProofData) + if data == nil { + return nil, nil, nil, errors.New("proof missing proof data") + } + oracleData, err := p.preimageLoader.LoadPreimage(proof) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to load preimage: %w", err) + } + return value, data, oracleData, nil +} + +func (p *AsteriscTraceProvider) absolutePreState() ([]byte, error) { + state, err := parseState(p.prestate) + if err != nil { + return nil, fmt.Errorf("cannot load absolute pre-state: %w", err) + } + return state.EncodeWitness(), nil +} + +func (p *AsteriscTraceProvider) AbsolutePreStateCommitment(_ context.Context) (common.Hash, error) { + state, err := p.absolutePreState() + if err != nil { + return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err) + } + hash, err := StateWitness(state).StateHash() + if err != nil { + return common.Hash{}, fmt.Errorf("cannot hash absolute pre-state: %w", err) + } + return hash, nil +} + +// loadProof will attempt to load or generate the proof data at the specified index +// If the requested index is beyond the end of the actual trace it is extended with no-op instructions. +func (p *AsteriscTraceProvider) loadProof(ctx context.Context, i uint64) (*cannon.ProofData, error) { + // Attempt to read the last step from disk cache + if p.lastStep == 0 { + step, err := readLastStep(p.dir) + if err != nil { + p.logger.Warn("Failed to read last step from disk cache", "err", err) + } else { + p.lastStep = step + } + } + // If the last step is tracked, set i to the last step to generate or load the final proof + if p.lastStep != 0 && i > p.lastStep { + i = p.lastStep + } + path := filepath.Join(p.dir, proofsDir, fmt.Sprintf("%d.json.gz", i)) + file, err := ioutil.OpenDecompressed(path) + if errors.Is(err, os.ErrNotExist) { + if err := p.generator.GenerateProof(ctx, p.dir, i); err != nil { + return nil, fmt.Errorf("generate asterisc trace with proof at %v: %w", i, err) + } + // Try opening the file again now and it should exist. + file, err = ioutil.OpenDecompressed(path) + if errors.Is(err, os.ErrNotExist) { + // Expected proof wasn't generated, check if we reached the end of execution + state, err := p.finalState() + if err != nil { + return nil, err + } + if state.Exited && state.Step <= i { + p.logger.Warn("Requested proof was after the program exited", "proof", i, "last", state.Step) + // The final instruction has already been applied to this state, so the last step we can execute + // is one before its Step value. + p.lastStep = state.Step - 1 + // Extend the trace out to the full length using a no-op instruction that doesn't change any state + // No execution is done, so no proof-data or oracle values are required. + witness := state.EncodeWitness() + witnessHash, err := StateWitness(witness).StateHash() + if err != nil { + return nil, fmt.Errorf("cannot hash witness: %w", err) + } + proof := &cannon.ProofData{ + ClaimValue: witnessHash, + StateData: hexutil.Bytes(witness), + ProofData: []byte{}, + OracleKey: nil, + OracleValue: nil, + OracleOffset: 0, + } + if err := writeLastStep(p.dir, proof, p.lastStep); err != nil { + p.logger.Warn("Failed to write last step to disk cache", "step", p.lastStep) + } + return proof, nil + } else { + return nil, fmt.Errorf("expected proof not generated but final state was not exited, requested step %v, final state at step %v", i, state.Step) + } + } + } + if err != nil { + return nil, fmt.Errorf("cannot open proof file (%v): %w", path, err) + } + defer file.Close() + var proof cannon.ProofData + err = json.NewDecoder(file).Decode(&proof) + if err != nil { + return nil, fmt.Errorf("failed to read proof (%v): %w", path, err) + } + return &proof, nil +} + +func (c *AsteriscTraceProvider) finalState() (*VMState, error) { + state, err := parseState(filepath.Join(c.dir, finalState)) + if err != nil { + return nil, fmt.Errorf("cannot read final state: %w", err) + } + return state, nil +} + +type diskStateCacheObj struct { + Step uint64 `json:"step"` +} + +// readLastStep reads the tracked last step from disk. +func readLastStep(dir string) (uint64, error) { + state := diskStateCacheObj{} + file, err := ioutil.OpenDecompressed(filepath.Join(dir, diskStateCache)) + if err != nil { + return 0, err + } + defer file.Close() + err = json.NewDecoder(file).Decode(&state) + if err != nil { + return 0, err + } + return state.Step, nil +} + +// writeLastStep writes the last step and proof to disk as a persistent cache. +func writeLastStep(dir string, proof *cannon.ProofData, step uint64) error { + state := diskStateCacheObj{Step: step} + lastStepFile := filepath.Join(dir, diskStateCache) + if err := ioutil.WriteCompressedJson(lastStepFile, state); err != nil { + return fmt.Errorf("failed to write last step to %v: %w", lastStepFile, err) + } + if err := ioutil.WriteCompressedJson(filepath.Join(dir, proofsDir, fmt.Sprintf("%d.json.gz", step)), proof); err != nil { + return fmt.Errorf("failed to write proof: %w", err) + } + return nil +} + +// AsteriscTraceProviderForTest is a AsteriscTraceProvider that can find the step referencing the preimage read +// Only to be used for testing +type AsteriscTraceProviderForTest struct { + *AsteriscTraceProvider +} + +type preimageOpts []string + +type PreimageOpt func() preimageOpts + +func PreimageLoad(key preimage.Key, offset uint32) PreimageOpt { + return func() preimageOpts { + return []string{"--stop-at-preimage", fmt.Sprintf("%v@%v", common.Hash(key.PreimageKey()).Hex(), offset)} + } +} + +func FirstPreimageLoadOfType(preimageType string) PreimageOpt { + return func() preimageOpts { + return []string{"--stop-at-preimage-type", preimageType} + } +} + +func FirstKeccakPreimageLoad() PreimageOpt { + return FirstPreimageLoadOfType("keccak") +} + +func FirstPrecompilePreimageLoad() PreimageOpt { + return FirstPreimageLoadOfType("precompile") +} + +func PreimageLargerThan(size int) PreimageOpt { + return func() preimageOpts { + return []string{"--stop-at-preimage-larger-than", strconv.Itoa(size)} + } +} + +func NewTraceProviderForTest(logger log.Logger, m AsteriscMetricer, cfg *config.Config, localInputs cannon.LocalGameInputs, dir string, gameDepth types.Depth) *AsteriscTraceProviderForTest { + p := &AsteriscTraceProvider{ + logger: logger, + dir: dir, + prestate: cfg.AsteriscAbsolutePreState, + generator: NewExecutor(logger, m, cfg, localInputs), + gameDepth: gameDepth, + preimageLoader: cannon.NewPreimageLoader(kvstore.NewDiskKV(preimageDir(dir)).Get), + } + return &AsteriscTraceProviderForTest{p} +} + +func (p *AsteriscTraceProviderForTest) FindStep(ctx context.Context, start uint64, preimage PreimageOpt) (uint64, error) { + // Run asterisc to find the step that meets the preimage conditions + if err := p.generator.(*Executor).generateProof(ctx, p.dir, start, math.MaxUint64, preimage()...); err != nil { + return 0, fmt.Errorf("generate asterisc trace (until preimage read): %w", err) + } + // Load the step from the state asterisc finished with + state, err := p.finalState() + if err != nil { + return 0, fmt.Errorf("failed to load final state: %w", err) + } + // Check we didn't get to the end of the trace without finding the preimage read we were looking for + if state.Exited { + return 0, fmt.Errorf("preimage read not found: %w", io.EOF) + } + // The state is the post-state so the step we want to execute to read the preimage is step - 1. + return state.Step - 1, nil +} diff --git a/op-challenger/game/fault/trace/asterisc/provider_test.go b/op-challenger/game/fault/trace/asterisc/provider_test.go new file mode 100644 index 00000000000..e7ecd1d535b --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/provider_test.go @@ -0,0 +1,280 @@ +package asterisc + +import ( + "context" + "embed" + "encoding/json" + "fmt" + "math" + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum-optimism/optimism/op-service/ioutil" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +//go:embed test_data +var testData embed.FS + +func PositionFromTraceIndex(provider *AsteriscTraceProvider, idx *big.Int) types.Position { + return types.NewPosition(provider.gameDepth, idx) +} + +func TestGet(t *testing.T) { + dataDir, prestate := setupTestData(t) + t.Run("ExistingProof", func(t *testing.T) { + provider, generator := setupWithTestData(t, dataDir, prestate) + value, err := provider.Get(context.Background(), PositionFromTraceIndex(provider, common.Big0)) + require.NoError(t, err) + require.Equal(t, common.HexToHash("0x034689707b571db46b32c9e433def18e648f4e1fa9e5abd4012e7913031bfc10"), value) + require.Empty(t, generator.generated) + }) + + t.Run("ErrorsTraceIndexOutOfBounds", func(t *testing.T) { + provider, generator := setupWithTestData(t, dataDir, prestate) + largePosition := PositionFromTraceIndex(provider, new(big.Int).Mul(new(big.Int).SetUint64(math.MaxUint64), big.NewInt(2))) + _, err := provider.Get(context.Background(), largePosition) + require.ErrorContains(t, err, "trace index out of bounds") + require.Empty(t, generator.generated) + }) + + t.Run("ProofAfterEndOfTrace", func(t *testing.T) { + provider, generator := setupWithTestData(t, dataDir, prestate) + generator.finalState = &VMState{ + Memory: &Memory{}, + Step: 10, + Exited: true, + } + value, err := provider.Get(context.Background(), PositionFromTraceIndex(provider, big.NewInt(7000))) + require.NoError(t, err) + require.Contains(t, generator.generated, 7000, "should have tried to generate the proof") + stateHash, err := generator.finalState.EncodeWitness().StateHash() + require.NoError(t, err) + require.Equal(t, stateHash, value) + }) + + t.Run("MissingPostHash", func(t *testing.T) { + provider, generator := setupWithTestData(t, dataDir, prestate) + _, err := provider.Get(context.Background(), PositionFromTraceIndex(provider, big.NewInt(1))) + require.ErrorContains(t, err, "missing post hash") + require.Empty(t, generator.generated) + }) + + t.Run("IgnoreUnknownFields", func(t *testing.T) { + provider, generator := setupWithTestData(t, dataDir, prestate) + value, err := provider.Get(context.Background(), PositionFromTraceIndex(provider, big.NewInt(2))) + require.NoError(t, err) + expected := common.HexToHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + require.Equal(t, expected, value) + require.Empty(t, generator.generated) + }) +} + +func TestGetStepData(t *testing.T) { + t.Run("ExistingProof", func(t *testing.T) { + dataDir, prestate := setupTestData(t) + provider, generator := setupWithTestData(t, dataDir, prestate) + value, proof, data, err := provider.GetStepData(context.Background(), PositionFromTraceIndex(provider, common.Big0)) + require.NoError(t, err) + expected := common.FromHex("0x354cfaf28a5b60c3f64f22f9f171b64aa067f90c6de6c96f725f44c5cf9f8ac1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080e080000000000000000000000007f0000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + require.Equal(t, expected, value) + expectedProof := common.FromHex("0x000000000000000003350100930581006f00800100000000970f000067800f01000000000000000097c2ffff938282676780020000000000032581009308e0050e1893682c323d6695396f1122b3cb562af8c65cab19978c9246434fda0536c90ca1cfabf684ebce3ad9fbd54000a2b258f8d0e447c1bb6f7e97de47aadfc12cd7b6f466bfd024daa905886c5f638f4692d843709e6c1c0d9eb2e251c626d53d15e04b59735fe0781bc4357a4243fbc28e6981902a8c2669a2d6456f7a964423db5d1585da978861f8b84067654b29490275c82b54083ee09c82eb7aa9ae693911226bb8297ad82c0963ae943f22d0c6086f4f14437e4d1c87ceb17e68caf5eaec77f14b46225b417d2191ca7b49564c896836a95ad4e9c383bd1c8ff9d8e888c64fb3836daa9535e58372e9646b7b144219980a4389aca5da241c3ec11fbc9297bd7a94ac671ccec288604c23a0072b0c1ed069198959cacdc2574aff65b7eceffc391e21778a1775deceb3ec0990836df98d98a4f3f0dc854587230fbf59e4daa60e8240d74caf90f7e2cd014c1d5d707b2e44269d9a9caf133882fe1ebb2f4237f6282abe89639b357e9231418d0c41373229ae9edfa6815bec484cb79772c9e2a7d80912123558f79b539bb45d435f2a4446970f1e2123494740285cec3491b0a41a9fd7403bdc8cd239a87508039a77b48ee39a951a8bd196b583de2b93444aafd456d0cd92050fa6a816d5183c1d75e96df540c8ac3bb8638b971f0cf3fb5b4a321487a1c8992b921de110f3d5bbb87369b25fe743ad7e789ca52d9f9fe62ccb103b78fe65eaa2cd47895022c590639c8f0c6a3999d8a5c71ed94d355815851b479f8d93eae90822294c96b39724b33491f8497b0bf7e1b995b37e4d759ff8a7958d194da6e00c475a6ddcf6efcb5fb4bb383c9b273da18d01e000dbe9c65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2f4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd95a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e3774df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652cdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618db8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea32293237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d7358448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a927ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757bf558bebd2ceec7f3c5dce04a4782f88c2c6036ae78ee206d0bc5289d20461a2e21908c2968c0699040a6fd866a577a99a9d2ec88745c815fd4a472c789244daae824d72ddc272aab68a8c3022e36f10454437c1886f3ff9927b64f232df414f27e429a4bef3083bc31a671d046ea5c1f5b8c3094d72868d9dfdc12c7334ac5f743cc5c365a9a6a15c1f240ac25880c7a9d1de290696cb766074a1d83d9278164adcf616c3bfabf63999a01966c998b7bb572774035a63ead49da73b5987f34775786645d0c5dd7c04a2f8a75dcae085213652f5bce3ea8b9b9bedd1cab3c5e9b88b152c9b8a7b79637d35911848b0c41e7cc7cca2ab4fe9a15f9c38bb4bb9390c4e2d8ce834ffd7a6cd85d7113d4521abb857774845c4291e6f6d010d97e3185bc799d83e3bb31501b3da786680df30fbc18eb41cbce611e8c0e9c72f69571ca10d3ef857d04d9c03ead7c6317d797a090fa1271ad9c7addfbcb412e9643d4fb33b1809c42623f474055fa9400a2027a7a885c8dfa4efe20666b4ee27d7529c134d7f28d53f175f6bf4b62faa2110d5b76f0f770c15e628181c1fcc18f970a9c34d24b2fc8c50ca9c07a7156ef4e5ff4bdf002eda0b11c1d359d0b59a54680704dbb9db631457879b27e0dfdbe50158fd9cf9b4cf77605c4ac4c95bd65fc9f6f9295a686647cb999090819cda700820c282c613cedcd218540bbc6f37b01c6567c4a1ea624f092a3a5cca2d6f0f0db231972fce627f0ecca0dee60f17551c5f8fdaeb5ab560b2ceb781cdb339361a0fbee1b9dffad59115138c8d6a70dda9ccc1bf0bbdd7fee15764845db875f6432559ff8dbc9055324431bc34e5b93d15da307317849eccd90c0c7b98870b9317c15a5959dcfb84c76dcc908c4fe6ba92126339bf06e458f6646df5e83ba7c3d35bc263b3222c8e9040068847749ca8e8f95045e4342aeb521eb3a5587ec268ed3aa6faf32b62b0bc41a9d549521f406fc3ec7d4dabb75e0d3e144d7cc882372d13746b6dcd481b1b229bcaec9f7422cdfb84e35c5d92171376cae5c86300822d729cd3a8479583bef09527027dba5f11263c5cbbeb3834b7a5c1cba9aa5fee0c95ec3f17a33ec3d8047fff799187f5ae2040bbe913c226c34c9fbe4389dd728984257a816892b3cae3e43191dd291f0eb50000000000000000420000000000000035000000000000000000000000000000060000000000000000100000000000001900000000000000480000000000001050edbc06b4bfc3ee108b66f7a8f772ca4d90e1a085f4a8398505920f7465bb44b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3021ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85e58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a193440eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968ffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f839867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756afcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0f9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5f8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf8923490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99cc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8beccda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d22733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981fe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0b46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0c65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2f4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd95a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e3774df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652cdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618db8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea32293237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d7358448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a927ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757bf558bebd2ceec7f3c5dce04a4782f88c2c6036ae78ee206d0bc5289d20461a2e21908c2968c0699040a6fd866a577a99a9d2ec88745c815fd4a472c789244daae824d72ddc272aab68a8c3022e36f10454437c1886f3ff9927b64f232df414f27e429a4bef3083bc31a671d046ea5c1f5b8c3094d72868d9dfdc12c7334ac5f743cc5c365a9a6a15c1f240ac25880c7a9d1de290696cb766074a1d83d9278164adcf616c3bfabf63999a01966c998b7bb572774035a63ead49da73b5987f34775786645d0c5dd7c04a2f8a75dcae085213652f5bce3ea8b9b9bedd1cab3c5e9b88b152c9b8a7b79637d35911848b0c41e7cc7cca2ab4fe9a15f9c38bb4bb9390c4e2d8ce834ffd7a6cd85d7113d4521abb857774845c4291e6f6d010d97e3185bc799d83e3bb31501b3da786680df30fbc18eb41cbce611e8c0e9c72f69571ca10d3ef857d04d9c03ead7c6317d797a090fa1271ad9c7addfbcb412e9643d4fb33b1809c42623f474055fa9400a2027a7a885c8dfa4efe20666b4ee27d7529c134d7f28d53f175f6bf4b62faa2110d5b76f0f770c15e628181c1fcc18f970a9c34d24b2fc8c50ca9c07a7156ef4e5ff4bdf002eda0b11c1d359d0b59a54680704dbb9db631457879b27e0dfdbe50158fd9cf9b4cf77605c4ac4c95bd65fc9f6f9295a686647cb999090819cda700820c282c613cedcd218540bbc6f37b01c6567c4a1ea624f092a3a5cca2d6f0f0db231972fce627f0ecca0dee60f17551c5f8fdaeb5ab560b2ceb781cdb339361a0fbee1b9dffad59115138c8d6a70dda9ccc1bf0bbdd7fee15764845db875f6432559ff8dbc9055324431bc34e5b93d15da307317849eccd90c0c7b98870b9317c15a5959dcfb84c76dcc908c4fe6ba92126339bf06e458f6646df5e83ba7c3d35bc263b3222c8e9040068847749ca8e8f95045e4342aeb521eb3a5587ec268ed3aa6faf32b62b0bc41a9d549521f406fc30f3e39c5412c30550d1d07fb07ff0e546fbeea1988f6658f04a9b19693e5b99d84e35c5d92171376cae5c86300822d729cd3a8479583bef09527027dba5f11263c5cbbeb3834b7a5c1cba9aa5fee0c95ec3f17a33ec3d8047fff799187f5ae2040bbe913c226c34c9fbe4389dd728984257a816892b3cae3e43191dd291f0eb5") + require.Equal(t, expectedProof, proof) + // TODO: Need to add some oracle data + require.Nil(t, data) + require.Empty(t, generator.generated) + }) + + t.Run("ErrorsTraceIndexOutOfBounds", func(t *testing.T) { + dataDir, prestate := setupTestData(t) + provider, generator := setupWithTestData(t, dataDir, prestate) + largePosition := PositionFromTraceIndex(provider, new(big.Int).Mul(new(big.Int).SetUint64(math.MaxUint64), big.NewInt(2))) + _, _, _, err := provider.GetStepData(context.Background(), largePosition) + require.ErrorContains(t, err, "trace index out of bounds") + require.Empty(t, generator.generated) + }) + + t.Run("GenerateProof", func(t *testing.T) { + dataDir, prestate := setupTestData(t) + provider, generator := setupWithTestData(t, dataDir, prestate) + generator.finalState = &VMState{ + Memory: &Memory{}, + Step: 10, + Exited: true, + } + generator.proof = &cannon.ProofData{ + ClaimValue: common.Hash{0xaa}, + StateData: []byte{0xbb}, + ProofData: []byte{0xcc}, + OracleKey: common.Hash{0xdd}.Bytes(), + OracleValue: []byte{0xdd}, + OracleOffset: 10, + } + preimage, proof, data, err := provider.GetStepData(context.Background(), PositionFromTraceIndex(provider, big.NewInt(4))) + require.NoError(t, err) + require.Contains(t, generator.generated, 4, "should have tried to generate the proof") + + require.EqualValues(t, generator.proof.StateData, preimage) + require.EqualValues(t, generator.proof.ProofData, proof) + expectedData := types.NewPreimageOracleData(generator.proof.OracleKey, generator.proof.OracleValue, generator.proof.OracleOffset) + require.EqualValues(t, expectedData, data) + }) + + t.Run("ProofAfterEndOfTrace", func(t *testing.T) { + dataDir, prestate := setupTestData(t) + provider, generator := setupWithTestData(t, dataDir, prestate) + generator.finalState = &VMState{ + Memory: &Memory{}, + Step: 10, + Exited: true, + } + generator.proof = &cannon.ProofData{ + ClaimValue: common.Hash{0xaa}, + StateData: []byte{0xbb}, + ProofData: []byte{0xcc}, + OracleKey: common.Hash{0xdd}.Bytes(), + OracleValue: []byte{0xdd}, + OracleOffset: 10, + } + preimage, proof, data, err := provider.GetStepData(context.Background(), PositionFromTraceIndex(provider, big.NewInt(7000))) + require.NoError(t, err) + require.Contains(t, generator.generated, 7000, "should have tried to generate the proof") + + witness := generator.finalState.EncodeWitness() + require.EqualValues(t, witness, preimage) + require.Equal(t, []byte{}, proof) + require.Nil(t, data) + }) + + t.Run("ReadLastStepFromDisk", func(t *testing.T) { + dataDir, prestate := setupTestData(t) + provider, initGenerator := setupWithTestData(t, dataDir, prestate) + initGenerator.finalState = &VMState{ + Memory: &Memory{}, + Step: 10, + Exited: true, + } + initGenerator.proof = &cannon.ProofData{ + ClaimValue: common.Hash{0xaa}, + StateData: []byte{0xbb}, + ProofData: []byte{0xcc}, + OracleKey: common.Hash{0xdd}.Bytes(), + OracleValue: []byte{0xdd}, + OracleOffset: 10, + } + _, _, _, err := provider.GetStepData(context.Background(), PositionFromTraceIndex(provider, big.NewInt(7000))) + require.NoError(t, err) + require.Contains(t, initGenerator.generated, 7000, "should have tried to generate the proof") + + provider, generator := setupWithTestData(t, dataDir, prestate) + generator.finalState = &VMState{ + Memory: &Memory{}, + Step: 10, + Exited: true, + } + generator.proof = &cannon.ProofData{ + ClaimValue: common.Hash{0xaa}, + StateData: []byte{0xbb}, + ProofData: []byte{0xcc}, + } + preimage, proof, data, err := provider.GetStepData(context.Background(), PositionFromTraceIndex(provider, big.NewInt(7000))) + require.NoError(t, err) + require.Empty(t, generator.generated, "should not have to generate the proof again") + + require.EqualValues(t, initGenerator.finalState.EncodeWitness(), preimage) + require.Empty(t, proof) + require.Nil(t, data) + }) + + t.Run("MissingStateData", func(t *testing.T) { + dataDir, prestate := setupTestData(t) + provider, generator := setupWithTestData(t, dataDir, prestate) + _, _, _, err := provider.GetStepData(context.Background(), PositionFromTraceIndex(provider, big.NewInt(1))) + require.ErrorContains(t, err, "missing state data") + require.Empty(t, generator.generated) + }) + + t.Run("IgnoreUnknownFields", func(t *testing.T) { + dataDir, prestate := setupTestData(t) + provider, generator := setupWithTestData(t, dataDir, prestate) + value, proof, data, err := provider.GetStepData(context.Background(), PositionFromTraceIndex(provider, big.NewInt(2))) + require.NoError(t, err) + expected := common.FromHex("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc") + require.Equal(t, expected, value) + expectedProof := common.FromHex("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd") + require.Equal(t, expectedProof, proof) + require.Empty(t, generator.generated) + require.Nil(t, data) + }) +} + +func setupTestData(t *testing.T) (string, string) { + srcDir := filepath.Join("test_data", "proofs") + entries, err := testData.ReadDir(srcDir) + require.NoError(t, err) + dataDir := t.TempDir() + require.NoError(t, os.Mkdir(filepath.Join(dataDir, proofsDir), 0o777)) + for _, entry := range entries { + path := filepath.Join(srcDir, entry.Name()) + file, err := testData.ReadFile(path) + require.NoErrorf(t, err, "reading %v", path) + err = writeGzip(filepath.Join(dataDir, proofsDir, entry.Name()+".gz"), file) + require.NoErrorf(t, err, "writing %v", path) + } + return dataDir, "state.json" +} + +func setupWithTestData(t *testing.T, dataDir string, prestate string) (*AsteriscTraceProvider, *stubGenerator) { + generator := &stubGenerator{} + return &AsteriscTraceProvider{ + logger: testlog.Logger(t, log.LevelInfo), + dir: dataDir, + generator: generator, + prestate: filepath.Join(dataDir, prestate), + gameDepth: 63, + }, generator +} + +type stubGenerator struct { + generated []int // Using int makes assertions easier + finalState *VMState + proof *cannon.ProofData +} + +func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64) error { + e.generated = append(e.generated, int(i)) + if e.finalState != nil && e.finalState.Step <= i { + // Requesting a trace index past the end of the trace + data, err := json.Marshal(e.finalState) + if err != nil { + return err + } + return writeGzip(filepath.Join(dir, finalState), data) + } + if e.proof != nil { + proofFile := filepath.Join(dir, proofsDir, fmt.Sprintf("%d.json.gz", i)) + data, err := json.Marshal(e.proof) + if err != nil { + return err + } + return writeGzip(proofFile, data) + } + return nil +} + +func writeGzip(path string, data []byte) error { + writer, err := ioutil.OpenCompressed(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) + if err != nil { + return err + } + defer writer.Close() + _, err = writer.Write(data) + return err +} diff --git a/op-challenger/game/fault/trace/asterisc/test_data/invalid.json b/op-challenger/game/fault/trace/asterisc/test_data/invalid.json new file mode 100644 index 00000000000..06a76bf5b23 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/test_data/invalid.json @@ -0,0 +1,3 @@ +{ + "preimageKey": 1 +} diff --git a/op-challenger/game/fault/trace/asterisc/test_data/proofs/0.json b/op-challenger/game/fault/trace/asterisc/test_data/proofs/0.json new file mode 100644 index 00000000000..e5838ddfc5a --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/test_data/proofs/0.json @@ -0,0 +1,7 @@ +{ + "step": 0, + "pre": "0x03abd5c535c08bae7c4ad48fcae39b65f9c25239f65b4376c58638d262c97381", + "post": "0x034689707b571db46b32c9e433def18e648f4e1fa9e5abd4012e7913031bfc10", + "state-data": "0x354cfaf28a5b60c3f64f22f9f171b64aa067f90c6de6c96f725f44c5cf9f8ac1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080e080000000000000000000000007f0000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "proof-data": "0x000000000000000003350100930581006f00800100000000970f000067800f01000000000000000097c2ffff938282676780020000000000032581009308e0050e1893682c323d6695396f1122b3cb562af8c65cab19978c9246434fda0536c90ca1cfabf684ebce3ad9fbd54000a2b258f8d0e447c1bb6f7e97de47aadfc12cd7b6f466bfd024daa905886c5f638f4692d843709e6c1c0d9eb2e251c626d53d15e04b59735fe0781bc4357a4243fbc28e6981902a8c2669a2d6456f7a964423db5d1585da978861f8b84067654b29490275c82b54083ee09c82eb7aa9ae693911226bb8297ad82c0963ae943f22d0c6086f4f14437e4d1c87ceb17e68caf5eaec77f14b46225b417d2191ca7b49564c896836a95ad4e9c383bd1c8ff9d8e888c64fb3836daa9535e58372e9646b7b144219980a4389aca5da241c3ec11fbc9297bd7a94ac671ccec288604c23a0072b0c1ed069198959cacdc2574aff65b7eceffc391e21778a1775deceb3ec0990836df98d98a4f3f0dc854587230fbf59e4daa60e8240d74caf90f7e2cd014c1d5d707b2e44269d9a9caf133882fe1ebb2f4237f6282abe89639b357e9231418d0c41373229ae9edfa6815bec484cb79772c9e2a7d80912123558f79b539bb45d435f2a4446970f1e2123494740285cec3491b0a41a9fd7403bdc8cd239a87508039a77b48ee39a951a8bd196b583de2b93444aafd456d0cd92050fa6a816d5183c1d75e96df540c8ac3bb8638b971f0cf3fb5b4a321487a1c8992b921de110f3d5bbb87369b25fe743ad7e789ca52d9f9fe62ccb103b78fe65eaa2cd47895022c590639c8f0c6a3999d8a5c71ed94d355815851b479f8d93eae90822294c96b39724b33491f8497b0bf7e1b995b37e4d759ff8a7958d194da6e00c475a6ddcf6efcb5fb4bb383c9b273da18d01e000dbe9c65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2f4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd95a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e3774df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652cdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618db8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea32293237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d7358448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a927ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757bf558bebd2ceec7f3c5dce04a4782f88c2c6036ae78ee206d0bc5289d20461a2e21908c2968c0699040a6fd866a577a99a9d2ec88745c815fd4a472c789244daae824d72ddc272aab68a8c3022e36f10454437c1886f3ff9927b64f232df414f27e429a4bef3083bc31a671d046ea5c1f5b8c3094d72868d9dfdc12c7334ac5f743cc5c365a9a6a15c1f240ac25880c7a9d1de290696cb766074a1d83d9278164adcf616c3bfabf63999a01966c998b7bb572774035a63ead49da73b5987f34775786645d0c5dd7c04a2f8a75dcae085213652f5bce3ea8b9b9bedd1cab3c5e9b88b152c9b8a7b79637d35911848b0c41e7cc7cca2ab4fe9a15f9c38bb4bb9390c4e2d8ce834ffd7a6cd85d7113d4521abb857774845c4291e6f6d010d97e3185bc799d83e3bb31501b3da786680df30fbc18eb41cbce611e8c0e9c72f69571ca10d3ef857d04d9c03ead7c6317d797a090fa1271ad9c7addfbcb412e9643d4fb33b1809c42623f474055fa9400a2027a7a885c8dfa4efe20666b4ee27d7529c134d7f28d53f175f6bf4b62faa2110d5b76f0f770c15e628181c1fcc18f970a9c34d24b2fc8c50ca9c07a7156ef4e5ff4bdf002eda0b11c1d359d0b59a54680704dbb9db631457879b27e0dfdbe50158fd9cf9b4cf77605c4ac4c95bd65fc9f6f9295a686647cb999090819cda700820c282c613cedcd218540bbc6f37b01c6567c4a1ea624f092a3a5cca2d6f0f0db231972fce627f0ecca0dee60f17551c5f8fdaeb5ab560b2ceb781cdb339361a0fbee1b9dffad59115138c8d6a70dda9ccc1bf0bbdd7fee15764845db875f6432559ff8dbc9055324431bc34e5b93d15da307317849eccd90c0c7b98870b9317c15a5959dcfb84c76dcc908c4fe6ba92126339bf06e458f6646df5e83ba7c3d35bc263b3222c8e9040068847749ca8e8f95045e4342aeb521eb3a5587ec268ed3aa6faf32b62b0bc41a9d549521f406fc3ec7d4dabb75e0d3e144d7cc882372d13746b6dcd481b1b229bcaec9f7422cdfb84e35c5d92171376cae5c86300822d729cd3a8479583bef09527027dba5f11263c5cbbeb3834b7a5c1cba9aa5fee0c95ec3f17a33ec3d8047fff799187f5ae2040bbe913c226c34c9fbe4389dd728984257a816892b3cae3e43191dd291f0eb50000000000000000420000000000000035000000000000000000000000000000060000000000000000100000000000001900000000000000480000000000001050edbc06b4bfc3ee108b66f7a8f772ca4d90e1a085f4a8398505920f7465bb44b4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d3021ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85e58769b32a1beaf1ea27375a44095a0d1fb664ce2dd358e7fcbfb78c26a193440eb01ebfc9ed27500cd4dfc979272d1f0913cc9f66540d7e8005811109e1cf2d887c22bd8750d34016ac3c66b5ff102dacdd73f6b014e710b51e8022af9a1968ffd70157e48063fc33c97a050f7f640233bf646cc98d9524c6b92bcf3ab56f839867cc5f7f196b93bae1e27e6320742445d290f2263827498b54fec539f756afcefad4e508c098b9a7e1d8feb19955fb02ba9675585078710969d3440f5054e0f9dc3e7fe016e050eff260334f18a5d4fe391d82092319f5964f2e2eb7c1c3a5f8b13a49e282f609c317a833fb8d976d11517c571d1221a265d25af778ecf8923490c6ceeb450aecdc82e28293031d10c7d73bf85e57bf041a97360aa2c5d99cc1df82d9c4b87413eae2ef048f94b4d3554cea73d92b0f7af96e0271c691e2bb5c67add7c6caf302256adedf7ab114da0acfe870d449a3a489f781d659e8beccda7bce9f4e8618b6bd2f4132ce798cdc7a60e7e1460a7299e3c6342a579626d22733e50f526ec2fa19a22b31e8ed50f23cd1fdf94c9154ed3a7609a2f1ff981fe1d3b5c807b281e4683cc6d6315cf95b9ade8641defcb32372f1c126e398ef7a5a2dce0a8a7f68bb74560f8f71837c2c2ebbcbf7fffb42ae1896f13f7c7479a0b46a28b6f55540f89444f63de0378e3d121be09e06cc9ded1c20e65876d36aa0c65e9645644786b620e2dd2ad648ddfcbf4a7e5b1a3a4ecfe7f64667a3f0b7e2f4418588ed35a2458cffeb39b93d26f18d2ab13bdce6aee58e7b99359ec2dfd95a9c16dc00d6ef18b7933a6f8dc65ccb55667138776f7dea101070dc8796e3774df84f40ae0c8229d0d6069e5c8f39a7c299677a09d367fc7b05e3bc380ee652cdc72595f74c7b1043d0e1ffbab734648c838dfb0527d971b602bc216c9619ef0abf5ac974a1ed57f4050aa510dd9c74f508277b39d7973bb2dfccc5eeb0618db8cd74046ff337f0a7bf2c8e03e10f642c1886798d71806ab1e888d9e5ee87d0838c5655cb21c6cb83313b5a631175dff4963772cce9108188b34ac87c81c41e662ee4dd2dd7b2bc707961b1e646c4047669dcb6584f0d8d770daf5d7e7deb2e388ab20e2573d171a88108e79d820e98f26c0b84aa8b2f4aa4968dbb818ea32293237c50ba75ee485f4c22adf2f741400bdf8d6a9cc7df7ecae576221665d7358448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a927ae5ba08d7291c96c8cbddcc148bf48a6d68c7974b94356f53754ef6171d757bf558bebd2ceec7f3c5dce04a4782f88c2c6036ae78ee206d0bc5289d20461a2e21908c2968c0699040a6fd866a577a99a9d2ec88745c815fd4a472c789244daae824d72ddc272aab68a8c3022e36f10454437c1886f3ff9927b64f232df414f27e429a4bef3083bc31a671d046ea5c1f5b8c3094d72868d9dfdc12c7334ac5f743cc5c365a9a6a15c1f240ac25880c7a9d1de290696cb766074a1d83d9278164adcf616c3bfabf63999a01966c998b7bb572774035a63ead49da73b5987f34775786645d0c5dd7c04a2f8a75dcae085213652f5bce3ea8b9b9bedd1cab3c5e9b88b152c9b8a7b79637d35911848b0c41e7cc7cca2ab4fe9a15f9c38bb4bb9390c4e2d8ce834ffd7a6cd85d7113d4521abb857774845c4291e6f6d010d97e3185bc799d83e3bb31501b3da786680df30fbc18eb41cbce611e8c0e9c72f69571ca10d3ef857d04d9c03ead7c6317d797a090fa1271ad9c7addfbcb412e9643d4fb33b1809c42623f474055fa9400a2027a7a885c8dfa4efe20666b4ee27d7529c134d7f28d53f175f6bf4b62faa2110d5b76f0f770c15e628181c1fcc18f970a9c34d24b2fc8c50ca9c07a7156ef4e5ff4bdf002eda0b11c1d359d0b59a54680704dbb9db631457879b27e0dfdbe50158fd9cf9b4cf77605c4ac4c95bd65fc9f6f9295a686647cb999090819cda700820c282c613cedcd218540bbc6f37b01c6567c4a1ea624f092a3a5cca2d6f0f0db231972fce627f0ecca0dee60f17551c5f8fdaeb5ab560b2ceb781cdb339361a0fbee1b9dffad59115138c8d6a70dda9ccc1bf0bbdd7fee15764845db875f6432559ff8dbc9055324431bc34e5b93d15da307317849eccd90c0c7b98870b9317c15a5959dcfb84c76dcc908c4fe6ba92126339bf06e458f6646df5e83ba7c3d35bc263b3222c8e9040068847749ca8e8f95045e4342aeb521eb3a5587ec268ed3aa6faf32b62b0bc41a9d549521f406fc30f3e39c5412c30550d1d07fb07ff0e546fbeea1988f6658f04a9b19693e5b99d84e35c5d92171376cae5c86300822d729cd3a8479583bef09527027dba5f11263c5cbbeb3834b7a5c1cba9aa5fee0c95ec3f17a33ec3d8047fff799187f5ae2040bbe913c226c34c9fbe4389dd728984257a816892b3cae3e43191dd291f0eb5" +} diff --git a/op-challenger/game/fault/trace/asterisc/test_data/proofs/1.json b/op-challenger/game/fault/trace/asterisc/test_data/proofs/1.json new file mode 100644 index 00000000000..311847daa5a --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/test_data/proofs/1.json @@ -0,0 +1,2 @@ +{} + diff --git a/op-challenger/game/fault/trace/asterisc/test_data/proofs/2.json b/op-challenger/game/fault/trace/asterisc/test_data/proofs/2.json new file mode 100644 index 00000000000..96f58c8e8cb --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/test_data/proofs/2.json @@ -0,0 +1,9 @@ +{ + "foo": 0, + "bar": "0x71f9eb93ff904e5c03c3425228ef75766db0c906ad239df9a7a7f0d9c6a89705", + "step": 0, + "pre": "0x03abd5c535c08bae7c4ad48fcae39b65f9c25239f65b4376c58638d262c97381", + "post": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "state-data": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "proof-data": "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" +} diff --git a/op-challenger/game/fault/trace/asterisc/test_data/state.json b/op-challenger/game/fault/trace/asterisc/test_data/state.json new file mode 100644 index 00000000000..3eade83c31b --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/test_data/state.json @@ -0,0 +1,12 @@ +{ + "memory": [], + "preimageKey": [204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204], + "preimageOffset": 0, + "pc": 0, + "exit": 0, + "exited": false, + "step": 0, + "heap": 0, + "loadReservation": 0, + "registers": [] +} diff --git a/op-challenger/game/fault/trace/cannon/preimage.go b/op-challenger/game/fault/trace/cannon/preimage.go index 60bb6ec6fad..4539ffdeacd 100644 --- a/op-challenger/game/fault/trace/cannon/preimage.go +++ b/op-challenger/game/fault/trace/cannon/preimage.go @@ -29,17 +29,17 @@ var ( type preimageSource func(key common.Hash) ([]byte, error) -type preimageLoader struct { +type PreimageLoader struct { getPreimage preimageSource } -func newPreimageLoader(getPreimage preimageSource) *preimageLoader { - return &preimageLoader{ +func NewPreimageLoader(getPreimage preimageSource) *PreimageLoader { + return &PreimageLoader{ getPreimage: getPreimage, } } -func (l *preimageLoader) LoadPreimage(proof *proofData) (*types.PreimageOracleData, error) { +func (l *PreimageLoader) LoadPreimage(proof *ProofData) (*types.PreimageOracleData, error) { if len(proof.OracleKey) == 0 { return nil, nil } @@ -53,7 +53,7 @@ func (l *preimageLoader) LoadPreimage(proof *proofData) (*types.PreimageOracleDa } } -func (l *preimageLoader) loadBlobPreimage(proof *proofData) (*types.PreimageOracleData, error) { +func (l *PreimageLoader) loadBlobPreimage(proof *ProofData) (*types.PreimageOracleData, error) { // The key for a blob field element is a keccak hash of commitment++fieldElementIndex. // First retrieve the preimage of the key as a keccak hash so we have the commitment and required field element inputsKey := preimage.Keccak256Key(proof.OracleKey).PreimageKey() @@ -102,7 +102,7 @@ func (l *preimageLoader) loadBlobPreimage(proof *proofData) (*types.PreimageOrac return types.NewPreimageOracleBlobData(proof.OracleKey, claimWithLength, proof.OracleOffset, requiredFieldElement, commitment, kzgProof[:]), nil } -func (l *preimageLoader) loadPrecompilePreimage(proof *proofData) (*types.PreimageOracleData, error) { +func (l *PreimageLoader) loadPrecompilePreimage(proof *ProofData) (*types.PreimageOracleData, error) { inputKey := preimage.Keccak256Key(proof.OracleKey).PreimageKey() input, err := l.getPreimage(inputKey) if err != nil { diff --git a/op-challenger/game/fault/trace/cannon/preimage_test.go b/op-challenger/game/fault/trace/cannon/preimage_test.go index 94fbbc35bc8..7b92a4fa97b 100644 --- a/op-challenger/game/fault/trace/cannon/preimage_test.go +++ b/op-challenger/game/fault/trace/cannon/preimage_test.go @@ -20,15 +20,15 @@ import ( ) func TestPreimageLoader_NoPreimage(t *testing.T) { - loader := newPreimageLoader(kvstore.NewMemKV().Get) - actual, err := loader.LoadPreimage(&proofData{}) + loader := NewPreimageLoader(kvstore.NewMemKV().Get) + actual, err := loader.LoadPreimage(&ProofData{}) require.NoError(t, err) require.Nil(t, actual) } func TestPreimageLoader_LocalPreimage(t *testing.T) { - loader := newPreimageLoader(kvstore.NewMemKV().Get) - proof := &proofData{ + loader := NewPreimageLoader(kvstore.NewMemKV().Get) + proof := &ProofData{ OracleKey: common.Hash{byte(preimage.LocalKeyType), 0xaa, 0xbb}.Bytes(), OracleValue: nil, OracleOffset: 4, @@ -48,8 +48,8 @@ func TestPreimageLoader_SimpleTypes(t *testing.T) { for _, keyType := range tests { keyType := keyType t.Run(fmt.Sprintf("type-%v", keyType), func(t *testing.T) { - loader := newPreimageLoader(kvstore.NewMemKV().Get) - proof := &proofData{ + loader := NewPreimageLoader(kvstore.NewMemKV().Get) + proof := &ProofData{ OracleKey: common.Hash{byte(keyType), 0xaa, 0xbb}.Bytes(), OracleValue: []byte{1, 2, 3, 4, 5, 6}, OracleOffset: 3, @@ -82,7 +82,7 @@ func TestPreimageLoader_BlobPreimage(t *testing.T) { binary.BigEndian.PutUint64(keyBuf[72:], fieldIndex) key := preimage.BlobKey(crypto.Keccak256Hash(keyBuf)).PreimageKey() - proof := &proofData{ + proof := &ProofData{ OracleKey: key[:], OracleValue: elementDataWithLengthPrefix, OracleOffset: 4, @@ -90,8 +90,8 @@ func TestPreimageLoader_BlobPreimage(t *testing.T) { t.Run("NoKeyPreimage", func(t *testing.T) { kv := kvstore.NewMemKV() - loader := newPreimageLoader(kv.Get) - proof := &proofData{ + loader := NewPreimageLoader(kv.Get) + proof := &ProofData{ OracleKey: common.Hash{byte(preimage.BlobKeyType), 0xaf}.Bytes(), OracleValue: proof.OracleValue, OracleOffset: proof.OracleOffset, @@ -102,8 +102,8 @@ func TestPreimageLoader_BlobPreimage(t *testing.T) { t.Run("InvalidKeyPreimage", func(t *testing.T) { kv := kvstore.NewMemKV() - loader := newPreimageLoader(kv.Get) - proof := &proofData{ + loader := NewPreimageLoader(kv.Get) + proof := &ProofData{ OracleKey: common.Hash{byte(preimage.BlobKeyType), 0xad}.Bytes(), OracleValue: proof.OracleValue, OracleOffset: proof.OracleOffset, @@ -115,8 +115,8 @@ func TestPreimageLoader_BlobPreimage(t *testing.T) { t.Run("MissingBlobs", func(t *testing.T) { kv := kvstore.NewMemKV() - loader := newPreimageLoader(kv.Get) - proof := &proofData{ + loader := NewPreimageLoader(kv.Get) + proof := &ProofData{ OracleKey: common.Hash{byte(preimage.BlobKeyType), 0xae}.Bytes(), OracleValue: proof.OracleValue, OracleOffset: proof.OracleOffset, @@ -128,7 +128,7 @@ func TestPreimageLoader_BlobPreimage(t *testing.T) { t.Run("Valid", func(t *testing.T) { kv := kvstore.NewMemKV() - loader := newPreimageLoader(kv.Get) + loader := NewPreimageLoader(kv.Get) storeBlob(t, kv, gokzg4844.KZGCommitment(commitment), blob) actual, err := loader.LoadPreimage(proof) require.NoError(t, err) @@ -155,19 +155,19 @@ func TestPreimageLoader_BlobPreimage(t *testing.T) { func TestPreimageLoader_PrecompilePreimage(t *testing.T) { input := []byte("test input") key := preimage.PrecompileKey(crypto.Keccak256Hash(input)).PreimageKey() - proof := &proofData{ + proof := &ProofData{ OracleKey: key[:], } t.Run("NoInputPreimage", func(t *testing.T) { kv := kvstore.NewMemKV() - loader := newPreimageLoader(kv.Get) + loader := NewPreimageLoader(kv.Get) _, err := loader.LoadPreimage(proof) require.ErrorIs(t, err, kvstore.ErrNotFound) }) t.Run("Valid", func(t *testing.T) { kv := kvstore.NewMemKV() - loader := newPreimageLoader(kv.Get) + loader := NewPreimageLoader(kv.Get) require.NoError(t, kv.Put(preimage.Keccak256Key(proof.OracleKey).PreimageKey(), input)) actual, err := loader.LoadPreimage(proof) require.NoError(t, err) diff --git a/op-challenger/game/fault/trace/cannon/provider.go b/op-challenger/game/fault/trace/cannon/provider.go index bd71ce2a4db..e0287067668 100644 --- a/op-challenger/game/fault/trace/cannon/provider.go +++ b/op-challenger/game/fault/trace/cannon/provider.go @@ -28,7 +28,7 @@ const ( diskStateCache = "state.json.gz" ) -type proofData struct { +type ProofData struct { ClaimValue common.Hash `json:"post"` StateData hexutil.Bytes `json:"state-data"` ProofData hexutil.Bytes `json:"proof-data"` @@ -52,7 +52,7 @@ type CannonTraceProvider struct { prestate string generator ProofGenerator gameDepth types.Depth - preimageLoader *preimageLoader + preimageLoader *PreimageLoader // lastStep stores the last step in the actual trace if known. 0 indicates unknown. // Cached as an optimisation to avoid repeatedly attempting to execute beyond the end of the trace. @@ -66,7 +66,7 @@ func NewTraceProvider(logger log.Logger, m CannonMetricer, cfg *config.Config, l prestate: cfg.CannonAbsolutePreState, generator: NewExecutor(logger, m, cfg, localInputs), gameDepth: gameDepth, - preimageLoader: newPreimageLoader(kvstore.NewDiskKV(preimageDir(dir)).Get), + preimageLoader: NewPreimageLoader(kvstore.NewDiskKV(preimageDir(dir)).Get), } } @@ -137,7 +137,7 @@ func (p *CannonTraceProvider) AbsolutePreStateCommitment(_ context.Context) (com // loadProof will attempt to load or generate the proof data at the specified index // If the requested index is beyond the end of the actual trace it is extended with no-op instructions. -func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofData, error) { +func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*ProofData, error) { // Attempt to read the last step from disk cache if p.lastStep == 0 { step, err := readLastStep(p.dir) @@ -177,7 +177,7 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa if err != nil { return nil, fmt.Errorf("cannot hash witness: %w", err) } - proof := &proofData{ + proof := &ProofData{ ClaimValue: witnessHash, StateData: hexutil.Bytes(witness), ProofData: []byte{}, @@ -198,7 +198,7 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa return nil, fmt.Errorf("cannot open proof file (%v): %w", path, err) } defer file.Close() - var proof proofData + var proof ProofData err = json.NewDecoder(file).Decode(&proof) if err != nil { return nil, fmt.Errorf("failed to read proof (%v): %w", path, err) @@ -234,7 +234,7 @@ func readLastStep(dir string) (uint64, error) { } // writeLastStep writes the last step and proof to disk as a persistent cache. -func writeLastStep(dir string, proof *proofData, step uint64) error { +func writeLastStep(dir string, proof *ProofData, step uint64) error { state := diskStateCacheObj{Step: step} lastStepFile := filepath.Join(dir, diskStateCache) if err := ioutil.WriteCompressedJson(lastStepFile, state); err != nil { @@ -289,7 +289,7 @@ func NewTraceProviderForTest(logger log.Logger, m CannonMetricer, cfg *config.Co prestate: cfg.CannonAbsolutePreState, generator: NewExecutor(logger, m, cfg, localInputs), gameDepth: gameDepth, - preimageLoader: newPreimageLoader(kvstore.NewDiskKV(preimageDir(dir)).Get), + preimageLoader: NewPreimageLoader(kvstore.NewDiskKV(preimageDir(dir)).Get), } return &CannonTraceProviderForTest{p} } diff --git a/op-challenger/game/fault/trace/cannon/provider_test.go b/op-challenger/game/fault/trace/cannon/provider_test.go index a98b59adf6e..4874303955f 100644 --- a/op-challenger/game/fault/trace/cannon/provider_test.go +++ b/op-challenger/game/fault/trace/cannon/provider_test.go @@ -110,7 +110,7 @@ func TestGetStepData(t *testing.T) { Step: 10, Exited: true, } - generator.proof = &proofData{ + generator.proof = &ProofData{ ClaimValue: common.Hash{0xaa}, StateData: []byte{0xbb}, ProofData: []byte{0xcc}, @@ -136,7 +136,7 @@ func TestGetStepData(t *testing.T) { Step: 10, Exited: true, } - generator.proof = &proofData{ + generator.proof = &ProofData{ ClaimValue: common.Hash{0xaa}, StateData: []byte{0xbb}, ProofData: []byte{0xcc}, @@ -162,7 +162,7 @@ func TestGetStepData(t *testing.T) { Step: 10, Exited: true, } - initGenerator.proof = &proofData{ + initGenerator.proof = &ProofData{ ClaimValue: common.Hash{0xaa}, StateData: []byte{0xbb}, ProofData: []byte{0xcc}, @@ -180,7 +180,7 @@ func TestGetStepData(t *testing.T) { Step: 10, Exited: true, } - generator.proof = &proofData{ + generator.proof = &ProofData{ ClaimValue: common.Hash{0xaa}, StateData: []byte{0xbb}, ProofData: []byte{0xcc}, @@ -246,7 +246,7 @@ func setupWithTestData(t *testing.T, dataDir string, prestate string) (*CannonTr type stubGenerator struct { generated []int // Using int makes assertions easier finalState *mipsevm.State - proof *proofData + proof *ProofData } func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64) error { diff --git a/op-challenger/game/fault/trace/outputs/output_asterisc.go b/op-challenger/game/fault/trace/outputs/output_asterisc.go new file mode 100644 index 00000000000..ee93683e418 --- /dev/null +++ b/op-challenger/game/fault/trace/outputs/output_asterisc.go @@ -0,0 +1,49 @@ +package outputs + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/asterisc" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum-optimism/optimism/op-challenger/metrics" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +func NewOutputAsteriscTraceAccessor( + logger log.Logger, + m metrics.Metricer, + cfg *config.Config, + l2Client cannon.L2HeaderSource, + prestateProvider types.PrestateProvider, + rollupClient OutputRollupClient, + dir string, + l1Head eth.BlockID, + splitDepth types.Depth, + prestateBlock uint64, + poststateBlock uint64, +) (*trace.Accessor, error) { + outputProvider := NewTraceProvider(logger, prestateProvider, rollupClient, l1Head, splitDepth, prestateBlock, poststateBlock) + asteriscCreator := func(ctx context.Context, localContext common.Hash, depth types.Depth, agreed contracts.Proposal, claimed contracts.Proposal) (types.TraceProvider, error) { + logger := logger.New("pre", agreed.OutputRoot, "post", claimed.OutputRoot, "localContext", localContext) + subdir := filepath.Join(dir, localContext.Hex()) + localInputs, err := cannon.FetchLocalInputsFromProposals(ctx, l1Head.Hash, l2Client, agreed, claimed) + if err != nil { + return nil, fmt.Errorf("failed to fetch asterisc local inputs: %w", err) + } + provider := asterisc.NewTraceProvider(logger, m, cfg, localInputs, subdir, depth) + return provider, nil + } + + cache := NewProviderCache(m, "output_asterisc_provider", asteriscCreator) + selector := split.NewSplitProviderSelector(outputProvider, splitDepth, OutputRootSplitAdapter(outputProvider, cache.GetOrCreate)) + return trace.NewAccessor(selector), nil +} diff --git a/op-challenger/game/fault/types/types.go b/op-challenger/game/fault/types/types.go index 45d265030f2..d2efe5e3e27 100644 --- a/op-challenger/game/fault/types/types.go +++ b/op-challenger/game/fault/types/types.go @@ -18,6 +18,7 @@ var ( const ( CannonGameType uint32 = 0 PermissionedGameType uint32 = 1 + AsteriscGameType uint32 = 2 AlphabetGameType uint32 = 255 ) diff --git a/op-challenger/metrics/metrics.go b/op-challenger/metrics/metrics.go index 9d90b91b770..e657a57b57d 100644 --- a/op-challenger/metrics/metrics.go +++ b/op-challenger/metrics/metrics.go @@ -37,6 +37,7 @@ type Metricer interface { RecordGameStep() RecordGameMove() RecordCannonExecutionTime(t float64) + RecordAsteriscExecutionTime(t float64) RecordClaimResolutionTime(t float64) RecordGameActTime(t float64) @@ -85,9 +86,10 @@ type Metrics struct { moves prometheus.Counter steps prometheus.Counter - cannonExecutionTime prometheus.Histogram - claimResolutionTime prometheus.Histogram - gameActTime prometheus.Histogram + claimResolutionTime prometheus.Histogram + gameActTime prometheus.Histogram + cannonExecutionTime prometheus.Histogram + asteriscExecutionTime prometheus.Histogram trackedGames prometheus.GaugeVec inflightGames prometheus.Gauge @@ -165,6 +167,14 @@ func NewMetrics() *Metrics { []float64{1.0, 2.0, 5.0, 10.0}, prometheus.ExponentialBuckets(30.0, 2.0, 14)...), }), + asteriscExecutionTime: factory.NewHistogram(prometheus.HistogramOpts{ + Namespace: Namespace, + Name: "asterisc_execution_time", + Help: "Time (in seconds) to execute asterisc", + Buckets: append( + []float64{1.0, 10.0}, + prometheus.ExponentialBuckets(30.0, 2.0, 14)...), + }), bondClaimFailures: factory.NewCounter(prometheus.CounterOpts{ Namespace: Namespace, Name: "claim_failures", @@ -261,6 +271,10 @@ func (m *Metrics) RecordCannonExecutionTime(t float64) { m.cannonExecutionTime.Observe(t) } +func (m *Metrics) RecordAsteriscExecutionTime(t float64) { + m.asteriscExecutionTime.Observe(t) +} + func (m *Metrics) RecordClaimResolutionTime(t float64) { m.claimResolutionTime.Observe(t) } diff --git a/op-challenger/metrics/noop.go b/op-challenger/metrics/noop.go index e742c6b8225..7584e614733 100644 --- a/op-challenger/metrics/noop.go +++ b/op-challenger/metrics/noop.go @@ -36,9 +36,10 @@ func (*NoopMetricsImpl) RecordPreimageChallengeFailed() {} func (*NoopMetricsImpl) RecordBondClaimFailed() {} func (*NoopMetricsImpl) RecordBondClaimed(uint64) {} -func (*NoopMetricsImpl) RecordCannonExecutionTime(t float64) {} -func (*NoopMetricsImpl) RecordClaimResolutionTime(t float64) {} -func (*NoopMetricsImpl) RecordGameActTime(t float64) {} +func (*NoopMetricsImpl) RecordCannonExecutionTime(t float64) {} +func (*NoopMetricsImpl) RecordAsteriscExecutionTime(t float64) {} +func (*NoopMetricsImpl) RecordClaimResolutionTime(t float64) {} +func (*NoopMetricsImpl) RecordGameActTime(t float64) {} func (*NoopMetricsImpl) RecordGamesStatus(inProgress, defenderWon, challengerWon int) {} diff --git a/op-dispute-mon/mon/extract/caller.go b/op-dispute-mon/mon/extract/caller.go index ab38bf50466..5587fb2254c 100644 --- a/op-dispute-mon/mon/extract/caller.go +++ b/op-dispute-mon/mon/extract/caller.go @@ -50,7 +50,7 @@ func (g *GameCallerCreator) CreateContract(game gameTypes.GameMetadata) (GameCal return fdg, nil } switch game.GameType { - case faultTypes.CannonGameType, faultTypes.AlphabetGameType: + case faultTypes.CannonGameType, faultTypes.AsteriscGameType, faultTypes.AlphabetGameType: fdg, err := contracts.NewFaultDisputeGameContract(g.m, game.Proxy, g.caller) if err != nil { return nil, fmt.Errorf("failed to create FaultDisputeGameContract: %w", err) diff --git a/op-dispute-mon/mon/extract/caller_test.go b/op-dispute-mon/mon/extract/caller_test.go index a56aa4fbd0e..69cc470f55d 100644 --- a/op-dispute-mon/mon/extract/caller_test.go +++ b/op-dispute-mon/mon/extract/caller_test.go @@ -29,14 +29,18 @@ func TestMetadataCreator_CreateContract(t *testing.T) { name: "validCannonGameType", game: types.GameMetadata{GameType: faultTypes.CannonGameType, Proxy: fdgAddr}, }, + { + name: "validAsteriscGameType", + game: types.GameMetadata{GameType: faultTypes.AsteriscGameType, Proxy: fdgAddr}, + }, { name: "validAlphabetGameType", game: types.GameMetadata{GameType: faultTypes.AlphabetGameType, Proxy: fdgAddr}, }, { name: "InvalidGameType", - game: types.GameMetadata{GameType: 2, Proxy: fdgAddr}, - expectedErr: fmt.Errorf("unsupported game type: 2"), + game: types.GameMetadata{GameType: 3, Proxy: fdgAddr}, + expectedErr: fmt.Errorf("unsupported game type: 3"), }, }