From c38ce096def52e4acdaecb7ffd2d396f464693dd Mon Sep 17 00:00:00 2001 From: Park Changwan Date: Sun, 21 Apr 2024 19:41:01 -0600 Subject: [PATCH] op-challenger: Asterisc Support with Refactoring (#10094) * op-challenger: Refactor cannon trace provider Move gzip method to ioutil * op-challenger: Expose cannon tracer methods * op-challenger: asterisc implementation Basic unit tests mirrored from cannon. * op-challenger: asterisc flags and configs * op-challenger: asterisc metric helper * op-challenger: asterisc registration and add types * op-dispute-mon: enable asterisc type * Fix typo * Fix l2RPC when registering asterisc * Deduplicate preimageOpts for testing * Fix comments * Style issue fix * Deduplicate network flag for testing * Remove redundant prefix at filename * Add config test for using both cannon and asterisc * Refactor asterisc/cannon provider using utils * Remove redundant prefix at filename * Fix comments * Style issue fix * Refactor asterisc/cannon provider using utils * Remove unused methods * op-challenger: add l2-rpc flag and deprecate cannon-l2 flag --- docs/fault-proof-alpha/run-challenger.md | 2 +- op-challenger/README.md | 2 +- op-challenger/cmd/main_test.go | 222 ++++++++++++++- op-challenger/config/config.go | 94 ++++++- op-challenger/config/config_test.go | 91 +++++- op-challenger/flags/flags.go | 125 ++++++++- op-challenger/game/fault/register.go | 83 +++++- .../game/fault/trace/asterisc/executor.go | 127 +++++++++ .../fault/trace/asterisc/executor_test.go | 135 +++++++++ .../game/fault/trace/asterisc/prestate.go | 41 +++ .../fault/trace/asterisc/prestate_test.go | 59 ++++ .../game/fault/trace/asterisc/provider.go | 220 +++++++++++++++ .../fault/trace/asterisc/provider_test.go | 260 ++++++++++++++++++ .../game/fault/trace/asterisc/state.go | 69 +++++ .../game/fault/trace/asterisc/state_test.go | 83 ++++++ .../trace/asterisc/test_data/invalid.json | 3 + .../trace/asterisc/test_data/proofs/0.json | 7 + .../trace/asterisc/test_data/proofs/1.json | 2 + .../trace/asterisc/test_data/proofs/2.json | 9 + .../fault/trace/asterisc/test_data/state.json | 40 +++ .../game/fault/trace/cannon/executor.go | 96 +------ .../game/fault/trace/cannon/executor_test.go | 43 +-- .../game/fault/trace/cannon/prestate_test.go | 2 +- .../game/fault/trace/cannon/provider.go | 116 +------- .../game/fault/trace/cannon/provider_test.go | 40 ++- .../cannon/{cannon_state.go => state.go} | 0 .../{cannon_state_test.go => state_test.go} | 0 .../fault/trace/outputs/output_asterisc.go | 49 ++++ .../game/fault/trace/outputs/output_cannon.go | 5 +- .../game/fault/trace/utils/executor.go | 81 ++++++ .../fault/trace/{cannon => utils}/local.go | 2 +- .../trace/{cannon => utils}/local_test.go | 2 +- .../fault/trace/{cannon => utils}/preimage.go | 14 +- .../trace/{cannon => utils}/preimage_test.go | 36 +-- .../game/fault/trace/utils/provider.go | 96 +++++++ op-challenger/game/fault/types/types.go | 1 + op-challenger/metrics/metrics.go | 20 +- op-challenger/metrics/noop.go | 7 +- op-dispute-mon/mon/extract/caller.go | 2 +- op-dispute-mon/mon/extract/caller_test.go | 8 +- op-e2e/e2eutils/challenger/helper.go | 2 +- .../disputegame/output_cannon_helper.go | 7 +- op-e2e/faultproofs/output_cannon_test.go | 12 +- op-e2e/faultproofs/precompile_test.go | 5 +- op-service/ioutil/gzip.go | 12 + 45 files changed, 2005 insertions(+), 327 deletions(-) create mode 100644 op-challenger/game/fault/trace/asterisc/executor.go create mode 100644 op-challenger/game/fault/trace/asterisc/executor_test.go create mode 100644 op-challenger/game/fault/trace/asterisc/prestate.go create mode 100644 op-challenger/game/fault/trace/asterisc/prestate_test.go create mode 100644 op-challenger/game/fault/trace/asterisc/provider.go create mode 100644 op-challenger/game/fault/trace/asterisc/provider_test.go create mode 100644 op-challenger/game/fault/trace/asterisc/state.go create mode 100644 op-challenger/game/fault/trace/asterisc/state_test.go create mode 100644 op-challenger/game/fault/trace/asterisc/test_data/invalid.json create mode 100644 op-challenger/game/fault/trace/asterisc/test_data/proofs/0.json create mode 100644 op-challenger/game/fault/trace/asterisc/test_data/proofs/1.json create mode 100644 op-challenger/game/fault/trace/asterisc/test_data/proofs/2.json create mode 100644 op-challenger/game/fault/trace/asterisc/test_data/state.json rename op-challenger/game/fault/trace/cannon/{cannon_state.go => state.go} (100%) rename op-challenger/game/fault/trace/cannon/{cannon_state_test.go => state_test.go} (100%) create mode 100644 op-challenger/game/fault/trace/outputs/output_asterisc.go create mode 100644 op-challenger/game/fault/trace/utils/executor.go rename op-challenger/game/fault/trace/{cannon => utils}/local.go (99%) rename op-challenger/game/fault/trace/{cannon => utils}/local_test.go (99%) rename op-challenger/game/fault/trace/{cannon => utils}/preimage.go (92%) rename op-challenger/game/fault/trace/{cannon => utils}/preimage_test.go (91%) create mode 100644 op-challenger/game/fault/trace/utils/provider.go diff --git a/docs/fault-proof-alpha/run-challenger.md b/docs/fault-proof-alpha/run-challenger.md index 93017acac84c..c9f84057b1f3 100644 --- a/docs/fault-proof-alpha/run-challenger.md +++ b/docs/fault-proof-alpha/run-challenger.md @@ -45,7 +45,7 @@ make op-challenger op-program cannon --cannon-bin ./cannon/bin/cannon \ --cannon-server ./op-program/bin/op-program \ --cannon-prestate \ - --cannon-l2 \ + --l2-rpc \ --private-key ``` diff --git a/op-challenger/README.md b/op-challenger/README.md index 3d740dc5703e..a8090af3ec51 100644 --- a/op-challenger/README.md +++ b/op-challenger/README.md @@ -44,7 +44,7 @@ DISPUTE_GAME_FACTORY=$(jq -r .DisputeGameFactoryProxy .devnet/addresses.json) --cannon-bin ./cannon/bin/cannon \ --cannon-server ./op-program/bin/op-program \ --cannon-prestate ./op-program/bin/prestate.json \ - --cannon-l2 http://localhost:9545 \ + --l2-rpc http://localhost:9545 \ --mnemonic "test test test test test test test test test test test junk" \ --hd-path "m/44'/60'/0'/0/8" \ --num-confirmations 1 diff --git a/op-challenger/cmd/main_test.go b/op-challenger/cmd/main_test.go index cd0eefb4c657..78be94de1530 100644 --- a/op-challenger/cmd/main_test.go +++ b/op-challenger/cmd/main_test.go @@ -22,13 +22,17 @@ var ( l1Beacon = "http://example.com:9000" gameFactoryAddressValue = "0xbb00000000000000000000000000000000000000" cannonNetwork = "op-mainnet" - otherCannonNetwork = "op-sepolia" + testNetwork = "op-sepolia" + l2Rpc = "http://example.com:9545" cannonBin = "./bin/cannon" cannonServer = "./bin/op-program" cannonPreState = "./pre.json" datadir = "./test_data" - cannonL2 = "http://example.com:9545" rollupRpc = "http://example.com:8555" + asteriscNetwork = "op-mainnet" + asteriscBin = "./bin/asterisc" + asteriscServer = "./bin/op-program" + asteriscPreState = "./pre.json" ) func TestLogLevel(t *testing.T) { @@ -111,14 +115,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 +251,178 @@ 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("TestAsteriscAbsolutePrestate-%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("TestL2Rpc-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTraceLegacy", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-l2")) + }) + + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--l2-rpc")) + }) + + t.Run("RequiredForAsteriscTrace", func(t *testing.T) { + verifyArgsInvalid(t, "flag l2-rpc is required", addRequiredArgsExcept(traceType, "--l2-rpc")) + }) + + t.Run("ValidLegacy", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--l2-rpc", fmt.Sprintf("--cannon-l2=%s", l2Rpc))) + require.Equal(t, l2Rpc, cfg.L2Rpc) + }) + + t.Run("Valid", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgs(traceType)) + require.Equal(t, l2Rpc, cfg.L2Rpc) + }) + + t.Run("InvalidUsingBothFlags", func(t *testing.T) { + verifyArgsInvalid(t, "flag cannon-l2 and l2-rpc must not be both set", addRequiredArgsExcept(traceType, "", fmt.Sprintf("--cannon-l2=%s", l2Rpc))) + }) + }) + + 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", testNetwork)) + require.Equal(t, testNetwork, 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 @@ -291,18 +471,27 @@ func TestCannonRequiredArgs(t *testing.T) { }) }) - t.Run(fmt.Sprintf("TestCannonL2-%v", traceType), func(t *testing.T) { - t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + t.Run(fmt.Sprintf("TestL2Rpc-%v", traceType), func(t *testing.T) { + t.Run("NotRequiredForAlphabetTraceLegacy", func(t *testing.T) { configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-l2")) }) + t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { + configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--l2-rpc")) + }) + t.Run("RequiredForCannonTrace", func(t *testing.T) { - verifyArgsInvalid(t, "flag cannon-l2 is required", addRequiredArgsExcept(traceType, "--cannon-l2")) + verifyArgsInvalid(t, "flag l2-rpc is required", addRequiredArgsExcept(traceType, "--l2-rpc")) + }) + + t.Run("ValidLegacy", func(t *testing.T) { + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--l2-rpc", fmt.Sprintf("--cannon-l2=%s", l2Rpc))) + require.Equal(t, l2Rpc, cfg.L2Rpc) }) t.Run("Valid", func(t *testing.T) { cfg := configForArgs(t, addRequiredArgs(traceType)) - require.Equal(t, cannonL2, cfg.CannonL2) + require.Equal(t, l2Rpc, cfg.L2Rpc) }) }) @@ -374,8 +563,8 @@ func TestCannonRequiredArgs(t *testing.T) { }) t.Run("Valid", func(t *testing.T) { - cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--cannon-network", "--cannon-network", otherCannonNetwork)) - require.Equal(t, otherCannonNetwork, cfg.CannonNetwork) + cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--cannon-network", "--cannon-network", testNetwork)) + require.Equal(t, testNetwork, cfg.CannonNetwork) }) }) @@ -390,7 +579,7 @@ func TestCannonRequiredArgs(t *testing.T) { }) }) - t.Run(fmt.Sprintf("TestCannonL2Genesis-%v", traceType), func(t *testing.T) { + t.Run(fmt.Sprintf("TestCannonL2qGenesis-%v", traceType), func(t *testing.T) { t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) { configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-l2-genesis")) }) @@ -560,6 +749,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) } @@ -571,7 +762,16 @@ func addRequiredCannonArgs(args map[string]string) { args["--cannon-bin"] = cannonBin args["--cannon-server"] = cannonServer args["--cannon-prestate"] = cannonPreState - args["--cannon-l2"] = cannonL2 + args["--l2-rpc"] = l2Rpc + 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["--l2-rpc"] = l2Rpc addRequiredOutputArgs(args) } diff --git a/op-challenger/config/config.go b/op-challenger/config/config.go index d13c716b803f..fa4bca583c16 100644 --- a/op-challenger/config/config.go +++ b/op-challenger/config/config.go @@ -19,7 +19,7 @@ var ( ErrMissingTraceType = errors.New("no supported trace types specified") ErrMissingDatadir = errors.New("missing datadir") ErrMaxConcurrencyZero = errors.New("max concurrency must not be 0") - ErrMissingCannonL2 = errors.New("missing cannon L2") + ErrMissingL2Rpc = errors.New("missing L2 rpc url") ErrMissingCannonBin = errors.New("missing cannon bin") ErrMissingCannonServer = errors.New("missing cannon server") ErrMissingCannonAbsolutePreState = errors.New("missing cannon absolute pre-state") @@ -34,6 +34,17 @@ var ( ErrCannonNetworkAndL2Genesis = errors.New("only specify one of network or l2 genesis path") ErrCannonNetworkUnknown = errors.New("unknown cannon network") ErrMissingRollupRpc = errors.New("missing rollup rpc url") + + 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 @@ -41,10 +52,11 @@ 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 +86,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 +119,9 @@ type Config struct { TraceTypes []TraceType // Type of traces supported - // Specific to the output cannon trace type - RollupRpc string + RollupRpc string // L2 Rollup RPC Url + + L2Rpc string // L2 RPC Url // Specific to the cannon trace provider CannonBin string // Path to the cannon executable to run when generating trace data @@ -115,9 +130,18 @@ type Config struct { CannonNetwork string CannonRollupConfigPath string CannonL2GenesisPath string - CannonL2 string // L2 RPC Url - CannonSnapshotFreq uint // Frequency of snapshots to create when executing cannon (in VM instructions) - CannonInfoFreq uint // Frequency of cannon progress log messages (in VM instructions) + 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 + 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) @@ -150,9 +174,11 @@ func NewConfig( Datadir: datadir, - CannonSnapshotFreq: DefaultCannonSnapshotFreq, - CannonInfoFreq: DefaultCannonInfoFreq, - GameWindow: DefaultGameWindow, + CannonSnapshotFreq: DefaultCannonSnapshotFreq, + CannonInfoFreq: DefaultCannonInfoFreq, + AsteriscSnapshotFreq: DefaultAsteriscSnapshotFreq, + AsteriscInfoFreq: DefaultAsteriscInfoFreq, + GameWindow: DefaultGameWindow, } } @@ -210,8 +236,8 @@ func (c Config) Check() error { if c.CannonAbsolutePreState == "" { return ErrMissingCannonAbsolutePreState } - if c.CannonL2 == "" { - return ErrMissingCannonL2 + if c.L2Rpc == "" { + return ErrMissingL2Rpc } if c.CannonSnapshotFreq == 0 { return ErrMissingCannonSnapshotFreq @@ -220,6 +246,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.L2Rpc == "" { + return ErrMissingL2Rpc + } + 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 01210557c60d..517d3f9f1f20 100644 --- a/op-challenger/config/config_test.go +++ b/op-challenger/config/config_test.go @@ -20,20 +20,40 @@ var ( validCannonNetwork = "mainnet" validCannonAbsolutPreState = "pre.json" validDatadir = "/tmp/data" - validCannonL2 = "http://localhost:9545" + validL2Rpc = "http://localhost:9545" validRollupRpc = "http://localhost:8555" + + validAsteriscBin = "./bin/asterisc" + validAsteriscOpProgramBin = "./bin/op-program" + validAsteriscNetwork = "mainnet" + validAsteriscAbsolutPreState = "pre.json" ) var cannonTraceTypes = []TraceType{TraceTypeCannon, TraceTypePermissioned} +func applyValidConfigForCannon(cfg *Config) { + cfg.CannonBin = validCannonBin + cfg.CannonServer = validCannonOpProgramBin + cfg.CannonAbsolutePreState = validCannonAbsolutPreState + cfg.CannonNetwork = validCannonNetwork + cfg.L2Rpc = validL2Rpc +} + +func applyValidConfigForAsterisc(cfg *Config) { + cfg.AsteriscBin = validAsteriscBin + cfg.AsteriscServer = validAsteriscOpProgramBin + cfg.AsteriscAbsolutePreState = validAsteriscAbsolutPreState + cfg.AsteriscNetwork = validAsteriscNetwork + cfg.L2Rpc = validL2Rpc +} + func validConfig(traceType TraceType) Config { cfg := NewConfig(validGameFactoryAddress, validL1EthRpc, validL1BeaconUrl, validDatadir, traceType) if traceType == TraceTypeCannon || traceType == TraceTypePermissioned { - cfg.CannonBin = validCannonBin - cfg.CannonServer = validCannonOpProgramBin - cfg.CannonAbsolutePreState = validCannonAbsolutPreState - cfg.CannonL2 = validCannonL2 - cfg.CannonNetwork = validCannonNetwork + applyValidConfigForCannon(&cfg) + } + if traceType == TraceTypeAsterisc { + applyValidConfigForAsterisc(&cfg) } cfg.RollupRpc = validRollupRpc return cfg @@ -110,10 +130,10 @@ func TestCannonRequiredArgs(t *testing.T) { require.ErrorIs(t, config.Check(), ErrMissingCannonAbsolutePreState) }) - t.Run(fmt.Sprintf("TestCannonL2Required-%v", traceType), func(t *testing.T) { + t.Run(fmt.Sprintf("TestL2RpcRequired-%v", traceType), func(t *testing.T) { config := validConfig(traceType) - config.CannonL2 = "" - require.ErrorIs(t, config.Check(), ErrMissingCannonL2) + config.L2Rpc = "" + require.ErrorIs(t, config.Check(), ErrMissingL2Rpc) }) t.Run(fmt.Sprintf("TestCannonSnapshotFreq-%v", traceType), func(t *testing.T) { @@ -209,7 +229,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 @@ -217,11 +237,56 @@ func TestRequireConfigForMultipleTraceTypes(t *testing.T) { require.NoError(t, cfg.Check()) // Require cannon specific args - cfg.CannonL2 = "" - require.ErrorIs(t, cfg.Check(), ErrMissingCannonL2) - cfg.CannonL2 = validCannonL2 + cfg.CannonAbsolutePreState = "" + require.ErrorIs(t, cfg.Check(), ErrMissingCannonAbsolutePreState) + cfg.CannonAbsolutePreState = validCannonAbsolutPreState // Require output cannon specific args 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.AsteriscAbsolutePreState = "" + require.ErrorIs(t, cfg.Check(), ErrMissingAsteriscAbsolutePreState) + cfg.AsteriscAbsolutePreState = validAsteriscAbsolutPreState + + // Require output asterisc specific args + cfg.RollupRpc = "" + require.ErrorIs(t, cfg.Check(), ErrMissingRollupRpc) +} + +func TestRequireConfigForMultipleTraceTypesForCannonAndAsterisc(t *testing.T) { + cfg := validConfig(TraceTypeCannon) + applyValidConfigForAsterisc(&cfg) + + cfg.TraceTypes = []TraceType{TraceTypeCannon, TraceTypeAsterisc, TraceTypeAlphabet} + // Set all required options and check its valid + cfg.RollupRpc = validRollupRpc + require.NoError(t, cfg.Check()) + + // Require cannon specific args + cfg.CannonBin = "" + require.ErrorIs(t, cfg.Check(), ErrMissingCannonBin) + cfg.CannonBin = validCannonBin + + // Require asterisc specific args + cfg.AsteriscAbsolutePreState = "" + require.ErrorIs(t, cfg.Check(), ErrMissingAsteriscAbsolutePreState) + cfg.AsteriscAbsolutePreState = validAsteriscAbsolutPreState + + // Require cannon specific args + cfg.AsteriscServer = "" + require.ErrorIs(t, cfg.Check(), ErrMissingAsteriscServer) + cfg.AsteriscServer = validAsteriscOpProgramBin + + // Check final config is valid + require.NoError(t, cfg.Check()) +} diff --git a/op-challenger/flags/flags.go b/op-challenger/flags/flags.go index 718cd5c00b20..4b48647bdaa8 100644 --- a/op-challenger/flags/flags.go +++ b/op-challenger/flags/flags.go @@ -71,6 +71,11 @@ var ( EnvVars: prefixEnvVars("MAX_CONCURRENCY"), Value: uint(runtime.NumCPU()), } + L2RpcFlag = &cli.StringFlag{ + Name: "l2-rpc", + Usage: "L2 Address of L2 JSON-RPC endpoint to use (eth and debug namespace required) (cannon/asterisc trace type only)", + EnvVars: prefixEnvVars("L2_RPC"), + } MaxPendingTransactionsFlag = &cli.Uint64Flag{ Name: "max-pending-tx", Usage: "The maximum number of pending transactions. 0 for no limit.", @@ -123,7 +128,7 @@ var ( } CannonL2Flag = &cli.StringFlag{ Name: "cannon-l2", - Usage: "L2 Address of L2 JSON-RPC endpoint to use (eth and debug namespace required) (cannon trace type only)", + Usage: fmt.Sprintf("Deprecated: Use %v instead", L2RpcFlag.Name), EnvVars: prefixEnvVars("CANNON_L2"), } CannonSnapshotFreqFlag = &cli.UintFlag{ @@ -138,6 +143,51 @@ 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"), + } + 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. " + @@ -171,6 +221,7 @@ var requiredFlags = []cli.Flag{ var optionalFlags = []cli.Flag{ TraceTypeFlag, MaxConcurrencyFlag, + L2RpcFlag, MaxPendingTransactionsFlag, HTTPPollInterval, AdditionalBondClaimants, @@ -184,6 +235,14 @@ var optionalFlags = []cli.Flag{ CannonL2Flag, CannonSnapshotFreqFlag, CannonInfoFreqFlag, + AsteriscNetworkFlag, + AsteriscRollupConfigFlag, + AsteriscL2GenesisFlag, + AsteriscBinFlag, + AsteriscServerFlag, + AsteriscPreStateFlag, + AsteriscSnapshotFreqFlag, + AsteriscInfoFreqFlag, GameWindowFlag, SelectiveClaimResolutionFlag, UnsafeAllowInvalidPrestate, @@ -221,8 +280,36 @@ func CheckCannonFlags(ctx *cli.Context) error { if !ctx.IsSet(CannonPreStateFlag.Name) { return fmt.Errorf("flag %s is required", CannonPreStateFlag.Name) } - if !ctx.IsSet(CannonL2Flag.Name) { - return fmt.Errorf("flag %s is required", CannonL2Flag.Name) + // CannonL2Flag is checked because it is an alias with L2RpcFlag + if !ctx.IsSet(CannonL2Flag.Name) && !ctx.IsSet(L2RpcFlag.Name) { + return fmt.Errorf("flag %s is required", L2RpcFlag.Name) + } + 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) + } + // CannonL2Flag is checked because it is an alias with L2RpcFlag + if !ctx.IsSet(CannonL2Flag.Name) && !ctx.IsSet(L2RpcFlag.Name) { + return fmt.Errorf("flag %s is required", L2RpcFlag.Name) } return nil } @@ -239,6 +326,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) @@ -261,6 +352,20 @@ func parseTraceTypes(ctx *cli.Context) ([]config.TraceType, error) { return traceTypes, nil } +func getL2Rpc(ctx *cli.Context) (string, error) { + if ctx.IsSet(CannonL2Flag.Name) && ctx.IsSet(L2RpcFlag.Name) { + return "", fmt.Errorf("flag %v and %v must not be both set", CannonL2Flag.Name, L2RpcFlag.Name) + } + l2Rpc := "" + if ctx.IsSet(CannonL2Flag.Name) { + l2Rpc = ctx.String(CannonL2Flag.Name) + } + if ctx.IsSet(L2RpcFlag.Name) { + l2Rpc = ctx.String(L2RpcFlag.Name) + } + return l2Rpc, nil +} + // NewConfigFromCLI parses the Config from the provided flags or environment variables. func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { traceTypes, err := parseTraceTypes(ctx) @@ -303,6 +408,10 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { claimants = append(claimants, claimant) } } + l2Rpc, err := getL2Rpc(ctx) + if err != nil { + return nil, err + } return &config.Config{ // Required Flags L1EthRpc: ctx.String(L1EthRpcFlag.Name), @@ -312,6 +421,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { GameAllowlist: allowedGames, GameWindow: ctx.Duration(GameWindowFlag.Name), MaxConcurrency: maxConcurrency, + L2Rpc: l2Rpc, MaxPendingTx: ctx.Uint64(MaxPendingTransactionsFlag.Name), PollInterval: ctx.Duration(HTTPPollInterval.Name), AdditionalBondClaimants: claimants, @@ -323,9 +433,16 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) { CannonServer: ctx.String(CannonServerFlag.Name), CannonAbsolutePreState: ctx.String(CannonPreStateFlag.Name), Datadir: ctx.String(DatadirFlag.Name), - 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), + 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 1117dfb641e3..5af7866ce43e 100644 --- a/op-challenger/game/fault/register.go +++ b/op-challenger/game/fault/register.go @@ -8,8 +8,10 @@ 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" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types" "github.com/ethereum-optimism/optimism/op-challenger/game/scheduler" @@ -58,10 +60,10 @@ func RegisterGameTypes( ) (CloseFunc, error) { var closer CloseFunc var l2Client *ethclient.Client - if cfg.TraceTypeEnabled(config.TraceTypeCannon) || cfg.TraceTypeEnabled(config.TraceTypePermissioned) { - l2, err := ethclient.DialContext(ctx, cfg.CannonL2) + if cfg.TraceTypeEnabled(config.TraceTypeCannon) || cfg.TraceTypeEnabled(config.TraceTypePermissioned) || cfg.TraceTypeEnabled(config.TraceTypeAsterisc) { + l2, err := ethclient.DialContext(ctx, cfg.L2Rpc) if err != nil { - return nil, fmt.Errorf("dial l2 client %v: %w", cfg.CannonL2, err) + return nil, fmt.Errorf("dial l2 client %v: %w", cfg.L2Rpc, err) } l2Client = l2 closer = l2Client.Close @@ -78,6 +80,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 +174,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 utils.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, @@ -182,7 +257,7 @@ func registerCannon( txSender TxSender, gameFactory *contracts.DisputeGameFactoryContract, caller *batching.MultiCaller, - l2Client cannon.L2HeaderSource, + l2Client utils.L2HeaderSource, l1HeaderSource L1HeaderSource, selective bool, claimants []common.Address, 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 000000000000..4bfe3737a895 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/executor.go @@ -0,0 +1,127 @@ +package asterisc + +import ( + "context" + "fmt" + "math" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "github.com/ethereum/go-ethereum/log" +) + +type Executor struct { + logger log.Logger + metrics AsteriscMetricer + l1 string + l1Beacon string + l2 string + inputs utils.LocalGameInputs + asterisc string + server string + network string + rollupConfig string + l2Genesis string + absolutePreState string + snapshotFreq uint + infoFreq uint + selectSnapshot utils.SnapshotSelect + cmdExecutor utils.CmdExecutor +} + +func NewExecutor(logger log.Logger, m AsteriscMetricer, cfg *config.Config, inputs utils.LocalGameInputs) *Executor { + return &Executor{ + logger: logger, + metrics: m, + l1: cfg.L1EthRpc, + l1Beacon: cfg.L1Beacon, + l2: cfg.L2Rpc, + 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: utils.FindStartingSnapshot, + cmdExecutor: utils.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) +} + +// generateProof executes asterisc from the specified starting trace index until the end trace index. +// 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, utils.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 := utils.PreimageDir(dir) + lastGeneratedState := filepath.Join(dir, utils.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 +} 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 000000000000..4bfb537c9c75 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/executor_test.go @@ -0,0 +1,135 @@ +package asterisc + +import ( + "context" + "math" + "math/big" + "path/filepath" + "testing" + + "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "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" +) + +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.L2Rpc = "http://localhost:9999" + cfg.AsteriscAbsolutePreState = "pre.json" + cfg.AsteriscBin = "./bin/asterisc" + cfg.AsteriscServer = "./bin/op-program" + cfg.AsteriscSnapshotFreq = 500 + cfg.AsteriscInfoFreq = 900 + + inputs := utils.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, utils.PreimagesDir)) + require.DirExists(t, filepath.Join(dir, proofsDir)) + require.DirExists(t, filepath.Join(dir, utils.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, utils.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.L2Rpc, args["--l2"]) + require.Equal(t, filepath.Join(dir, utils.PreimagesDir), args["--datadir"]) + require.Equal(t, filepath.Join(dir, proofsDir, "%d.json.gz"), args["--proof-fmt"]) + require.Equal(t, filepath.Join(dir, utils.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") + }) +} + +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 000000000000..64165edabc5d --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/prestate.go @@ -0,0 +1,41 @@ +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() (*VMState, error) { + state, err := parseState(p.prestate) + if err != nil { + return nil, fmt.Errorf("cannot load absolute pre-state: %w", err) + } + return state, 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) + } + p.prestateCommitment = state.StateHash + return state.StateHash, 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 000000000000..38bc6d360e7b --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/prestate_test.go @@ -0,0 +1,59 @@ +package asterisc + +import ( + "context" + "os" + "path/filepath" + "testing" + + "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("CacheAbsolutePreState", func(t *testing.T) { + setupPreState(t, dataDir, prestate) + 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 000000000000..fd4cbbbbb778 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/provider.go @@ -0,0 +1,220 @@ +package asterisc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "os" + "path/filepath" + + "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "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/log" +) + +const ( + proofsDir = "proofs" + diskStateCache = "state.json.gz" +) + +type AsteriscMetricer interface { + RecordAsteriscExecutionTime(t float64) +} + +type AsteriscTraceProvider struct { + logger log.Logger + dir string + prestate string + generator utils.ProofGenerator + gameDepth types.Depth + preimageLoader *utils.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 utils.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: utils.NewPreimageLoader(kvstore.NewDiskKV(utils.PreimageDir(dir)).Get), + } +} + +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() (*VMState, error) { + state, err := parseState(p.prestate) + if err != nil { + return nil, fmt.Errorf("cannot load absolute pre-state: %w", err) + } + return state, 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) + } + return state.StateHash, 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) (*utils.ProofData, error) { + // Attempt to read the last step from disk cache + if p.lastStep == 0 { + step, err := utils.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. + proof := &utils.ProofData{ + ClaimValue: state.StateHash, + StateData: state.Witness, + ProofData: []byte{}, + OracleKey: nil, + OracleValue: nil, + OracleOffset: 0, + } + if err := utils.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 utils.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, utils.FinalState)) + if err != nil { + return nil, fmt.Errorf("cannot read final state: %w", err) + } + return state, nil +} + +// AsteriscTraceProviderForTest is a AsteriscTraceProvider that can find the step referencing the preimage read +// Only to be used for testing +type AsteriscTraceProviderForTest struct { + *AsteriscTraceProvider +} + +func NewTraceProviderForTest(logger log.Logger, m AsteriscMetricer, cfg *config.Config, localInputs utils.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: utils.NewPreimageLoader(kvstore.NewDiskKV(utils.PreimageDir(dir)).Get), + } + return &AsteriscTraceProviderForTest{p} +} + +func (p *AsteriscTraceProviderForTest) FindStep(ctx context.Context, start uint64, preimage utils.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 000000000000..950a5ad70375 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/provider_test.go @@ -0,0 +1,260 @@ +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/utils" + "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("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{ + Step: 10, + Exited: true, + Witness: make([]byte, asteriscWitnessLen), + } + generator.proof = &utils.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{ + Step: 10, + Exited: true, + Witness: make([]byte, asteriscWitnessLen), + } + generator.proof = &utils.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.Witness + 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{ + Step: 10, + Exited: true, + Witness: make([]byte, asteriscWitnessLen), + } + initGenerator.proof = &utils.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{ + Step: 10, + Exited: true, + Witness: make([]byte, asteriscWitnessLen), + } + generator.proof = &utils.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.Witness, 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) + proofFile := filepath.Join(dataDir, proofsDir, entry.Name()+".gz") + err = ioutil.WriteCompressedBytes(proofFile, file, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) + 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 *utils.ProofData +} + +func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64) error { + e.generated = append(e.generated, int(i)) + var proofFile string + var data []byte + var err error + if e.finalState != nil && e.finalState.Step <= i { + // Requesting a trace index past the end of the trace + proofFile = filepath.Join(dir, utils.FinalState) + data, err = json.Marshal(e.finalState) + if err != nil { + return err + } + return ioutil.WriteCompressedBytes(proofFile, data, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) + } + 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 ioutil.WriteCompressedBytes(proofFile, data, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) + } + return nil +} diff --git a/op-challenger/game/fault/trace/asterisc/state.go b/op-challenger/game/fault/trace/asterisc/state.go new file mode 100644 index 000000000000..39b1469ff047 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/state.go @@ -0,0 +1,69 @@ +package asterisc + +import ( + "encoding/json" + "fmt" + + "github.com/ethereum-optimism/optimism/cannon/mipsevm" + "github.com/ethereum-optimism/optimism/op-service/ioutil" +) + +var asteriscWitnessLen = 362 + +// The state struct will be read from json. +// other fields included in json are specific to FPVM implementation, and not required for trace provider. +type VMState struct { + PC uint64 `json:"pc"` + Exited bool `json:"exited"` + Step uint64 `json:"step"` + Witness []byte `json:"witness"` + StateHash [32]byte `json:"stateHash"` +} + +func (state *VMState) validateStateHash() error { + exitCode := state.StateHash[0] + if exitCode >= 4 { + return fmt.Errorf("invalid stateHash: unknown exitCode %d", exitCode) + } + if (state.Exited && exitCode == mipsevm.VMStatusUnfinished) || (!state.Exited && exitCode != mipsevm.VMStatusUnfinished) { + return fmt.Errorf("invalid stateHash: invalid exitCode %d", exitCode) + } + return nil +} + +func (state *VMState) validateWitness() error { + witnessLen := len(state.Witness) + if witnessLen != asteriscWitnessLen { + return fmt.Errorf("invalid witness: Length must be 362 but got %d", witnessLen) + } + return nil +} + +// validateState performs verification of state; it is not perfect. +// It does not recalculate whether witness nor stateHash is correctly set from state. +func (state *VMState) validateState() error { + if err := state.validateStateHash(); err != nil { + return err + } + if err := state.validateWitness(); err != nil { + return err + } + return nil +} + +// parseState parses state from json and goes on state validation +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 + if err := json.NewDecoder(file).Decode(&state); err != nil { + return nil, fmt.Errorf("invalid asterisc VM state (%v): %w", path, err) + } + if err := state.validateState(); 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/state_test.go b/op-challenger/game/fault/trace/asterisc/state_test.go new file mode 100644 index 000000000000..02b38eb41565 --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/state_test.go @@ -0,0 +1,83 @@ +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) + }) + + t.Run("InvalidStateWitness", func(t *testing.T) { + invalidWitnessLen := asteriscWitnessLen - 1 + state := &VMState{ + Step: 10, + Exited: true, + Witness: make([]byte, invalidWitnessLen), + } + err := state.validateState() + require.ErrorContains(t, err, "invalid witness") + }) + + t.Run("InvalidStateHash", func(t *testing.T) { + state := &VMState{ + Step: 10, + Exited: true, + Witness: make([]byte, asteriscWitnessLen), + } + // Unknown exit code + state.StateHash[0] = 37 + err := state.validateState() + require.ErrorContains(t, err, "invalid stateHash: unknown exitCode") + // Exited but ExitCode is VMStatusUnfinished + state.StateHash[0] = 3 + err = state.validateState() + require.ErrorContains(t, err, "invalid stateHash: invalid exitCode") + // Not Exited but ExitCode is not VMStatusUnfinished + state.Exited = false + for exitCode := 0; exitCode < 3; exitCode++ { + state.StateHash[0] = byte(exitCode) + err = state.validateState() + require.ErrorContains(t, err, "invalid stateHash: invalid exitCode") + } + }) +} 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 000000000000..06a76bf5b23d --- /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 000000000000..e5838ddfc5ab --- /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 000000000000..311847daa5a0 --- /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 000000000000..96f58c8e8cb3 --- /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 000000000000..a1bf2e5b412e --- /dev/null +++ b/op-challenger/game/fault/trace/asterisc/test_data/state.json @@ -0,0 +1,40 @@ +{ + "pc": 0, + "exited": false, + "step": 0, + "witness": "wOSi8Cm62dDmKt1OGwxlLrSznk6zE4ghp7evP1rfrXYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIGCAAAAAAAAAAAAAAAAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "stateHash": [ + 3, + 33, + 111, + 220, + 74, + 123, + 253, + 76, + 113, + 96, + 250, + 148, + 109, + 27, + 254, + 69, + 29, + 19, + 255, + 50, + 218, + 73, + 102, + 9, + 254, + 24, + 53, + 82, + 130, + 185, + 16, + 198 + ] +} diff --git a/op-challenger/game/fault/trace/cannon/executor.go b/op-challenger/game/fault/trace/cannon/executor.go index e2aaf7693cfc..6bf5300c4c9a 100644 --- a/op-challenger/game/fault/trace/cannon/executor.go +++ b/op-challenger/game/fault/trace/cannon/executor.go @@ -2,40 +2,26 @@ package cannon import ( "context" - "errors" "fmt" "math" "os" - "os/exec" "path/filepath" - "regexp" "strconv" "strings" "time" "github.com/ethereum-optimism/optimism/op-challenger/config" - oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "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 CannonMetricer l1 string l1Beacon string l2 string - inputs LocalGameInputs + inputs utils.LocalGameInputs cannon string server string network string @@ -44,17 +30,17 @@ type Executor struct { absolutePreState string snapshotFreq uint infoFreq uint - selectSnapshot snapshotSelect - cmdExecutor cmdExecutor + selectSnapshot utils.SnapshotSelect + cmdExecutor utils.CmdExecutor } -func NewExecutor(logger log.Logger, m CannonMetricer, cfg *config.Config, inputs LocalGameInputs) *Executor { +func NewExecutor(logger log.Logger, m CannonMetricer, cfg *config.Config, inputs utils.LocalGameInputs) *Executor { return &Executor{ logger: logger, metrics: m, l1: cfg.L1EthRpc, l1Beacon: cfg.L1Beacon, - l2: cfg.CannonL2, + l2: cfg.L2Rpc, inputs: inputs, cannon: cfg.CannonBin, server: cfg.CannonServer, @@ -64,8 +50,8 @@ func NewExecutor(logger log.Logger, m CannonMetricer, cfg *config.Config, inputs absolutePreState: cfg.CannonAbsolutePreState, snapshotFreq: cfg.CannonSnapshotFreq, infoFreq: cfg.CannonInfoFreq, - selectSnapshot: findStartingSnapshot, - cmdExecutor: runCmd, + selectSnapshot: utils.FindStartingSnapshot, + cmdExecutor: utils.RunCmd, } } @@ -75,18 +61,17 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro return e.generateProof(ctx, dir, i, i) } -// generateProofOrUntilPreimageRead executes cannon to generate a proof at the specified trace index, -// or until a non-local preimage read is encountered if untilPreimageRead is true. +// generateProof executes cannon from the specified starting trace index until the end trace index. // The proof is stored at the specified directory. func (e *Executor) generateProof(ctx context.Context, dir string, begin uint64, end uint64, extraCannonArgs ...string) error { - snapshotDir := filepath.Join(dir, snapsDir) + snapshotDir := filepath.Join(dir, utils.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) + proofDir := filepath.Join(dir, utils.ProofsDir) + dataDir := utils.PreimageDir(dir) + lastGeneratedState := filepath.Join(dir, utils.FinalState) args := []string{ "run", "--input", start, @@ -140,58 +125,3 @@ func (e *Executor) generateProof(ctx context.Context, dir string, begin uint64, e.metrics.RecordCannonExecutionTime(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/cannon/executor_test.go b/op-challenger/game/fault/trace/cannon/executor_test.go index 9ac7db834bd7..c755d50031c6 100644 --- a/op-challenger/game/fault/trace/cannon/executor_test.go +++ b/op-challenger/game/fault/trace/cannon/executor_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-service/testlog" "github.com/ethereum/go-ethereum/common" @@ -25,14 +26,14 @@ func TestGenerateProof(t *testing.T) { tempDir := t.TempDir() dir := filepath.Join(tempDir, "gameDir") cfg := config.NewConfig(common.Address{0xbb}, "http://localhost:8888", "http://localhost:9000", tempDir, config.TraceTypeCannon) + cfg.L2Rpc = "http://localhost:9999" cfg.CannonAbsolutePreState = "pre.json" cfg.CannonBin = "./bin/cannon" cfg.CannonServer = "./bin/op-program" - cfg.CannonL2 = "http://localhost:9999" cfg.CannonSnapshotFreq = 500 cfg.CannonInfoFreq = 900 - inputs := LocalGameInputs{ + inputs := utils.LocalGameInputs{ L1Head: common.Hash{0x11}, L2Head: common.Hash{0x22}, L2OutputRoot: common.Hash{0x33}, @@ -73,15 +74,15 @@ func TestGenerateProof(t *testing.T) { cfg.CannonRollupConfigPath = "" cfg.CannonL2GenesisPath = "" 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.DirExists(t, filepath.Join(dir, utils.PreimagesDir)) + require.DirExists(t, filepath.Join(dir, utils.ProofsDir)) + require.DirExists(t, filepath.Join(dir, utils.SnapsDir)) require.Equal(t, cfg.CannonBin, 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, filepath.Join(dir, utils.FinalState), args["--output"]) require.Equal(t, "=150000000", args["--proof-at"]) require.Equal(t, "=150000001", args["--stop-at"]) require.Equal(t, "%500", args["--snapshot-at"]) @@ -92,10 +93,10 @@ func TestGenerateProof(t *testing.T) { require.Equal(t, "--server", args[cfg.CannonServer]) require.Equal(t, cfg.L1EthRpc, args["--l1"]) require.Equal(t, cfg.L1Beacon, args["--l1.beacon"]) - require.Equal(t, cfg.CannonL2, 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.L2Rpc, args["--l2"]) + require.Equal(t, filepath.Join(dir, utils.PreimagesDir), args["--datadir"]) + require.Equal(t, filepath.Join(dir, utils.ProofsDir, "%d.json.gz"), args["--proof-fmt"]) + require.Equal(t, filepath.Join(dir, utils.SnapsDir, "%d.json.gz"), args["--snapshot-fmt"]) require.Equal(t, cfg.CannonNetwork, args["--network"]) require.NotContains(t, args, "--rollup.config") require.NotContains(t, args, "--l2.genesis") @@ -137,7 +138,7 @@ func TestRunCmdLogsOutput(t *testing.T) { 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") + err := utils.RunCmd(ctx, logger, bin, "Hello World") require.NoError(t, err) levelFilter := testlog.NewLevelFilter(log.LevelInfo) msgFilter := testlog.NewMessageFilter("Hello World") @@ -157,25 +158,25 @@ func TestFindStartingSnapshot(t *testing.T) { t.Run("UsePrestateWhenSnapshotsDirDoesNotExist", func(t *testing.T) { dir := t.TempDir() - snapshot, err := findStartingSnapshot(logger, filepath.Join(dir, "doesNotExist"), execTestCannonPrestate, 1200) + snapshot, err := utils.FindStartingSnapshot(logger, filepath.Join(dir, "doesNotExist"), execTestCannonPrestate, 1200) require.NoError(t, err) require.Equal(t, execTestCannonPrestate, snapshot) }) t.Run("UsePrestateWhenSnapshotsDirEmpty", func(t *testing.T) { dir := withSnapshots(t) - snapshot, err := findStartingSnapshot(logger, dir, execTestCannonPrestate, 1200) + snapshot, err := utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 1200) require.NoError(t, err) require.Equal(t, execTestCannonPrestate, snapshot) }) t.Run("UsePrestateWhenNoSnapshotBeforeTraceIndex", func(t *testing.T) { dir := withSnapshots(t, "100.json", "200.json") - snapshot, err := findStartingSnapshot(logger, dir, execTestCannonPrestate, 99) + snapshot, err := utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 99) require.NoError(t, err) require.Equal(t, execTestCannonPrestate, snapshot) - snapshot, err = findStartingSnapshot(logger, dir, execTestCannonPrestate, 100) + snapshot, err = utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 100) require.NoError(t, err) require.Equal(t, execTestCannonPrestate, snapshot) }) @@ -183,19 +184,19 @@ func TestFindStartingSnapshot(t *testing.T) { t.Run("UseClosestAvailableSnapshot", func(t *testing.T) { dir := withSnapshots(t, "100.json.gz", "123.json.gz", "250.json.gz") - snapshot, err := findStartingSnapshot(logger, dir, execTestCannonPrestate, 101) + snapshot, err := utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 101) require.NoError(t, err) require.Equal(t, filepath.Join(dir, "100.json.gz"), snapshot) - snapshot, err = findStartingSnapshot(logger, dir, execTestCannonPrestate, 123) + snapshot, err = utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 123) require.NoError(t, err) require.Equal(t, filepath.Join(dir, "100.json.gz"), snapshot) - snapshot, err = findStartingSnapshot(logger, dir, execTestCannonPrestate, 124) + snapshot, err = utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 124) require.NoError(t, err) require.Equal(t, filepath.Join(dir, "123.json.gz"), snapshot) - snapshot, err = findStartingSnapshot(logger, dir, execTestCannonPrestate, 256) + snapshot, err = utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 256) require.NoError(t, err) require.Equal(t, filepath.Join(dir, "250.json.gz"), snapshot) }) @@ -203,14 +204,14 @@ func TestFindStartingSnapshot(t *testing.T) { 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, execTestCannonPrestate, 150) + snapshot, err := utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 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, execTestCannonPrestate, 150) + snapshot, err := utils.FindStartingSnapshot(logger, dir, execTestCannonPrestate, 150) require.NoError(t, err) require.Equal(t, filepath.Join(dir, "100.json.gz"), snapshot) }) diff --git a/op-challenger/game/fault/trace/cannon/prestate_test.go b/op-challenger/game/fault/trace/cannon/prestate_test.go index 1f14ebac118e..1297da54bd88 100644 --- a/op-challenger/game/fault/trace/cannon/prestate_test.go +++ b/op-challenger/game/fault/trace/cannon/prestate_test.go @@ -60,7 +60,7 @@ func TestAbsolutePreStateCommitment(t *testing.T) { }) t.Run("CacheAbsolutePreState", func(t *testing.T) { - setupPreState(t, dataDir, "state.json") + setupPreState(t, dataDir, prestate) provider := newCannonPrestateProvider(dataDir, prestate) first, err := provider.AbsolutePreStateCommitment(context.Background()) 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 bd71ce2a4db5..95842ee51baf 100644 --- a/op-challenger/game/fault/trace/cannon/provider.go +++ b/op-challenger/game/fault/trace/cannon/provider.go @@ -9,11 +9,10 @@ import ( "math" "os" "path/filepath" - "strconv" "github.com/ethereum-optimism/optimism/op-challenger/config" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "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" @@ -23,57 +22,34 @@ import ( "github.com/ethereum-optimism/optimism/cannon/mipsevm" ) -const ( - proofsDir = "proofs" - diskStateCache = "state.json.gz" -) - -type proofData struct { - ClaimValue common.Hash `json:"post"` - StateData hexutil.Bytes `json:"state-data"` - ProofData hexutil.Bytes `json:"proof-data"` - OracleKey hexutil.Bytes `json:"oracle-key,omitempty"` - OracleValue hexutil.Bytes `json:"oracle-value,omitempty"` - OracleOffset uint32 `json:"oracle-offset,omitempty"` -} - type CannonMetricer interface { RecordCannonExecutionTime(t float64) } -type ProofGenerator interface { - // GenerateProof executes cannon to generate a proof at the specified trace index in dataDir. - GenerateProof(ctx context.Context, dataDir string, proofAt uint64) error -} - type CannonTraceProvider struct { logger log.Logger dir string prestate string - generator ProofGenerator + generator utils.ProofGenerator gameDepth types.Depth - preimageLoader *preimageLoader + preimageLoader *utils.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 CannonMetricer, cfg *config.Config, localInputs LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProvider { +func NewTraceProvider(logger log.Logger, m CannonMetricer, cfg *config.Config, localInputs utils.LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProvider { return &CannonTraceProvider{ logger: logger, dir: dir, prestate: cfg.CannonAbsolutePreState, generator: NewExecutor(logger, m, cfg, localInputs), gameDepth: gameDepth, - preimageLoader: newPreimageLoader(kvstore.NewDiskKV(preimageDir(dir)).Get), + preimageLoader: utils.NewPreimageLoader(kvstore.NewDiskKV(utils.PreimageDir(dir)).Get), } } -func (p *CannonTraceProvider) SetMaxDepth(gameDepth types.Depth) { - p.gameDepth = gameDepth -} - func (p *CannonTraceProvider) Get(ctx context.Context, pos types.Position) (common.Hash, error) { traceIndex := pos.TraceIndex(p.gameDepth) if !traceIndex.IsUint64() { @@ -137,10 +113,10 @@ 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) (*utils.ProofData, error) { // Attempt to read the last step from disk cache if p.lastStep == 0 { - step, err := readLastStep(p.dir) + step, err := utils.ReadLastStep(p.dir) if err != nil { p.logger.Warn("Failed to read last step from disk cache", "err", err) } else { @@ -151,7 +127,7 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa if p.lastStep != 0 && i > p.lastStep { i = p.lastStep } - path := filepath.Join(p.dir, proofsDir, fmt.Sprintf("%d.json.gz", i)) + path := filepath.Join(p.dir, utils.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 { @@ -177,7 +153,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 := &utils.ProofData{ ClaimValue: witnessHash, StateData: hexutil.Bytes(witness), ProofData: []byte{}, @@ -185,7 +161,7 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa OracleValue: nil, OracleOffset: 0, } - if err := writeLastStep(p.dir, proof, p.lastStep); err != nil { + if err := utils.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 @@ -198,7 +174,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 utils.ProofData err = json.NewDecoder(file).Decode(&proof) if err != nil { return nil, fmt.Errorf("failed to read proof (%v): %w", path, err) @@ -207,94 +183,32 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa } func (c *CannonTraceProvider) finalState() (*mipsevm.State, error) { - state, err := parseState(filepath.Join(c.dir, finalState)) + state, err := parseState(filepath.Join(c.dir, utils.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 *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 -} - // CannonTraceProviderForTest is a CannonTraceProvider that can find the step referencing the preimage read // Only to be used for testing type CannonTraceProviderForTest struct { *CannonTraceProvider } -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 CannonMetricer, cfg *config.Config, localInputs LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProviderForTest { +func NewTraceProviderForTest(logger log.Logger, m CannonMetricer, cfg *config.Config, localInputs utils.LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProviderForTest { p := &CannonTraceProvider{ logger: logger, dir: dir, prestate: cfg.CannonAbsolutePreState, generator: NewExecutor(logger, m, cfg, localInputs), gameDepth: gameDepth, - preimageLoader: newPreimageLoader(kvstore.NewDiskKV(preimageDir(dir)).Get), + preimageLoader: utils.NewPreimageLoader(kvstore.NewDiskKV(utils.PreimageDir(dir)).Get), } return &CannonTraceProviderForTest{p} } -func (p *CannonTraceProviderForTest) FindStep(ctx context.Context, start uint64, preimage PreimageOpt) (uint64, error) { +func (p *CannonTraceProviderForTest) FindStep(ctx context.Context, start uint64, preimage utils.PreimageOpt) (uint64, error) { // Run cannon 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 cannon trace (until preimage read): %w", err) diff --git a/op-challenger/game/fault/trace/cannon/provider_test.go b/op-challenger/game/fault/trace/cannon/provider_test.go index a98b59adf6e8..ec102309c16e 100644 --- a/op-challenger/game/fault/trace/cannon/provider_test.go +++ b/op-challenger/game/fault/trace/cannon/provider_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/ethereum-optimism/optimism/cannon/mipsevm" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "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" @@ -110,7 +111,7 @@ func TestGetStepData(t *testing.T) { Step: 10, Exited: true, } - generator.proof = &proofData{ + generator.proof = &utils.ProofData{ ClaimValue: common.Hash{0xaa}, StateData: []byte{0xbb}, ProofData: []byte{0xcc}, @@ -136,7 +137,7 @@ func TestGetStepData(t *testing.T) { Step: 10, Exited: true, } - generator.proof = &proofData{ + generator.proof = &utils.ProofData{ ClaimValue: common.Hash{0xaa}, StateData: []byte{0xbb}, ProofData: []byte{0xcc}, @@ -162,7 +163,7 @@ func TestGetStepData(t *testing.T) { Step: 10, Exited: true, } - initGenerator.proof = &proofData{ + initGenerator.proof = &utils.ProofData{ ClaimValue: common.Hash{0xaa}, StateData: []byte{0xbb}, ProofData: []byte{0xcc}, @@ -180,7 +181,7 @@ func TestGetStepData(t *testing.T) { Step: 10, Exited: true, } - generator.proof = &proofData{ + generator.proof = &utils.ProofData{ ClaimValue: common.Hash{0xaa}, StateData: []byte{0xbb}, ProofData: []byte{0xcc}, @@ -221,12 +222,13 @@ func setupTestData(t *testing.T) (string, string) { entries, err := testData.ReadDir(srcDir) require.NoError(t, err) dataDir := t.TempDir() - require.NoError(t, os.Mkdir(filepath.Join(dataDir, proofsDir), 0o777)) + require.NoError(t, os.Mkdir(filepath.Join(dataDir, utils.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) + proofFile := filepath.Join(dataDir, utils.ProofsDir, entry.Name()+".gz") + err = ioutil.WriteCompressedBytes(proofFile, file, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) require.NoErrorf(t, err, "writing %v", path) } return dataDir, "state.json" @@ -246,36 +248,30 @@ 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 *utils.ProofData } func (e *stubGenerator) GenerateProof(ctx context.Context, dir string, i uint64) error { e.generated = append(e.generated, int(i)) + var proofFile string + var data []byte + var err error if e.finalState != nil && e.finalState.Step <= i { // Requesting a trace index past the end of the trace - data, err := json.Marshal(e.finalState) + proofFile = filepath.Join(dir, utils.FinalState) + data, err = json.Marshal(e.finalState) if err != nil { return err } - return writeGzip(filepath.Join(dir, finalState), data) + return ioutil.WriteCompressedBytes(proofFile, data, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) } if e.proof != nil { - proofFile := filepath.Join(dir, proofsDir, fmt.Sprintf("%d.json.gz", i)) - data, err := json.Marshal(e.proof) + proofFile = filepath.Join(dir, utils.ProofsDir, fmt.Sprintf("%d.json.gz", i)) + data, err = json.Marshal(e.proof) if err != nil { return err } - return writeGzip(proofFile, data) + return ioutil.WriteCompressedBytes(proofFile, data, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) } 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/cannon/cannon_state.go b/op-challenger/game/fault/trace/cannon/state.go similarity index 100% rename from op-challenger/game/fault/trace/cannon/cannon_state.go rename to op-challenger/game/fault/trace/cannon/state.go diff --git a/op-challenger/game/fault/trace/cannon/cannon_state_test.go b/op-challenger/game/fault/trace/cannon/state_test.go similarity index 100% rename from op-challenger/game/fault/trace/cannon/cannon_state_test.go rename to op-challenger/game/fault/trace/cannon/state_test.go 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 000000000000..f4c119b93e9c --- /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/split" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" + "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 utils.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 := utils.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/trace/outputs/output_cannon.go b/op-challenger/game/fault/trace/outputs/output_cannon.go index 03c2bfb902e5..35d626bc04da 100644 --- a/op-challenger/game/fault/trace/outputs/output_cannon.go +++ b/op-challenger/game/fault/trace/outputs/output_cannon.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace" "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/trace/utils" "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" @@ -21,7 +22,7 @@ func NewOutputCannonTraceAccessor( logger log.Logger, m metrics.Metricer, cfg *config.Config, - l2Client cannon.L2HeaderSource, + l2Client utils.L2HeaderSource, prestateProvider types.PrestateProvider, rollupClient OutputRollupClient, dir string, @@ -34,7 +35,7 @@ func NewOutputCannonTraceAccessor( cannonCreator := 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) + localInputs, err := utils.FetchLocalInputsFromProposals(ctx, l1Head.Hash, l2Client, agreed, claimed) if err != nil { return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err) } diff --git a/op-challenger/game/fault/trace/utils/executor.go b/op-challenger/game/fault/trace/utils/executor.go new file mode 100644 index 000000000000..f3c5feac8311 --- /dev/null +++ b/op-challenger/game/fault/trace/utils/executor.go @@ -0,0 +1,81 @@ +package utils + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum/go-ethereum/log" +) + +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 + +const ( + SnapsDir = "snapshots" + PreimagesDir = "preimages" + FinalState = "final.json.gz" +) + +var snapshotNameRegexp = regexp.MustCompile(`^[0-9]+\.json.gz$`) + +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 FPVM 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/cannon/local.go b/op-challenger/game/fault/trace/utils/local.go similarity index 99% rename from op-challenger/game/fault/trace/cannon/local.go rename to op-challenger/game/fault/trace/utils/local.go index 7f44886fd387..c84087b4c631 100644 --- a/op-challenger/game/fault/trace/cannon/local.go +++ b/op-challenger/game/fault/trace/utils/local.go @@ -1,4 +1,4 @@ -package cannon +package utils import ( "context" diff --git a/op-challenger/game/fault/trace/cannon/local_test.go b/op-challenger/game/fault/trace/utils/local_test.go similarity index 99% rename from op-challenger/game/fault/trace/cannon/local_test.go rename to op-challenger/game/fault/trace/utils/local_test.go index 4248b4dc6226..66fb40352fe6 100644 --- a/op-challenger/game/fault/trace/cannon/local_test.go +++ b/op-challenger/game/fault/trace/utils/local_test.go @@ -1,4 +1,4 @@ -package cannon +package utils import ( "context" diff --git a/op-challenger/game/fault/trace/cannon/preimage.go b/op-challenger/game/fault/trace/utils/preimage.go similarity index 92% rename from op-challenger/game/fault/trace/cannon/preimage.go rename to op-challenger/game/fault/trace/utils/preimage.go index 60bb6ec6fad2..b13333a566f6 100644 --- a/op-challenger/game/fault/trace/cannon/preimage.go +++ b/op-challenger/game/fault/trace/utils/preimage.go @@ -1,4 +1,4 @@ -package cannon +package utils import ( "bytes" @@ -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/utils/preimage_test.go similarity index 91% rename from op-challenger/game/fault/trace/cannon/preimage_test.go rename to op-challenger/game/fault/trace/utils/preimage_test.go index 94fbbc35bc88..e2c9e87ede75 100644 --- a/op-challenger/game/fault/trace/cannon/preimage_test.go +++ b/op-challenger/game/fault/trace/utils/preimage_test.go @@ -1,4 +1,4 @@ -package cannon +package utils import ( "crypto/sha256" @@ -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/utils/provider.go b/op-challenger/game/fault/trace/utils/provider.go new file mode 100644 index 000000000000..3417b8386318 --- /dev/null +++ b/op-challenger/game/fault/trace/utils/provider.go @@ -0,0 +1,96 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "strconv" + + preimage "github.com/ethereum-optimism/optimism/op-preimage" + "github.com/ethereum-optimism/optimism/op-service/ioutil" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +const ( + ProofsDir = "proofs" + diskStateCache = "state.json.gz" +) + +type ProofData struct { + ClaimValue common.Hash `json:"post"` + StateData hexutil.Bytes `json:"state-data"` + ProofData hexutil.Bytes `json:"proof-data"` + OracleKey hexutil.Bytes `json:"oracle-key,omitempty"` + OracleValue hexutil.Bytes `json:"oracle-value,omitempty"` + OracleOffset uint32 `json:"oracle-offset,omitempty"` +} + +type ProofGenerator interface { + // GenerateProof executes FPVM binary to generate a proof at the specified trace index in dataDir. + GenerateProof(ctx context.Context, dataDir string, proofAt uint64) error +} + +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 *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 +} + +// below methods and definitions are only to be used for testing +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)} + } +} diff --git a/op-challenger/game/fault/types/types.go b/op-challenger/game/fault/types/types.go index 45d265030f2f..d2efe5e3e27a 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 9d90b91b7703..e657a57b57d4 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 e742c6b8225e..7584e614733d 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 ab38bf50466e..5587fb2254cd 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 a56aa4fbd0ec..69cc470f55d6 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"), }, } diff --git a/op-e2e/e2eutils/challenger/helper.go b/op-e2e/e2eutils/challenger/helper.go index a662aff560fb..1bef7b66c7f5 100644 --- a/op-e2e/e2eutils/challenger/helper.go +++ b/op-e2e/e2eutils/challenger/helper.go @@ -93,7 +93,7 @@ func applyCannonConfig( l2Endpoint string, ) { require := require.New(t) - c.CannonL2 = l2Endpoint + c.L2Rpc = l2Endpoint root := findMonorepoRoot(t) c.CannonBin = root + "cannon/bin/cannon" c.CannonServer = root + "op-program/bin/op-program" diff --git a/op-e2e/e2eutils/disputegame/output_cannon_helper.go b/op-e2e/e2eutils/disputegame/output_cannon_helper.go index 1e9c8e6241f8..059276951f34 100644 --- a/op-e2e/e2eutils/disputegame/output_cannon_helper.go +++ b/op-e2e/e2eutils/disputegame/output_cannon_helper.go @@ -17,6 +17,7 @@ import ( "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/split" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/metrics" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" @@ -130,7 +131,7 @@ func (g *OutputCannonGameHelper) CreateStepPreimageLoadCheck(ctx context.Context // 2. Descending the execution game tree to reach the step that loads the preimage // 3. Asserting that the preimage was indeed loaded by an honest challenger (assuming the preimage is not preloaded) // This expects an odd execution game depth in order for the honest challenger to step on our leaf claim -func (g *OutputCannonGameHelper) ChallengeToPreimageLoad(ctx context.Context, outputRootClaim *ClaimHelper, challengerKey *ecdsa.PrivateKey, preimage cannon.PreimageOpt, preimageCheck PreimageLoadCheck, preloadPreimage bool) { +func (g *OutputCannonGameHelper) ChallengeToPreimageLoad(ctx context.Context, outputRootClaim *ClaimHelper, challengerKey *ecdsa.PrivateKey, preimage utils.PreimageOpt, preimageCheck PreimageLoadCheck, preloadPreimage bool) { // Identifying the first state transition that loads a global preimage provider, _ := g.createCannonTraceProvider(ctx, "sequencer", outputRootClaim, challenger.WithPrivKey(challengerKey)) targetTraceIndex, err := provider.FindStep(ctx, 0, preimage) @@ -229,7 +230,7 @@ func (g *OutputCannonGameHelper) VerifyPreimage(ctx context.Context, outputRootC start := uint64(0) found := false for offset := uint32(0); ; offset += 4 { - preimageOpt := cannon.PreimageLoad(preimageKey, offset) + preimageOpt := utils.PreimageLoad(preimageKey, offset) g.t.Logf("Searching for step with key %x and offset %v", preimageKey.PreimageKey(), offset) targetTraceIndex, err := provider.FindStep(ctx, start, preimageOpt) if errors.Is(err, io.EOF) { @@ -310,7 +311,7 @@ func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context, agreed, disputed, err := outputs.FetchProposals(ctx, outputProvider, pre, post) g.require.NoError(err) g.t.Logf("Using trace between blocks %v and %v\n", agreed.L2BlockNumber, disputed.L2BlockNumber) - localInputs, err := cannon.FetchLocalInputsFromProposals(ctx, l1Head.Hash, l2Client, agreed, disputed) + localInputs, err := utils.FetchLocalInputsFromProposals(ctx, l1Head.Hash, l2Client, agreed, disputed) g.require.NoError(err, "Failed to fetch local inputs") localContext = outputs.CreateLocalContext(pre, post) dir := filepath.Join(cfg.Datadir, "cannon-trace") diff --git a/op-e2e/faultproofs/output_cannon_test.go b/op-e2e/faultproofs/output_cannon_test.go index 28d4817261c3..92a39a8012d2 100644 --- a/op-e2e/faultproofs/output_cannon_test.go +++ b/op-e2e/faultproofs/output_cannon_test.go @@ -6,7 +6,7 @@ import ( "math/big" "testing" - "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" op_e2e "github.com/ethereum-optimism/optimism/op-e2e" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" @@ -252,14 +252,14 @@ func TestOutputCannonStepWithLargePreimage(t *testing.T) { // execution game. We then move to challenge it to induce a large preimage load. sender := sys.Cfg.Secrets.Addresses().Alice preimageLoadCheck := game.CreateStepLargePreimageLoadCheck(ctx, sender) - game.ChallengeToPreimageLoad(ctx, outputRootClaim, sys.Cfg.Secrets.Alice, cannon.PreimageLargerThan(preimage.MinPreimageSize), preimageLoadCheck, false) + game.ChallengeToPreimageLoad(ctx, outputRootClaim, sys.Cfg.Secrets.Alice, utils.PreimageLargerThan(preimage.MinPreimageSize), preimageLoadCheck, false) // The above method already verified the image was uploaded and step called successfully // So we don't waste time resolving the game - that's tested elsewhere. } func TestOutputCannonStepWithPreimage(t *testing.T) { op_e2e.InitParallel(t, op_e2e.UsesCannon) - testPreimageStep := func(t *testing.T, preimageType cannon.PreimageOpt, preloadPreimage bool) { + testPreimageStep := func(t *testing.T, preimageType utils.PreimageOpt, preloadPreimage bool) { op_e2e.InitParallel(t, op_e2e.UsesCannon) ctx := context.Background() @@ -290,12 +290,12 @@ func TestOutputCannonStepWithPreimage(t *testing.T) { for _, preimageType := range preimageConditions { preimageType := preimageType t.Run("non-existing preimage-"+preimageType, func(t *testing.T) { - testPreimageStep(t, cannon.FirstPreimageLoadOfType(preimageType), false) + testPreimageStep(t, utils.FirstPreimageLoadOfType(preimageType), false) }) } // Only test pre-existing images with one type to save runtime t.Run("preimage already exists", func(t *testing.T) { - testPreimageStep(t, cannon.FirstKeccakPreimageLoad(), true) + testPreimageStep(t, utils.FirstKeccakPreimageLoad(), true) }) } @@ -334,7 +334,7 @@ func TestOutputCannonStepWithKZGPointEvaluation(t *testing.T) { // Now the honest challenger is positioned as the defender of the execution game // We then move to challenge it to induce a preimage load preimageLoadCheck := game.CreateStepPreimageLoadCheck(ctx) - game.ChallengeToPreimageLoad(ctx, outputRootClaim, sys.Cfg.Secrets.Alice, cannon.FirstPrecompilePreimageLoad(), preimageLoadCheck, preloadPreimage) + game.ChallengeToPreimageLoad(ctx, outputRootClaim, sys.Cfg.Secrets.Alice, utils.FirstPrecompilePreimageLoad(), preimageLoadCheck, preloadPreimage) // The above method already verified the image was uploaded and step called successfully // So we don't waste time resolving the game - that's tested elsewhere. } diff --git a/op-e2e/faultproofs/precompile_test.go b/op-e2e/faultproofs/precompile_test.go index 388fc6aeba52..dfd9f5d2c14e 100644 --- a/op-e2e/faultproofs/precompile_test.go +++ b/op-e2e/faultproofs/precompile_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum-optimism/optimism/cannon/mipsevm" "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/trace/utils" "github.com/ethereum-optimism/optimism/op-challenger/metrics" op_e2e "github.com/ethereum-optimism/optimism/op-e2e" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" @@ -122,7 +123,7 @@ func TestPrecompiles(t *testing.T) { require.NoError(t, err, "get l1 head block") l1Head := l1HeadBlock.Hash() - inputs := cannon.LocalGameInputs{ + inputs := utils.LocalGameInputs{ L1Head: l1Head, L2Head: l2Head, L2Claim: common.Hash(l2Claim), @@ -134,7 +135,7 @@ func TestPrecompiles(t *testing.T) { } } -func runCannon(t *testing.T, ctx context.Context, sys *op_e2e.System, inputs cannon.LocalGameInputs, l2Node string) { +func runCannon(t *testing.T, ctx context.Context, sys *op_e2e.System, inputs utils.LocalGameInputs, l2Node string) { l1Endpoint := sys.NodeEndpoint("l1") l1Beacon := sys.L1BeaconEndpoint() cannonOpts := challenger.WithCannon(t, sys.RollupCfg(), sys.L2Genesis(), sys.RollupEndpoint(l2Node), sys.NodeEndpoint(l2Node)) diff --git a/op-service/ioutil/gzip.go b/op-service/ioutil/gzip.go index 5af9b99d7841..52fbb10cc4c1 100644 --- a/op-service/ioutil/gzip.go +++ b/op-service/ioutil/gzip.go @@ -36,6 +36,18 @@ func OpenCompressed(file string, flag int, perm os.FileMode) (io.WriteCloser, er return CompressByFileType(file, out), nil } +// WriteCompressedBytes writes a byte slice to the specified file. +// If the filename ends with .gz, a byte slice is compressed and written. +func WriteCompressedBytes(file string, data []byte, flag int, perm os.FileMode) error { + out, err := OpenCompressed(file, flag, perm) + if err != nil { + return err + } + defer out.Close() + _, err = out.Write(data) + return err +} + // WriteCompressedJson writes the object to the specified file as a compressed json object // if the filename ends with .gz. func WriteCompressedJson(file string, obj any) error {