diff --git a/op-e2e/e2eutils/challenger/helper.go b/op-e2e/e2eutils/challenger/helper.go index f155912774958..c7a7687ec127a 100644 --- a/op-e2e/e2eutils/challenger/helper.go +++ b/op-e2e/e2eutils/challenger/helper.go @@ -11,7 +11,6 @@ import ( "testing" "time" - e2econfig "github.com/ethereum-optimism/optimism/op-e2e/config" "github.com/ethereum-optimism/optimism/op-service/crypto" "github.com/ethereum/go-ethereum/ethclient" @@ -32,6 +31,14 @@ import ( "github.com/ethereum-optimism/optimism/op-service/testlog" ) +type PrestateVariant string + +const ( + STCannonVariant PrestateVariant = "" + MTCannonVariant PrestateVariant = "mt64" + InteropVariant PrestateVariant = "interop" +) + type EndpointProvider interface { NodeEndpoint(name string) endpoint.RPC RollupEndpoint(name string) endpoint.RPC @@ -39,9 +46,9 @@ type EndpointProvider interface { } type System interface { - RollupCfg() *rollup.Config - L2Genesis() *core.Genesis - AllocType() e2econfig.AllocType + RollupCfgs() []*rollup.Config + L2Geneses() []*core.Genesis + PrestateVariant() PrestateVariant } type Helper struct { log log.Logger @@ -120,43 +127,47 @@ func FindMonorepoRoot(t *testing.T) string { return "" } -func applyCannonConfig(c *config.Config, t *testing.T, rollupCfg *rollup.Config, l2Genesis *core.Genesis, allocType e2econfig.AllocType) { +func applyCannonConfig(c *config.Config, t *testing.T, rollupCfgs []*rollup.Config, l2Geneses []*core.Genesis, prestateVariant PrestateVariant) { require := require.New(t) root := FindMonorepoRoot(t) c.Cannon.VmBin = root + "cannon/bin/cannon" c.Cannon.Server = root + "op-program/bin/op-program" - if allocType == e2econfig.AllocTypeMTCannon { - t.Log("Using Cannon64 absolute prestate") - c.CannonAbsolutePreState = root + "op-program/bin/prestate-mt64.bin.gz" + t.Logf("Using absolute prestate variant %v", prestateVariant) + if prestateVariant != "" { + c.CannonAbsolutePreState = root + "op-program/bin/prestate-" + string(prestateVariant) + ".bin.gz" } else { c.CannonAbsolutePreState = root + "op-program/bin/prestate.bin.gz" } c.Cannon.SnapshotFreq = 10_000_000 - genesisBytes, err := json.Marshal(l2Genesis) - require.NoError(err, "marshall l2 genesis config") - genesisFile := filepath.Join(c.Datadir, "l2-genesis.json") - require.NoError(os.WriteFile(genesisFile, genesisBytes, 0o644)) - c.Cannon.L2GenesisPaths = []string{genesisFile} - - rollupBytes, err := json.Marshal(rollupCfg) - require.NoError(err, "marshall rollup config") - rollupFile := filepath.Join(c.Datadir, "rollup.json") - require.NoError(os.WriteFile(rollupFile, rollupBytes, 0o644)) - c.Cannon.RollupConfigPaths = []string{rollupFile} + for _, l2Genesis := range l2Geneses { + genesisBytes, err := json.Marshal(l2Genesis) + require.NoError(err, "marshall l2 genesis config") + genesisFile := filepath.Join(c.Datadir, "l2-genesis.json") + require.NoError(os.WriteFile(genesisFile, genesisBytes, 0o644)) + c.Cannon.L2GenesisPaths = append(c.Cannon.L2GenesisPaths, genesisFile) + } + + for _, rollupCfg := range rollupCfgs { + rollupBytes, err := json.Marshal(rollupCfg) + require.NoError(err, "marshall rollup config") + rollupFile := filepath.Join(c.Datadir, "rollup.json") + require.NoError(os.WriteFile(rollupFile, rollupBytes, 0o644)) + c.Cannon.RollupConfigPaths = append(c.Cannon.RollupConfigPaths, rollupFile) + } } func WithCannon(t *testing.T, system System) Option { return func(c *config.Config) { c.TraceTypes = append(c.TraceTypes, types.TraceTypeCannon) - applyCannonConfig(c, t, system.RollupCfg(), system.L2Genesis(), system.AllocType()) + applyCannonConfig(c, t, system.RollupCfgs(), system.L2Geneses(), system.PrestateVariant()) } } func WithPermissioned(t *testing.T, system System) Option { return func(c *config.Config) { c.TraceTypes = append(c.TraceTypes, types.TraceTypePermissioned) - applyCannonConfig(c, t, system.RollupCfg(), system.L2Genesis(), system.AllocType()) + applyCannonConfig(c, t, system.RollupCfgs(), system.L2Geneses(), system.PrestateVariant()) } } diff --git a/op-e2e/e2eutils/disputegame/cannon_helper.go b/op-e2e/e2eutils/disputegame/cannon_helper.go new file mode 100644 index 0000000000000..b49c70c5bed8f --- /dev/null +++ b/op-e2e/e2eutils/disputegame/cannon_helper.go @@ -0,0 +1,368 @@ +package disputegame + +import ( + "context" + "crypto/ecdsa" + "errors" + "io" + "math/big" + "path/filepath" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/preimages" + "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/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" + keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types" + "github.com/ethereum-optimism/optimism/op-challenger/metrics" + "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + preimage "github.com/ethereum-optimism/optimism/op-preimage" + "github.com/ethereum-optimism/optimism/op-service/sources/batching" + "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +type CannonHelper struct { + t *testing.T + require *require.Assertions + client *ethclient.Client + privKey *ecdsa.PrivateKey + system DisputeSystem + splitGame *SplitGameHelper + defaultChallengerOptions func() []challenger.Option +} + +func NewCannonHelper(splitGameHelper *SplitGameHelper, defaultChallengerOptions func() []challenger.Option) *CannonHelper { + return &CannonHelper{ + t: splitGameHelper.T, + require: splitGameHelper.Require, + client: splitGameHelper.Client, + privKey: splitGameHelper.PrivKey, + splitGame: splitGameHelper, + system: splitGameHelper.System, + defaultChallengerOptions: defaultChallengerOptions, + } +} + +func (g *CannonHelper) StartChallenger(ctx context.Context, name string, options ...challenger.Option) *challenger.Helper { + opts := g.defaultChallengerOptions() + opts = append(opts, options...) + c := challenger.NewChallenger(g.t, ctx, g.system, name, opts...) + g.t.Cleanup(func() { + _ = c.Close() + }) + return c +} + +// ChallengePeriod returns the challenge period fetched from the PreimageOracle contract. +// The returned uint64 value is the number of seconds for the challenge period. +func (g *CannonHelper) ChallengePeriod(ctx context.Context) uint64 { + oracle := g.oracle(ctx) + period, err := oracle.ChallengePeriod(ctx) + g.require.NoError(err, "Failed to get challenge period") + return period +} + +// WaitForChallengePeriodStart waits for the challenge period to start for a given large preimage claim. +func (g *CannonHelper) WaitForChallengePeriodStart(ctx context.Context, sender common.Address, data *types.PreimageOracleData) { + timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + err := wait.For(timedCtx, time.Second, func() (bool, error) { + ctx, cancel := context.WithTimeout(timedCtx, 30*time.Second) + defer cancel() + timestamp := g.ChallengePeriodStartTime(ctx, sender, data) + g.t.Log("Waiting for challenge period start", "timestamp", timestamp, "key", data.OracleKey, "game", g.splitGame.Addr) + return timestamp > 0, nil + }) + if err != nil { + g.splitGame.LogGameData(ctx) + g.require.NoErrorf(err, "Failed to get challenge start period for preimage data %v", data) + } +} + +// ChallengePeriodStartTime returns the start time of the challenge period for a given large preimage claim. +// If the returned start time is 0, the challenge period has not started. +func (g *CannonHelper) ChallengePeriodStartTime(ctx context.Context, sender common.Address, data *types.PreimageOracleData) uint64 { + oracle := g.oracle(ctx) + uuid := preimages.NewUUID(sender, data) + metadata, err := oracle.GetProposalMetadata(ctx, rpcblock.Latest, keccakTypes.LargePreimageIdent{ + Claimant: sender, + UUID: uuid, + }) + g.require.NoError(err, "Failed to get proposal metadata") + if len(metadata) == 0 { + return 0 + } + return metadata[0].Timestamp +} + +func (g *CannonHelper) WaitForPreimageInOracle(ctx context.Context, data *types.PreimageOracleData) { + timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + oracle := g.oracle(ctx) + err := wait.For(timedCtx, time.Second, func() (bool, error) { + g.t.Logf("Waiting for preimage (%v) to be present in oracle", common.Bytes2Hex(data.OracleKey)) + return oracle.GlobalDataExists(ctx, data) + }) + g.require.NoErrorf(err, "Did not find preimage (%v) in oracle", common.Bytes2Hex(data.OracleKey)) +} + +func (g *CannonHelper) UploadPreimage(ctx context.Context, data *types.PreimageOracleData) { + oracle := g.oracle(ctx) + tx, err := oracle.AddGlobalDataTx(data) + g.require.NoError(err, "Failed to create preimage upload tx") + transactions.RequireSendTx(g.t, ctx, g.client, tx, g.privKey) +} + +func (g *CannonHelper) oracle(ctx context.Context) contracts.PreimageOracleContract { + oracle, err := g.splitGame.Game.GetOracle(ctx) + g.require.NoError(err, "Failed to create oracle contract") + return oracle +} + +type PreimageLoadCheck func(types.TraceProvider, uint64) error + +func (g *CannonHelper) CreateStepLargePreimageLoadCheck(ctx context.Context, sender common.Address) PreimageLoadCheck { + return func(provider types.TraceProvider, targetTraceIndex uint64) error { + // Fetch the challenge period + challengePeriod := g.ChallengePeriod(ctx) + + // Get the preimage data + execDepth := g.splitGame.ExecDepth(ctx) + _, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex)))) + g.require.NoError(err) + + // Wait until the challenge period has started by checking until the challenge + // period start time is not zero by calling the ChallengePeriodStartTime method + g.WaitForChallengePeriodStart(ctx, sender, preimageData) + + challengePeriodStart := g.ChallengePeriodStartTime(ctx, sender, preimageData) + challengePeriodEnd := challengePeriodStart + challengePeriod + + // Time travel past the challenge period. + g.system.AdvanceTime(time.Duration(challengePeriod) * time.Second) + g.require.NoError(wait.ForBlockWithTimestamp(ctx, g.system.NodeClient("l1"), challengePeriodEnd)) + + // Assert that the preimage was indeed loaded by an honest challenger + g.WaitForPreimageInOracle(ctx, preimageData) + return nil + } +} + +func (g *CannonHelper) CreateStepPreimageLoadCheck(ctx context.Context) PreimageLoadCheck { + return func(provider types.TraceProvider, targetTraceIndex uint64) error { + execDepth := g.splitGame.ExecDepth(ctx) + _, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex)))) + g.require.NoError(err) + g.WaitForPreimageInOracle(ctx, preimageData) + return nil + } +} + +// ChallengeToPreimageLoad challenges the supplied execution root claim by inducing a step that requires a preimage to be loaded +// It does this by: +// 1. Identifying the first state transition that loads a global preimage +// 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 *CannonHelper) 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) + g.require.NoError(err) + + splitDepth := g.splitGame.SplitDepth(ctx) + execDepth := g.splitGame.ExecDepth(ctx) + g.require.NotEqual(outputRootClaim.Position.TraceIndex(execDepth).Uint64(), targetTraceIndex, "cannot move to defend a terminal trace index") + g.require.EqualValues(splitDepth+1, outputRootClaim.Depth(), "supplied claim must be the root of an execution game") + g.require.EqualValues(execDepth%2, 1, "execution game depth must be odd") // since we're challenging the execution root claim + + if preloadPreimage { + _, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex)))) + g.require.NoError(err) + g.UploadPreimage(ctx, preimageData) + g.WaitForPreimageInOracle(ctx, preimageData) + } + + // Descending the execution game tree to reach the step that loads the preimage + bisectTraceIndex := func(claim *ClaimHelper) *ClaimHelper { + execClaimPosition, err := claim.Position.RelativeToAncestorAtDepth(splitDepth + 1) + g.require.NoError(err) + + claimTraceIndex := execClaimPosition.TraceIndex(execDepth).Uint64() + g.t.Logf("Bisecting: Into targetTraceIndex %v: claimIndex=%v at depth=%v. claimPosition=%v execClaimPosition=%v claimTraceIndex=%v", + targetTraceIndex, claim.Index, claim.Depth(), claim.Position, execClaimPosition, claimTraceIndex) + + // We always want to position ourselves such that the challenger generates proofs for the targetTraceIndex as prestate + if execClaimPosition.Depth() == execDepth-1 { + if execClaimPosition.TraceIndex(execDepth).Uint64() == targetTraceIndex { + newPosition := execClaimPosition.Attack() + correct, err := provider.Get(ctx, newPosition) + g.require.NoError(err) + g.t.Logf("Bisecting: Attack correctly for step at newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) + return claim.Attack(ctx, correct) + } else if execClaimPosition.TraceIndex(execDepth).Uint64() > targetTraceIndex { + g.t.Logf("Bisecting: Attack incorrectly for step") + return claim.Attack(ctx, common.Hash{0xdd}) + } else if execClaimPosition.TraceIndex(execDepth).Uint64()+1 == targetTraceIndex { + g.t.Logf("Bisecting: Defend incorrectly for step") + return claim.Defend(ctx, common.Hash{0xcc}) + } else { + newPosition := execClaimPosition.Defend() + correct, err := provider.Get(ctx, newPosition) + g.require.NoError(err) + g.t.Logf("Bisecting: Defend correctly for step at newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) + return claim.Defend(ctx, correct) + } + } + + // Attack or Defend depending on whether the claim we're responding to is to the left or right of the trace index + // Induce the honest challenger to attack or defend depending on whether our new position will be to the left or right of the trace index + if execClaimPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex && claim.Depth() != splitDepth+1 { + newPosition := execClaimPosition.Defend() + if newPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex { + g.t.Logf("Bisecting: Defend correct. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) + correct, err := provider.Get(ctx, newPosition) + g.require.NoError(err) + return claim.Defend(ctx, correct) + } else { + g.t.Logf("Bisecting: Defend incorrect. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) + return claim.Defend(ctx, common.Hash{0xaa}) + } + } else { + newPosition := execClaimPosition.Attack() + if newPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex { + g.t.Logf("Bisecting: Attack correct. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) + correct, err := provider.Get(ctx, newPosition) + g.require.NoError(err) + return claim.Attack(ctx, correct) + } else { + g.t.Logf("Bisecting: Attack incorrect. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) + return claim.Attack(ctx, common.Hash{0xbb}) + } + } + } + + g.splitGame.LogGameData(ctx) + // Initial bisect to put us on defense + mover := bisectTraceIndex(outputRootClaim) + leafClaim := g.splitGame.DefendClaim(ctx, mover, bisectTraceIndex, WithoutWaitingForStep()) + + // Validate that the preimage was loaded correctly + g.require.NoError(preimageCheck(provider, targetTraceIndex)) + + // Now the preimage is available wait for the step call to succeed. + leafClaim.WaitForCountered(ctx) + g.splitGame.LogGameData(ctx) +} + +func (g *CannonHelper) VerifyPreimage(ctx context.Context, outputRootClaim *ClaimHelper, preimageKey preimage.Key) { + execDepth := g.splitGame.ExecDepth(ctx) + + // Identifying the first state transition that loads a global preimage + provider, localContext := g.createCannonTraceProvider(ctx, "sequencer", outputRootClaim, challenger.WithPrivKey(TestKey)) + start := uint64(0) + found := false + for offset := uint32(0); ; offset += 4 { + 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) { + // Did not find any more reads + g.require.True(found, "Should have found at least one preimage read") + g.t.Logf("Searching for step with key %x and offset %v did not find another read", preimageKey.PreimageKey(), offset) + return + } + g.require.NoError(err, "Failed to find step that loads requested preimage") + start = targetTraceIndex + found = true + + g.t.Logf("Target trace index: %v", targetTraceIndex) + pos := types.NewPosition(execDepth, new(big.Int).SetUint64(targetTraceIndex)) + g.require.Equal(targetTraceIndex, pos.TraceIndex(execDepth).Uint64()) + + prestate, proof, oracleData, err := provider.GetStepData(ctx, pos) + g.require.NoError(err, "Failed to get step data") + g.require.NotNil(oracleData, "Should have had required preimage oracle data") + g.require.Equal(common.Hash(preimageKey.PreimageKey()).Bytes(), oracleData.OracleKey, "Must have correct preimage key") + + candidate, err := g.splitGame.Game.UpdateOracleTx(ctx, uint64(outputRootClaim.Index), oracleData) + g.require.NoError(err, "failed to get oracle") + transactions.RequireSendTx(g.t, ctx, g.client, candidate, g.privKey) + + expectedPostState, err := provider.Get(ctx, pos) + g.require.NoError(err, "Failed to get expected post state") + + vm, err := g.splitGame.Game.Vm(ctx) + g.require.NoError(err, "Failed to get VM address") + + abi, err := bindings.MIPSMetaData.GetAbi() + g.require.NoError(err, "Failed to load MIPS ABI") + caller := batching.NewMultiCaller(g.client.Client(), batching.DefaultBatchSize) + result, err := caller.SingleCall(ctx, rpcblock.Latest, &batching.ContractCall{ + Abi: abi, + Addr: vm.Addr(), + Method: "step", + Args: []interface{}{ + prestate, proof, localContext, + }, + From: g.splitGame.Addr, + }) + g.require.NoError(err, "Failed to call step") + actualPostState := result.GetBytes32(0) + g.require.Equal(expectedPostState, common.Hash(actualPostState)) + } +} + +func (g *CannonHelper) createCannonTraceProvider(ctx context.Context, l2Node string, outputRootClaim *ClaimHelper, options ...challenger.Option) (*cannon.CannonTraceProviderForTest, common.Hash) { + splitDepth := g.splitGame.SplitDepth(ctx) + g.require.EqualValues(outputRootClaim.Depth(), splitDepth+1, "outputRootClaim must be the root of an execution game") + + logger := testlog.Logger(g.t, log.LevelInfo).New("role", "CannonTraceProvider", "game", g.splitGame.Addr) + opt := g.defaultChallengerOptions() + opt = append(opt, options...) + cfg := challenger.NewChallengerConfig(g.t, g.system, l2Node, opt...) + + l2Client := g.system.NodeClient(l2Node) + + prestateBlock, poststateBlock, err := g.splitGame.Game.GetBlockRange(ctx) + g.require.NoError(err, "Failed to load block range") + rollupClient := g.system.RollupClient(l2Node) + prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock) + l1Head := g.splitGame.GetL1Head(ctx) + outputProvider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l2Client, l1Head, splitDepth, prestateBlock, poststateBlock) + + var localContext common.Hash + selector := split.NewSplitProviderSelector(outputProvider, splitDepth, func(ctx context.Context, depth types.Depth, pre types.Claim, post types.Claim) (types.TraceProvider, error) { + 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 := utils.FetchLocalInputsFromProposals(ctx, l1Head.Hash, l2Client, agreed, disputed) + g.require.NoError(err, "Failed to fetch local inputs") + localContext = split.CreateLocalContext(pre, post) + dir := filepath.Join(cfg.Datadir, "cannon-trace") + subdir := filepath.Join(dir, localContext.Hex()) + return cannon.NewTraceProviderForTest(logger, metrics.NoopMetrics.ToTypedVmMetrics(types.TraceTypeCannon.String()), cfg, localInputs, subdir, g.splitGame.MaxDepth(ctx)-splitDepth-1), nil + }) + + claims, err := g.splitGame.Game.GetAllClaims(ctx, rpcblock.Latest) + g.require.NoError(err) + game := types.NewGameState(claims, g.splitGame.MaxDepth(ctx)) + + provider, err := selector(ctx, game, game.Claims()[outputRootClaim.ParentIndex], outputRootClaim.Position) + g.require.NoError(err) + translatingProvider := provider.(*trace.TranslatingProvider) + return translatingProvider.Original().(*cannon.CannonTraceProviderForTest), localContext +} diff --git a/op-e2e/e2eutils/disputegame/claim_helper.go b/op-e2e/e2eutils/disputegame/claim_helper.go index a3abf2b79e79f..46b2958c11d0f 100644 --- a/op-e2e/e2eutils/disputegame/claim_helper.go +++ b/op-e2e/e2eutils/disputegame/claim_helper.go @@ -15,14 +15,14 @@ import ( type ClaimHelper struct { require *require.Assertions - game *OutputGameHelper + game *SplitGameHelper Index int64 ParentIndex int Position types.Position claim common.Hash } -func newClaimHelper(game *OutputGameHelper, idx int64, claim types.Claim) *ClaimHelper { +func newClaimHelper(game *SplitGameHelper, idx int64, claim types.Claim) *ClaimHelper { return &ClaimHelper{ require: game.Require, game: game, diff --git a/op-e2e/e2eutils/disputegame/helper.go b/op-e2e/e2eutils/disputegame/helper.go index 658eedffc5145..50d9cd9c4f141 100644 --- a/op-e2e/e2eutils/disputegame/helper.go +++ b/op-e2e/e2eutils/disputegame/helper.go @@ -8,12 +8,10 @@ import ( "testing" "time" - "github.com/ethereum-optimism/optimism/op-e2e/config" - - "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts/metrics" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/super" "github.com/ethereum-optimism/optimism/op-e2e/bindings" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame/preimage" @@ -43,9 +41,11 @@ var ( ) const ( - cannonGameType uint32 = 0 - permissionedGameType uint32 = 1 - alphabetGameType uint32 = 255 + cannonGameType uint32 = 0 + permissionedGameType uint32 = 1 + superCannonGameType uint32 = 4 + superPermissionedGameType uint32 = 5 + alphabetGameType uint32 = 255 ) type GameCfg struct { @@ -75,15 +75,16 @@ func WithFutureProposal() GameOpt { type DisputeSystem interface { L1BeaconEndpoint() endpoint.RestHTTP + SupervisorClient() *sources.SupervisorClient NodeEndpoint(name string) endpoint.RPC NodeClient(name string) *ethclient.Client RollupEndpoint(name string) endpoint.RPC RollupClient(name string) *sources.RollupClient - L1Deployments() *genesis.L1Deployments - RollupCfg() *rollup.Config - L2Genesis() *core.Genesis - AllocType() config.AllocType + DisputeGameFactoryAddr() common.Address + RollupCfgs() []*rollup.Config + L2Geneses() []*core.Genesis + PrestateVariant() challenger.PrestateVariant AdvanceTime(time.Duration) } @@ -97,7 +98,6 @@ type FactoryHelper struct { PrivKey *ecdsa.PrivateKey FactoryAddr common.Address Factory *bindings.DisputeGameFactory - AllocType config.AllocType } type FactoryCfg struct { @@ -118,9 +118,6 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, system DisputeSystem, o chainID, err := client.ChainID(ctx) require.NoError(err) - allocType := system.AllocType() - require.True(allocType.UsesProofs(), "AllocType %v does not support proofs", allocType) - factoryCfg := &FactoryCfg{PrivKey: TestKey} for _, opt := range opts { opt(factoryCfg) @@ -128,8 +125,7 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, system DisputeSystem, o txOpts, err := bind.NewKeyedTransactorWithChainID(factoryCfg.PrivKey, chainID) require.NoError(err) - l1Deployments := system.L1Deployments() - factoryAddr := l1Deployments.DisputeGameFactoryProxy + factoryAddr := system.DisputeGameFactoryAddr() factory, err := bindings.NewDisputeGameFactory(factoryAddr, client) require.NoError(err) @@ -142,7 +138,6 @@ func NewFactoryHelper(t *testing.T, ctx context.Context, system DisputeSystem, o PrivKey: factoryCfg.PrivKey, Factory: factory, FactoryAddr: factoryAddr, - AllocType: allocType, } } @@ -216,9 +211,47 @@ func (h *FactoryHelper) startOutputCannonGameOfType(ctx context.Context, l2Node prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock) provider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l2Client, l1Head, splitDepth, prestateBlock, poststateBlock) - return &OutputCannonGameHelper{ - OutputGameHelper: *NewOutputGameHelper(h.T, h.Require, h.Client, h.Opts, h.PrivKey, game, h.FactoryAddr, createdEvent.DisputeProxy, provider, h.System, h.AllocType), - } + return NewOutputCannonGameHelper(h.T, h.Client, h.Opts, h.PrivKey, game, h.FactoryAddr, createdEvent.DisputeProxy, provider, h.System) +} + +func (h *FactoryHelper) StartSuperCannonGame(ctx context.Context, timestamp uint64, rootClaim common.Hash, opts ...GameOpt) *SuperCannonGameHelper { + return h.startSuperCannonGameOfType(ctx, timestamp, rootClaim, superCannonGameType, opts...) +} + +func (h *FactoryHelper) startSuperCannonGameOfType(ctx context.Context, timestamp uint64, rootClaim common.Hash, gameType uint32, opts ...GameOpt) *SuperCannonGameHelper { + cfg := NewGameCfg(opts...) + logger := testlog.Logger(h.T, log.LevelInfo).New("role", "OutputCannonGameHelper") + rootProvider := h.System.SupervisorClient() + + extraData := h.CreateSuperGameExtraData(ctx, rootProvider, timestamp, cfg) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) + defer cancel() + + tx, err := transactions.PadGasEstimate(h.Opts, 2, func(opts *bind.TransactOpts) (*types.Transaction, error) { + return h.Factory.Create(opts, gameType, rootClaim, extraData) + }) + h.Require.NoError(err, "create fault dispute game") + rcpt, err := wait.ForReceiptOK(ctx, h.Client, tx.Hash()) + h.Require.NoError(err, "wait for create fault dispute game receipt to be OK") + h.Require.Len(rcpt.Logs, 2, "should have emitted a single DisputeGameCreated event") + createdEvent, err := h.Factory.ParseDisputeGameCreated(*rcpt.Logs[1]) + h.Require.NoError(err) + game, err := contracts.NewFaultDisputeGameContract(ctx, metrics.NoopContractMetrics, createdEvent.DisputeProxy, batching.NewMultiCaller(h.Client.Client(), batching.DefaultBatchSize)) + h.Require.NoError(err) + + prestateTimestamp, poststateTimestamp, err := game.GetBlockRange(ctx) + h.Require.NoError(err, "Failed to load starting block number") + splitDepth, err := game.GetSplitDepth(ctx) + h.Require.NoError(err, "Failed to load split depth") + l1Head := h.GetL1Head(ctx, game) + + prestateProvider := super.NewSuperRootPrestateProvider(rootProvider, prestateTimestamp) + rollupCfgs, err := super.NewRollupConfigsFromParsed(h.System.RollupCfgs()...) + require.NoError(h.T, err, "failed to create rollup configs") + provider := super.NewSuperTraceProvider(logger, rollupCfgs, prestateProvider, rootProvider, l1Head, splitDepth, prestateTimestamp, poststateTimestamp) + + return NewSuperCannonGameHelper(h.T, h.Client, h.Opts, h.PrivKey, game, h.FactoryAddr, createdEvent.DisputeProxy, provider, h.System) } func (h *FactoryHelper) GetL1Head(ctx context.Context, game contracts.FaultDisputeGameContract) eth.BlockID { @@ -271,7 +304,7 @@ func (h *FactoryHelper) StartOutputAlphabetGame(ctx context.Context, l2Node stri provider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l2Client, l1Head, splitDepth, prestateBlock, poststateBlock) return &OutputAlphabetGameHelper{ - OutputGameHelper: *NewOutputGameHelper(h.T, h.Require, h.Client, h.Opts, h.PrivKey, game, h.FactoryAddr, createdEvent.DisputeProxy, provider, h.System, h.AllocType), + OutputGameHelper: *NewOutputGameHelper(h.T, h.Require, h.Client, h.Opts, h.PrivKey, game, h.FactoryAddr, createdEvent.DisputeProxy, provider, h.System), } } @@ -283,6 +316,25 @@ func (h *FactoryHelper) CreateBisectionGameExtraData(l2Node string, l2BlockNumbe return extraData } +func (h *FactoryHelper) CreateSuperGameExtraData(ctx context.Context, supervisor *sources.SupervisorClient, timestamp uint64, cfg *GameCfg) []byte { + if !cfg.allowFuture { + timedCtx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + err := wait.For(timedCtx, time.Second, func() (bool, error) { + status, err := supervisor.SyncStatus(ctx) + if err != nil { + return false, err + } + return status.SafeTimestamp >= timestamp, nil + }) + require.NoError(h.T, err, "Safe head did not reach proposal timestamp") + } + h.T.Logf("Creating game with l2 timestamp: %v", timestamp) + extraData := make([]byte, 32) + binary.BigEndian.PutUint64(extraData[24:], timestamp) + return extraData +} + func (h *FactoryHelper) WaitForBlock(l2Node string, l2BlockNumber uint64, cfg *GameCfg) { if cfg.allowFuture { // Proposing a block that doesn't exist yet, so don't perform any checks diff --git a/op-e2e/e2eutils/disputegame/output_alphabet_helper.go b/op-e2e/e2eutils/disputegame/output_alphabet_helper.go index d2e7ec8fe8f75..b9acf24f2fa99 100644 --- a/op-e2e/e2eutils/disputegame/output_alphabet_helper.go +++ b/op-e2e/e2eutils/disputegame/output_alphabet_helper.go @@ -44,7 +44,7 @@ func (g *OutputAlphabetGameHelper) CreateHonestActor(ctx context.Context, l2Node prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock) correctTrace, err := outputs.NewOutputAlphabetTraceAccessor(logger, metrics.NoopMetrics, prestateProvider, rollupClient, l2Client, l1Head, splitDepth, prestateBlock, poststateBlock) g.Require.NoError(err, "Create trace accessor") - return NewOutputHonestHelper(g.T, g.Require, &g.OutputGameHelper, g.Game, correctTrace) + return NewOutputHonestHelper(g.T, g.Require, &g.OutputGameHelper.SplitGameHelper, g.Game, correctTrace) } func (g *OutputAlphabetGameHelper) CreateDishonestHelper(ctx context.Context, l2Node string, defender bool) *DishonestHelper { diff --git a/op-e2e/e2eutils/disputegame/output_cannon_helper.go b/op-e2e/e2eutils/disputegame/output_cannon_helper.go index 1ced9717589a2..533e4f779f4cc 100644 --- a/op-e2e/e2eutils/disputegame/output_cannon_helper.go +++ b/op-e2e/e2eutils/disputegame/output_cannon_helper.go @@ -3,49 +3,41 @@ package disputegame import ( "context" "crypto/ecdsa" - "errors" - "io" - "math/big" "path/filepath" - "time" + "testing" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" - "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/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/trace/vm" - "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/bindings" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" - "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions" - "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" - preimage "github.com/ethereum-optimism/optimism/op-preimage" - "github.com/ethereum-optimism/optimism/op-service/sources/batching" - "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum-optimism/optimism/op-service/testlog" ) type OutputCannonGameHelper struct { OutputGameHelper + CannonHelper } -func (g *OutputCannonGameHelper) StartChallenger(ctx context.Context, name string, options ...challenger.Option) *challenger.Helper { - opts := []challenger.Option{ - challenger.WithCannon(g.T, g.System), - challenger.WithFactoryAddress(g.FactoryAddr), - challenger.WithGameAddress(g.Addr), +func NewOutputCannonGameHelper(t *testing.T, client *ethclient.Client, opts *bind.TransactOpts, key *ecdsa.PrivateKey, game contracts.FaultDisputeGameContract, factoryAddr common.Address, gameAddr common.Address, provider *outputs.OutputTraceProvider, system DisputeSystem) *OutputCannonGameHelper { + outputGameHelper := NewOutputGameHelper(t, require.New(t), client, opts, key, game, factoryAddr, gameAddr, provider, system) + defaultChallengerOptions := func() []challenger.Option { + return []challenger.Option{ + challenger.WithCannon(t, system), + challenger.WithFactoryAddress(factoryAddr), + challenger.WithGameAddress(gameAddr), + } + } + return &OutputCannonGameHelper{ + OutputGameHelper: *outputGameHelper, + CannonHelper: *NewCannonHelper(&outputGameHelper.SplitGameHelper, defaultChallengerOptions), } - opts = append(opts, options...) - c := challenger.NewChallenger(g.T, ctx, g.System, name, opts...) - g.T.Cleanup(func() { - _ = c.Close() - }) - return c } type HonestActorConfig struct { @@ -92,248 +84,5 @@ func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node s accessor, err := outputs.NewOutputCannonTraceAccessor( logger, metrics.NoopMetrics, cfg.Cannon, vm.NewOpProgramServerExecutor(logger), l2Client, prestateProvider, cfg.CannonAbsolutePreState, rollupClient, dir, l1Head, splitDepth, actorCfg.PrestateBlock, actorCfg.PoststateBlock) g.Require.NoError(err, "Failed to create output cannon trace accessor") - return NewOutputHonestHelper(g.T, g.Require, &g.OutputGameHelper, g.Game, accessor) -} - -type PreimageLoadCheck func(types.TraceProvider, uint64) error - -func (g *OutputCannonGameHelper) CreateStepLargePreimageLoadCheck(ctx context.Context, sender common.Address) PreimageLoadCheck { - return func(provider types.TraceProvider, targetTraceIndex uint64) error { - // Fetch the challenge period - challengePeriod := g.ChallengePeriod(ctx) - - // Get the preimage data - execDepth := g.ExecDepth(ctx) - _, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex)))) - g.Require.NoError(err) - - // Wait until the challenge period has started by checking until the challenge - // period start time is not zero by calling the ChallengePeriodStartTime method - g.WaitForChallengePeriodStart(ctx, sender, preimageData) - - challengePeriodStart := g.ChallengePeriodStartTime(ctx, sender, preimageData) - challengePeriodEnd := challengePeriodStart + challengePeriod - - // Time travel past the challenge period. - g.System.AdvanceTime(time.Duration(challengePeriod) * time.Second) - g.Require.NoError(wait.ForBlockWithTimestamp(ctx, g.System.NodeClient("l1"), challengePeriodEnd)) - - // Assert that the preimage was indeed loaded by an honest challenger - g.WaitForPreimageInOracle(ctx, preimageData) - return nil - } -} - -func (g *OutputCannonGameHelper) CreateStepPreimageLoadCheck(ctx context.Context) PreimageLoadCheck { - return func(provider types.TraceProvider, targetTraceIndex uint64) error { - execDepth := g.ExecDepth(ctx) - _, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex)))) - g.Require.NoError(err) - g.WaitForPreimageInOracle(ctx, preimageData) - return nil - } -} - -// ChallengeToPreimageLoad challenges the supplied execution root claim by inducing a step that requires a preimage to be loaded -// It does this by: -// 1. Identifying the first state transition that loads a global preimage -// 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 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) - g.Require.NoError(err) - - splitDepth := g.SplitDepth(ctx) - execDepth := g.ExecDepth(ctx) - g.Require.NotEqual(outputRootClaim.Position.TraceIndex(execDepth).Uint64(), targetTraceIndex, "cannot move to defend a terminal trace index") - g.Require.EqualValues(splitDepth+1, outputRootClaim.Depth(), "supplied claim must be the root of an execution game") - g.Require.EqualValues(execDepth%2, 1, "execution game depth must be odd") // since we're challenging the execution root claim - - if preloadPreimage { - _, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex)))) - g.Require.NoError(err) - g.UploadPreimage(ctx, preimageData) - g.WaitForPreimageInOracle(ctx, preimageData) - } - - // Descending the execution game tree to reach the step that loads the preimage - bisectTraceIndex := func(claim *ClaimHelper) *ClaimHelper { - execClaimPosition, err := claim.Position.RelativeToAncestorAtDepth(splitDepth + 1) - g.Require.NoError(err) - - claimTraceIndex := execClaimPosition.TraceIndex(execDepth).Uint64() - g.T.Logf("Bisecting: Into targetTraceIndex %v: claimIndex=%v at depth=%v. claimPosition=%v execClaimPosition=%v claimTraceIndex=%v", - targetTraceIndex, claim.Index, claim.Depth(), claim.Position, execClaimPosition, claimTraceIndex) - - // We always want to position ourselves such that the challenger generates proofs for the targetTraceIndex as prestate - if execClaimPosition.Depth() == execDepth-1 { - if execClaimPosition.TraceIndex(execDepth).Uint64() == targetTraceIndex { - newPosition := execClaimPosition.Attack() - correct, err := provider.Get(ctx, newPosition) - g.Require.NoError(err) - g.T.Logf("Bisecting: Attack correctly for step at newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) - return claim.Attack(ctx, correct) - } else if execClaimPosition.TraceIndex(execDepth).Uint64() > targetTraceIndex { - g.T.Logf("Bisecting: Attack incorrectly for step") - return claim.Attack(ctx, common.Hash{0xdd}) - } else if execClaimPosition.TraceIndex(execDepth).Uint64()+1 == targetTraceIndex { - g.T.Logf("Bisecting: Defend incorrectly for step") - return claim.Defend(ctx, common.Hash{0xcc}) - } else { - newPosition := execClaimPosition.Defend() - correct, err := provider.Get(ctx, newPosition) - g.Require.NoError(err) - g.T.Logf("Bisecting: Defend correctly for step at newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) - return claim.Defend(ctx, correct) - } - } - - // Attack or Defend depending on whether the claim we're responding to is to the left or right of the trace index - // Induce the honest challenger to attack or defend depending on whether our new position will be to the left or right of the trace index - if execClaimPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex && claim.Depth() != splitDepth+1 { - newPosition := execClaimPosition.Defend() - if newPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex { - g.T.Logf("Bisecting: Defend correct. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) - correct, err := provider.Get(ctx, newPosition) - g.Require.NoError(err) - return claim.Defend(ctx, correct) - } else { - g.T.Logf("Bisecting: Defend incorrect. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) - return claim.Defend(ctx, common.Hash{0xaa}) - } - } else { - newPosition := execClaimPosition.Attack() - if newPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex { - g.T.Logf("Bisecting: Attack correct. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) - correct, err := provider.Get(ctx, newPosition) - g.Require.NoError(err) - return claim.Attack(ctx, correct) - } else { - g.T.Logf("Bisecting: Attack incorrect. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth)) - return claim.Attack(ctx, common.Hash{0xbb}) - } - } - } - - g.LogGameData(ctx) - // Initial bisect to put us on defense - mover := bisectTraceIndex(outputRootClaim) - leafClaim := g.DefendClaim(ctx, mover, bisectTraceIndex, WithoutWaitingForStep()) - - // Validate that the preimage was loaded correctly - g.Require.NoError(preimageCheck(provider, targetTraceIndex)) - - // Now the preimage is available wait for the step call to succeed. - leafClaim.WaitForCountered(ctx) - g.LogGameData(ctx) -} - -func (g *OutputCannonGameHelper) VerifyPreimage(ctx context.Context, outputRootClaim *ClaimHelper, preimageKey preimage.Key) { - execDepth := g.ExecDepth(ctx) - - // Identifying the first state transition that loads a global preimage - provider, localContext := g.createCannonTraceProvider(ctx, "sequencer", outputRootClaim, challenger.WithPrivKey(TestKey)) - start := uint64(0) - found := false - for offset := uint32(0); ; offset += 4 { - 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) { - // Did not find any more reads - g.Require.True(found, "Should have found at least one preimage read") - g.T.Logf("Searching for step with key %x and offset %v did not find another read", preimageKey.PreimageKey(), offset) - return - } - g.Require.NoError(err, "Failed to find step that loads requested preimage") - start = targetTraceIndex - found = true - - g.T.Logf("Target trace index: %v", targetTraceIndex) - pos := types.NewPosition(execDepth, new(big.Int).SetUint64(targetTraceIndex)) - g.Require.Equal(targetTraceIndex, pos.TraceIndex(execDepth).Uint64()) - - prestate, proof, oracleData, err := provider.GetStepData(ctx, pos) - g.Require.NoError(err, "Failed to get step data") - g.Require.NotNil(oracleData, "Should have had required preimage oracle data") - g.Require.Equal(common.Hash(preimageKey.PreimageKey()).Bytes(), oracleData.OracleKey, "Must have correct preimage key") - - candidate, err := g.Game.UpdateOracleTx(ctx, uint64(outputRootClaim.Index), oracleData) - g.Require.NoError(err, "failed to get oracle") - transactions.RequireSendTx(g.T, ctx, g.Client, candidate, g.PrivKey) - - expectedPostState, err := provider.Get(ctx, pos) - g.Require.NoError(err, "Failed to get expected post state") - - vm, err := g.Game.Vm(ctx) - g.Require.NoError(err, "Failed to get VM address") - - abi, err := bindings.MIPSMetaData.GetAbi() - g.Require.NoError(err, "Failed to load MIPS ABI") - caller := batching.NewMultiCaller(g.Client.Client(), batching.DefaultBatchSize) - result, err := caller.SingleCall(ctx, rpcblock.Latest, &batching.ContractCall{ - Abi: abi, - Addr: vm.Addr(), - Method: "step", - Args: []interface{}{ - prestate, proof, localContext, - }, - From: g.Addr, - }) - g.Require.NoError(err, "Failed to call step") - actualPostState := result.GetBytes32(0) - g.Require.Equal(expectedPostState, common.Hash(actualPostState)) - } -} - -func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context, l2Node string, outputRootClaim *ClaimHelper, options ...challenger.Option) (*cannon.CannonTraceProviderForTest, common.Hash) { - splitDepth := g.SplitDepth(ctx) - g.Require.EqualValues(outputRootClaim.Depth(), splitDepth+1, "outputRootClaim must be the root of an execution game") - - logger := testlog.Logger(g.T, log.LevelInfo).New("role", "CannonTraceProvider", "game", g.Addr) - opt := g.defaultChallengerOptions() - opt = append(opt, options...) - cfg := challenger.NewChallengerConfig(g.T, g.System, l2Node, opt...) - - l2Client := g.System.NodeClient(l2Node) - - prestateBlock, poststateBlock, err := g.Game.GetBlockRange(ctx) - g.Require.NoError(err, "Failed to load block range") - rollupClient := g.System.RollupClient(l2Node) - prestateProvider := outputs.NewPrestateProvider(rollupClient, prestateBlock) - l1Head := g.GetL1Head(ctx) - outputProvider := outputs.NewTraceProvider(logger, prestateProvider, rollupClient, l2Client, l1Head, splitDepth, prestateBlock, poststateBlock) - - var localContext common.Hash - selector := split.NewSplitProviderSelector(outputProvider, splitDepth, func(ctx context.Context, depth types.Depth, pre types.Claim, post types.Claim) (types.TraceProvider, error) { - 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 := utils.FetchLocalInputsFromProposals(ctx, l1Head.Hash, l2Client, agreed, disputed) - g.Require.NoError(err, "Failed to fetch local inputs") - localContext = split.CreateLocalContext(pre, post) - dir := filepath.Join(cfg.Datadir, "cannon-trace") - subdir := filepath.Join(dir, localContext.Hex()) - return cannon.NewTraceProviderForTest(logger, metrics.NoopMetrics.ToTypedVmMetrics(types.TraceTypeCannon.String()), cfg, localInputs, subdir, g.MaxDepth(ctx)-splitDepth-1), nil - }) - - claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) - g.Require.NoError(err) - game := types.NewGameState(claims, g.MaxDepth(ctx)) - - provider, err := selector(ctx, game, game.Claims()[outputRootClaim.ParentIndex], outputRootClaim.Position) - g.Require.NoError(err) - translatingProvider := provider.(*trace.TranslatingProvider) - return translatingProvider.Original().(*cannon.CannonTraceProviderForTest), localContext -} - -func (g *OutputCannonGameHelper) defaultChallengerOptions() []challenger.Option { - return []challenger.Option{ - challenger.WithCannon(g.T, g.System), - challenger.WithFactoryAddress(g.FactoryAddr), - challenger.WithGameAddress(g.Addr), - } + return NewOutputHonestHelper(g.T, g.Require, &g.OutputGameHelper.SplitGameHelper, g.Game, accessor) } diff --git a/op-e2e/e2eutils/disputegame/output_game_helper.go b/op-e2e/e2eutils/disputegame/output_game_helper.go index 090d1c8fdb280..a969c593094a1 100644 --- a/op-e2e/e2eutils/disputegame/output_game_helper.go +++ b/op-e2e/e2eutils/disputegame/output_game_helper.go @@ -5,104 +5,54 @@ import ( "crypto/ecdsa" "fmt" "math/big" - "strings" "testing" "time" - "github.com/ethereum-optimism/optimism/op-e2e/config" - "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" - "github.com/ethereum-optimism/optimism/op-challenger/game/fault/preimages" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs" "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" - keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types" - gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" - "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" - "github.com/ethereum-optimism/optimism/op-service/errutil" - "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" ) -const defaultTimeout = 5 * time.Minute - type OutputGameHelper struct { - T *testing.T - Require *require.Assertions - Client *ethclient.Client - Opts *bind.TransactOpts - PrivKey *ecdsa.PrivateKey - Game contracts.FaultDisputeGameContract - FactoryAddr common.Address - Addr common.Address - CorrectOutputProvider *outputs.OutputTraceProvider - System DisputeSystem + SplitGameHelper + ClaimedBlockNumber func(pos types.Position) (uint64, error) } func NewOutputGameHelper(t *testing.T, require *require.Assertions, client *ethclient.Client, opts *bind.TransactOpts, privKey *ecdsa.PrivateKey, - game contracts.FaultDisputeGameContract, factoryAddr common.Address, addr common.Address, correctOutputProvider *outputs.OutputTraceProvider, system DisputeSystem, allocType config.AllocType) *OutputGameHelper { + game contracts.FaultDisputeGameContract, factoryAddr common.Address, addr common.Address, correctOutputProvider *outputs.OutputTraceProvider, system DisputeSystem) *OutputGameHelper { return &OutputGameHelper{ - T: t, - Require: require, - Client: client, - Opts: opts, - PrivKey: privKey, - Game: game, - FactoryAddr: factoryAddr, - Addr: addr, - CorrectOutputProvider: correctOutputProvider, - System: system, + SplitGameHelper: SplitGameHelper{ + T: t, + Require: require, + Client: client, + Opts: opts, + PrivKey: privKey, + Game: game, + FactoryAddr: factoryAddr, + Addr: addr, + CorrectOutputProvider: correctOutputProvider, + System: system, + DescribePosition: func(pos types.Position, splitDepth types.Depth) string { + if pos.Depth() > splitDepth { + return "" + } + blockNum, err := correctOutputProvider.ClaimedBlockNumber(pos) + if err != nil { + return "" + } + return fmt.Sprintf("Block num: %v", blockNum) + }, + }, + ClaimedBlockNumber: correctOutputProvider.ClaimedBlockNumber, } } -type moveCfg struct { - Opts *bind.TransactOpts - ignoreDupes bool -} - -type MoveOpt interface { - Apply(cfg *moveCfg) -} - -type moveOptFn func(c *moveCfg) - -func (f moveOptFn) Apply(c *moveCfg) { - f(c) -} - -func WithTransactOpts(Opts *bind.TransactOpts) MoveOpt { - return moveOptFn(func(c *moveCfg) { - c.Opts = Opts - }) -} - -func WithIgnoreDuplicates() MoveOpt { - return moveOptFn(func(c *moveCfg) { - c.ignoreDupes = true - }) -} - -func (g *OutputGameHelper) SplitDepth(ctx context.Context) types.Depth { - splitDepth, err := g.Game.GetSplitDepth(ctx) - g.Require.NoError(err, "failed to load split depth") - return splitDepth -} - -func (g *OutputGameHelper) ExecDepth(ctx context.Context) types.Depth { - return g.MaxDepth(ctx) - g.SplitDepth(ctx) - 1 -} - -func (g *OutputGameHelper) L2BlockNum(ctx context.Context) uint64 { - _, blockNum, err := g.Game.GetBlockRange(ctx) - g.Require.NoError(err, "failed to load l2 block number") - return blockNum -} - func (g *OutputGameHelper) StartingBlockNum(ctx context.Context) uint64 { blockNum, _, err := g.Game.GetBlockRange(ctx) g.Require.NoError(err, "failed to load starting block number") @@ -118,7 +68,7 @@ func (g *OutputGameHelper) DisputeLastBlock(ctx context.Context) *ClaimHelper { // to execute cannon on. ie the first block the honest and dishonest actors disagree about is the l2 block of the game. func (g *OutputGameHelper) DisputeBlock(ctx context.Context, disputeBlockNum uint64) *ClaimHelper { dishonestValue := g.GetClaimValue(ctx, 0) - correctRootClaim := g.correctOutputRoot(ctx, types.NewPositionFromGIndex(big.NewInt(1))) + correctRootClaim := g.correctClaimValue(ctx, types.NewPositionFromGIndex(big.NewInt(1))) rootIsValid := dishonestValue == correctRootClaim if rootIsValid { // Ensure that the dishonest actor is actually posting invalid roots. @@ -127,25 +77,25 @@ func (g *OutputGameHelper) DisputeBlock(ctx context.Context, disputeBlockNum uin } pos := types.NewPositionFromGIndex(big.NewInt(1)) getClaimValue := func(parentClaim *ClaimHelper, claimPos types.Position) common.Hash { - claimBlockNum, err := g.CorrectOutputProvider.ClaimedBlockNumber(claimPos) + claimBlockNum, err := g.ClaimedBlockNumber(claimPos) g.Require.NoError(err, "failed to calculate claim block number") if claimBlockNum < disputeBlockNum { // Use the correct output root for all claims prior to the dispute block number // This pushes the game to dispute the last block in the range - return g.correctOutputRoot(ctx, claimPos) + return g.correctClaimValue(ctx, claimPos) } if rootIsValid == parentClaim.AgreesWithOutputRoot() { // We are responding to a parent claim that agrees with a valid root, so we're being dishonest return dishonestValue } else { // Otherwise we must be the honest actor so use the correct root - return g.correctOutputRoot(ctx, claimPos) + return g.correctClaimValue(ctx, claimPos) } } claim := g.RootClaim(ctx) for !claim.IsOutputRootLeaf(ctx) { - parentClaimBlockNum, err := g.CorrectOutputProvider.ClaimedBlockNumber(pos) + parentClaimBlockNum, err := g.ClaimedBlockNumber(pos) g.Require.NoError(err, "failed to calculate parent claim block number") if parentClaimBlockNum >= disputeBlockNum { pos = pos.Attack() @@ -158,285 +108,6 @@ func (g *OutputGameHelper) DisputeBlock(ctx context.Context, disputeBlockNum uin return claim } -func (g *OutputGameHelper) RootClaim(ctx context.Context) *ClaimHelper { - claim := g.getClaim(ctx, 0) - return newClaimHelper(g, 0, claim) -} - -func (g *OutputGameHelper) WaitForCorrectOutputRoot(ctx context.Context, claimIdx int64) { - g.WaitForClaimCount(ctx, claimIdx+1) - claim := g.getClaim(ctx, claimIdx) - output := g.correctOutputRoot(ctx, claim.Position) - g.Require.EqualValuesf(output, claim.Value, "Incorrect output root at claim %v at position %v", claimIdx, claim.Position.ToGIndex().Uint64()) -} - -func (g *OutputGameHelper) correctOutputRoot(ctx context.Context, pos types.Position) common.Hash { - outputRoot, err := g.CorrectOutputProvider.Get(ctx, pos) - g.Require.NoErrorf(err, "Failed to get correct output for position %v", pos) - return outputRoot -} - -func (g *OutputGameHelper) MaxClockDuration(ctx context.Context) time.Duration { - duration, err := g.Game.GetMaxClockDuration(ctx) - g.Require.NoError(err, "failed to get max clock duration") - return duration -} - -func (g *OutputGameHelper) WaitForNoAvailableCredit(ctx context.Context, addr common.Address) { - timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) - defer cancel() - err := wait.For(timedCtx, time.Second, func() (bool, error) { - bal, _, err := g.Game.GetCredit(timedCtx, addr) - if err != nil { - return false, err - } - g.T.Log("Waiting for zero available credit", "current", bal, "addr", addr) - return bal.Cmp(big.NewInt(0)) == 0, nil - }) - if err != nil { - g.LogGameData(ctx) - g.Require.NoError(err, "Failed to wait for zero available credit") - } -} - -func (g *OutputGameHelper) AvailableCredit(ctx context.Context, addr common.Address) *big.Int { - credit, _, err := g.Game.GetCredit(ctx, addr) - g.Require.NoErrorf(err, "Failed to fetch available credit for %v", addr) - return credit -} - -func (g *OutputGameHelper) CreditUnlockDuration(ctx context.Context) time.Duration { - _, delay, _, err := g.Game.GetBalanceAndDelay(ctx, rpcblock.Latest) - g.Require.NoError(err, "Failed to get withdrawal delay") - return delay -} - -func (g *OutputGameHelper) WethBalance(ctx context.Context, addr common.Address) *big.Int { - balance, _, _, err := g.Game.GetBalanceAndDelay(ctx, rpcblock.Latest) - g.Require.NoError(err, "Failed to get WETH balance") - return balance -} - -// WaitForClaimCount waits until there are at least count claims in the game. -// This does not check that the number of claims is exactly the specified count to avoid intermittent failures -// where a challenger posts an additional claim before this method sees the number of claims it was waiting for. -func (g *OutputGameHelper) WaitForClaimCount(ctx context.Context, count int64) { - timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) - defer cancel() - err := wait.For(timedCtx, time.Second, func() (bool, error) { - actual, err := g.Game.GetClaimCount(timedCtx) - if err != nil { - return false, err - } - g.T.Log("Waiting for claim count", "current", actual, "expected", count, "game", g.Addr) - return int64(actual) >= count, nil - }) - if err != nil { - g.LogGameData(ctx) - g.Require.NoErrorf(err, "Did not find expected claim count %v", count) - } -} - -type ContractClaim struct { - ParentIndex uint32 - CounteredBy common.Address - Claimant common.Address - Bond *big.Int - Claim [32]byte - Position *big.Int - Clock *big.Int -} - -func (g *OutputGameHelper) MaxDepth(ctx context.Context) types.Depth { - depth, err := g.Game.GetMaxGameDepth(ctx) - g.Require.NoError(err, "Failed to load game depth") - return depth -} - -func (g *OutputGameHelper) waitForClaim(ctx context.Context, timeout time.Duration, errorMsg string, predicate func(claimIdx int64, claim types.Claim) bool) (int64, types.Claim) { - timedCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - var matchedClaim types.Claim - var matchClaimIdx int64 - err := wait.For(timedCtx, time.Second, func() (bool, error) { - claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) - if err != nil { - return false, fmt.Errorf("retrieve all claims: %w", err) - } - // Search backwards because the new claims are at the end and more likely the ones we want. - for i := len(claims) - 1; i >= 0; i-- { - claim := claims[i] - if predicate(int64(i), claim) { - matchClaimIdx = int64(i) - matchedClaim = claim - return true, nil - } - } - return false, nil - }) - if err != nil { // Avoid waiting time capturing game data when there's no error - g.Require.NoErrorf(err, "%v\n%v", errorMsg, g.GameData(ctx)) - } - return matchClaimIdx, matchedClaim -} - -func (g *OutputGameHelper) waitForNoClaim(ctx context.Context, errorMsg string, predicate func(claim types.Claim) bool) { - timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) - defer cancel() - err := wait.For(timedCtx, time.Second, func() (bool, error) { - claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) - if err != nil { - return false, fmt.Errorf("retrieve all claims: %w", err) - } - // Search backwards because the new claims are at the end and more likely the ones we want. - for i := len(claims) - 1; i >= 0; i-- { - claim := claims[i] - if predicate(claim) { - return false, nil - } - } - return true, nil - }) - if err != nil { // Avoid waiting time capturing game data when there's no error - g.Require.NoErrorf(err, "%v\n%v", errorMsg, g.GameData(ctx)) - } -} - -func (g *OutputGameHelper) GetClaimValue(ctx context.Context, claimIdx int64) common.Hash { - g.WaitForClaimCount(ctx, claimIdx+1) - claim := g.getClaim(ctx, claimIdx) - return claim.Value -} - -func (g *OutputGameHelper) getAllClaims(ctx context.Context) []types.Claim { - claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) - g.Require.NoError(err, "Failed to get all claims") - return claims -} - -// getClaim retrieves the claim data for a specific index. -// Note that it is deliberately not exported as tests should use WaitForClaim to avoid race conditions. -func (g *OutputGameHelper) getClaim(ctx context.Context, claimIdx int64) types.Claim { - claimData, err := g.Game.GetClaim(ctx, uint64(claimIdx)) - if err != nil { - g.Require.NoErrorf(err, "retrieve claim %v", claimIdx) - } - return claimData -} - -func (g *OutputGameHelper) WaitForClaimAtDepth(ctx context.Context, depth types.Depth) { - g.waitForClaim( - ctx, - defaultTimeout, - fmt.Sprintf("Could not find claim depth %v", depth), - func(_ int64, claim types.Claim) bool { - return claim.Depth() == depth - }) -} - -func (g *OutputGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered bool) { - maxDepth := g.MaxDepth(ctx) - g.waitForClaim( - ctx, - defaultTimeout, - fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered), - func(_ int64, claim types.Claim) bool { - return claim.Depth() == maxDepth && (claim.CounteredBy != common.Address{}) == countered - }) -} - -func (g *OutputGameHelper) WaitForAllClaimsCountered(ctx context.Context) { - g.waitForNoClaim( - ctx, - "Did not find all claims countered", - func(claim types.Claim) bool { - return claim.CounteredBy == common.Address{} - }) -} - -func (g *OutputGameHelper) Resolve(ctx context.Context) { - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - candidate, err := g.Game.ResolveTx() - g.Require.NoError(err) - transactions.RequireSendTx(g.T, ctx, g.Client, candidate, g.PrivKey) -} - -func (g *OutputGameHelper) Status(ctx context.Context) gameTypes.GameStatus { - status, err := g.Game.GetStatus(ctx) - g.Require.NoError(err) - return status -} - -func (g *OutputGameHelper) WaitForBondModeDecided(ctx context.Context) { - g.T.Logf("Waiting for game %v to have bond mode set", g.Addr) - timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) - defer cancel() - err := wait.For(timedCtx, time.Second, func() (bool, error) { - bondMode, err := g.Game.GetBondDistributionMode(ctx, rpcblock.Latest) - g.Require.NoError(err) - return bondMode != types.UndecidedDistributionMode, nil - }) - g.Require.NoError(err, "Failed to wait for bond mode to be set") -} - -func (g *OutputGameHelper) WaitForGameStatus(ctx context.Context, expected gameTypes.GameStatus) { - g.T.Logf("Waiting for game %v to have status %v", g.Addr, expected) - timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) - defer cancel() - err := wait.For(timedCtx, time.Second, func() (bool, error) { - ctx, cancel := context.WithTimeout(timedCtx, 30*time.Second) - defer cancel() - status, err := g.Game.GetStatus(ctx) - if err != nil { - return false, fmt.Errorf("game status unavailable: %w", err) - } - g.T.Logf("Game %v has state %v, waiting for state %v", g.Addr, status, expected) - return expected == status, nil - }) - g.Require.NoErrorf(err, "wait for Game status. Game state: \n%v", g.GameData(ctx)) -} - -func (g *OutputGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlocks int, untilGameEnds bool) { - g.T.Logf("Waiting for game %v to have no activity for %v blocks", g.Addr, numInactiveBlocks) - headCh := make(chan *gethtypes.Header, 100) - headSub, err := g.Client.SubscribeNewHead(ctx, headCh) - g.Require.NoError(err) - defer headSub.Unsubscribe() - - var lastActiveBlock uint64 - for { - if untilGameEnds && g.Status(ctx) != gameTypes.GameStatusInProgress { - break - } - select { - case head := <-headCh: - if lastActiveBlock == 0 { - lastActiveBlock = head.Number.Uint64() - continue - } else if lastActiveBlock+uint64(numInactiveBlocks) < head.Number.Uint64() { - return - } - block, err := g.Client.BlockByNumber(ctx, head.Number) - g.Require.NoError(err) - numActions := 0 - for _, tx := range block.Transactions() { - if tx.To().Hex() == g.Addr.Hex() { - numActions++ - } - } - if numActions != 0 { - g.T.Logf("Game %v has %v actions in block %d. Resetting inactivity timeout", g.Addr, numActions, block.NumberU64()) - lastActiveBlock = head.Number.Uint64() - } - case err := <-headSub.Err(): - g.Require.NoError(err) - case <-ctx.Done(): - g.Require.Fail("Context canceled", ctx.Err()) - } - } -} - func (g *OutputGameHelper) WaitForL2BlockNumberChallenged(ctx context.Context) { g.T.Logf("Waiting for game %v to have L2 block number challenged", g.Addr) timedCtx, cancel := context.WithTimeout(ctx, 30*time.Second) @@ -446,280 +117,3 @@ func (g *OutputGameHelper) WaitForL2BlockNumberChallenged(ctx context.Context) { }) g.Require.NoError(err, "L2 block number was not challenged in time") } - -// Mover is a function that either attacks or defends the claim at parentClaimIdx -type Mover func(parent *ClaimHelper) *ClaimHelper - -// Stepper is a function that attempts to perform a step against the claim at parentClaimIdx -type Stepper func(parentClaimIdx int64) - -type defendClaimCfg struct { - skipWaitingForStep bool -} - -type DefendClaimOpt func(cfg *defendClaimCfg) - -func WithoutWaitingForStep() DefendClaimOpt { - return func(cfg *defendClaimCfg) { - cfg.skipWaitingForStep = true - } -} - -// DefendClaim uses the supplied Mover to perform moves in an attempt to defend the supplied claim. -// It is assumed that the specified claim is invalid and that an honest op-challenger is already running. -// When the game has reached the maximum depth it waits for the honest challenger to counter the leaf claim with step. -// Returns the final leaf claim -func (g *OutputGameHelper) DefendClaim(ctx context.Context, claim *ClaimHelper, performMove Mover, Opts ...DefendClaimOpt) *ClaimHelper { - g.T.Logf("Defending claim %v at depth %v", claim.Index, claim.Depth()) - cfg := &defendClaimCfg{} - for _, opt := range Opts { - opt(cfg) - } - for !claim.IsMaxDepth(ctx) { - g.LogGameData(ctx) - // Wait for the challenger to counter - claim = claim.WaitForCounterClaim(ctx) - g.LogGameData(ctx) - - // Respond with our own move - claim = performMove(claim) - } - - if !cfg.skipWaitingForStep { - claim.WaitForCountered(ctx) - } - return claim -} - -// ChallengeClaim uses the supplied functions to perform moves and steps in an attempt to challenge the supplied claim. -// It is assumed that the claim being disputed is valid and that an honest op-challenger is already running. -// When the game has reached the maximum depth it calls the Stepper to attempt to counter the leaf claim. -// Since the output root is valid, it should not be possible for the Stepper to call step successfully. -func (g *OutputGameHelper) ChallengeClaim(ctx context.Context, claim *ClaimHelper, performMove Mover, attemptStep Stepper) { - for !claim.IsMaxDepth(ctx) { - g.LogGameData(ctx) - // Perform our move - claim = performMove(claim) - - // Wait for the challenger to counter - g.LogGameData(ctx) - claim = claim.WaitForCounterClaim(ctx) - } - - // Confirm the game has reached max depth and the last claim hasn't been countered - g.WaitForClaimAtMaxDepth(ctx, false) - g.LogGameData(ctx) - - // It's on us to call step if we want to win but shouldn't be possible - attemptStep(claim.Index) -} - -func (g *OutputGameHelper) WaitForNewClaim(ctx context.Context, checkPoint int64) (int64, error) { - return g.waitForNewClaim(ctx, checkPoint, defaultTimeout) -} - -func (g *OutputGameHelper) waitForNewClaim(ctx context.Context, checkPoint int64, timeout time.Duration) (int64, error) { - timedCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - var newClaimLen int64 - err := wait.For(timedCtx, time.Second, func() (bool, error) { - actual, err := g.Game.GetClaimCount(ctx) - if err != nil { - return false, err - } - newClaimLen = int64(actual) - return int64(actual) > checkPoint, nil - }) - return newClaimLen, err -} - -func (g *OutputGameHelper) moveCfg(Opts ...MoveOpt) *moveCfg { - cfg := &moveCfg{ - Opts: g.Opts, - } - for _, opt := range Opts { - opt.Apply(cfg) - } - return cfg -} - -func (g *OutputGameHelper) Attack(ctx context.Context, claimIdx int64, claim common.Hash, Opts ...MoveOpt) { - g.T.Logf("Attacking claim %v with value %v", claimIdx, claim) - cfg := g.moveCfg(Opts...) - - claimData, err := g.Game.GetClaim(ctx, uint64(claimIdx)) - g.Require.NoError(err, "Failed to get claim data") - attackPos := claimData.Position.Attack() - - candidate, err := g.Game.AttackTx(ctx, claimData, claim) - g.Require.NoError(err, "Failed to create tx candidate") - _, _, err = transactions.SendTx(ctx, g.Client, candidate, g.PrivKey) - if err != nil { - if cfg.ignoreDupes && g.hasClaim(ctx, claimIdx, attackPos, claim) { - return - } - g.Require.NoErrorf(err, "Defend transaction failed. Game state: \n%v", g.GameData(ctx)) - } -} - -func (g *OutputGameHelper) Defend(ctx context.Context, claimIdx int64, claim common.Hash, Opts ...MoveOpt) { - g.T.Logf("Defending claim %v with value %v", claimIdx, claim) - cfg := g.moveCfg(Opts...) - - claimData, err := g.Game.GetClaim(ctx, uint64(claimIdx)) - g.Require.NoError(err, "Failed to get claim data") - defendPos := claimData.Position.Defend() - - candidate, err := g.Game.DefendTx(ctx, claimData, claim) - g.Require.NoError(err, "Failed to create tx candidate") - _, _, err = transactions.SendTx(ctx, g.Client, candidate, g.PrivKey) - if err != nil { - if cfg.ignoreDupes && g.hasClaim(ctx, claimIdx, defendPos, claim) { - return - } - g.Require.NoErrorf(err, "Defend transaction failed. Game state: \n%v", g.GameData(ctx)) - } -} - -func (g *OutputGameHelper) hasClaim(ctx context.Context, parentIdx int64, pos types.Position, value common.Hash) bool { - claims := g.getAllClaims(ctx) - for _, claim := range claims { - if int64(claim.ParentContractIndex) == parentIdx && claim.Position.ToGIndex().Cmp(pos.ToGIndex()) == 0 && claim.Value == value { - return true - } - } - return false -} - -// StepFails attempts to call step and verifies that it fails with ValidStep() -func (g *OutputGameHelper) StepFails(ctx context.Context, claimIdx int64, isAttack bool, stateData []byte, proof []byte) { - g.T.Logf("Attempting step against claim %v isAttack: %v", claimIdx, isAttack) - candidate, err := g.Game.StepTx(uint64(claimIdx), isAttack, stateData, proof) - g.Require.NoError(err, "Failed to create tx candidate") - _, _, err = transactions.SendTx(ctx, g.Client, candidate, g.PrivKey, transactions.WithReceiptFail()) - err = errutil.TryAddRevertReason(err) - g.Require.Error(err, "Transaction should fail") - validStepErr := "0xfb4e40dd" - invalidPrestateErr := "0x696550ff" - if !strings.Contains(err.Error(), validStepErr) && !strings.Contains(err.Error(), invalidPrestateErr) { - g.Require.Failf("Revert reason should be abi encoded ValidStep() or InvalidPrestate() but was: %v", err.Error()) - } -} - -// ResolveClaim resolves a single subgame -func (g *OutputGameHelper) ResolveClaim(ctx context.Context, claimIdx int64) { - candidate, err := g.Game.ResolveClaimTx(uint64(claimIdx)) - g.Require.NoError(err, "Failed to create resolve claim candidate tx") - transactions.RequireSendTx(g.T, ctx, g.Client, candidate, g.PrivKey) -} - -// ChallengePeriod returns the challenge period fetched from the PreimageOracle contract. -// The returned uint64 value is the number of seconds for the challenge period. -func (g *OutputGameHelper) ChallengePeriod(ctx context.Context) uint64 { - oracle := g.oracle(ctx) - period, err := oracle.ChallengePeriod(ctx) - g.Require.NoError(err, "Failed to get challenge period") - return period -} - -// WaitForChallengePeriodStart waits for the challenge period to start for a given large preimage claim. -func (g *OutputGameHelper) WaitForChallengePeriodStart(ctx context.Context, sender common.Address, data *types.PreimageOracleData) { - timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) - defer cancel() - err := wait.For(timedCtx, time.Second, func() (bool, error) { - ctx, cancel := context.WithTimeout(timedCtx, 30*time.Second) - defer cancel() - timestamp := g.ChallengePeriodStartTime(ctx, sender, data) - g.T.Log("Waiting for challenge period start", "timestamp", timestamp, "key", data.OracleKey, "game", g.Addr) - return timestamp > 0, nil - }) - if err != nil { - g.LogGameData(ctx) - g.Require.NoErrorf(err, "Failed to get challenge start period for preimage data %v", data) - } -} - -// ChallengePeriodStartTime returns the start time of the challenge period for a given large preimage claim. -// If the returned start time is 0, the challenge period has not started. -func (g *OutputGameHelper) ChallengePeriodStartTime(ctx context.Context, sender common.Address, data *types.PreimageOracleData) uint64 { - oracle := g.oracle(ctx) - uuid := preimages.NewUUID(sender, data) - metadata, err := oracle.GetProposalMetadata(ctx, rpcblock.Latest, keccakTypes.LargePreimageIdent{ - Claimant: sender, - UUID: uuid, - }) - g.Require.NoError(err, "Failed to get proposal metadata") - if len(metadata) == 0 { - return 0 - } - return metadata[0].Timestamp -} - -func (g *OutputGameHelper) WaitForPreimageInOracle(ctx context.Context, data *types.PreimageOracleData) { - timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) - defer cancel() - oracle := g.oracle(ctx) - err := wait.For(timedCtx, time.Second, func() (bool, error) { - g.T.Logf("Waiting for preimage (%v) to be present in oracle", common.Bytes2Hex(data.OracleKey)) - return oracle.GlobalDataExists(ctx, data) - }) - g.Require.NoErrorf(err, "Did not find preimage (%v) in oracle", common.Bytes2Hex(data.OracleKey)) -} - -func (g *OutputGameHelper) UploadPreimage(ctx context.Context, data *types.PreimageOracleData) { - oracle := g.oracle(ctx) - tx, err := oracle.AddGlobalDataTx(data) - g.Require.NoError(err, "Failed to create preimage upload tx") - transactions.RequireSendTx(g.T, ctx, g.Client, tx, g.PrivKey) -} - -func (g *OutputGameHelper) oracle(ctx context.Context) contracts.PreimageOracleContract { - oracle, err := g.Game.GetOracle(ctx) - g.Require.NoError(err, "Failed to create oracle contract") - return oracle -} - -func (g *OutputGameHelper) GameData(ctx context.Context) string { - maxDepth := g.MaxDepth(ctx) - splitDepth := g.SplitDepth(ctx) - claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) - g.Require.NoError(err, "Fetching claims") - info := fmt.Sprintf("Claim count: %v\n", len(claims)) - for i, claim := range claims { - pos := claim.Position - extra := "" - if pos.Depth() <= splitDepth { - blockNum, err := g.CorrectOutputProvider.ClaimedBlockNumber(pos) - if err != nil { - } else { - extra = fmt.Sprintf("Block num: %v", blockNum) - } - } - info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, ClaimHash: %v, Countered By: %v, ParentIndex: %v Claimant: %v Bond: %v %v\n", - i, claim.Position.ToGIndex().Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), claim.Value.Hex(), claim.CounteredBy, claim.ParentContractIndex, claim.Claimant, claim.Bond, extra) - } - l2BlockNum := g.L2BlockNum(ctx) - status, err := g.Game.GetStatus(ctx) - g.Require.NoError(err, "Load game status") - return fmt.Sprintf("Game %v - %v - L2 Block: %v - Split Depth: %v - Max Depth: %v:\n%v\n", - g.Addr, status, l2BlockNum, splitDepth, maxDepth, info) -} - -func (g *OutputGameHelper) LogGameData(ctx context.Context) { - g.T.Log(g.GameData(ctx)) -} - -func (g *OutputGameHelper) Credit(ctx context.Context, addr common.Address) *big.Int { - amt, _, err := g.Game.GetCredit(ctx, addr) - g.Require.NoError(err) - return amt -} - -func (g *OutputGameHelper) GetL1Head(ctx context.Context) eth.BlockID { - l1HeadHash, err := g.Game.GetL1Head(ctx) - g.Require.NoError(err, "Failed to load L1 head") - l1Header, err := g.Client.HeaderByHash(ctx, l1HeadHash) - g.Require.NoError(err, "Failed to load L1 header") - l1Head := eth.HeaderBlockID(l1Header) - return l1Head -} diff --git a/op-e2e/e2eutils/disputegame/output_honest_helper.go b/op-e2e/e2eutils/disputegame/output_honest_helper.go index 917b05912d560..e2fc406ffb94c 100644 --- a/op-e2e/e2eutils/disputegame/output_honest_helper.go +++ b/op-e2e/e2eutils/disputegame/output_honest_helper.go @@ -17,12 +17,12 @@ const getTraceTimeout = 10 * time.Minute type OutputHonestHelper struct { t *testing.T require *require.Assertions - game *OutputGameHelper + game *SplitGameHelper contract contracts.FaultDisputeGameContract correctTrace types.TraceAccessor } -func NewOutputHonestHelper(t *testing.T, require *require.Assertions, game *OutputGameHelper, contract contracts.FaultDisputeGameContract, correctTrace types.TraceAccessor) *OutputHonestHelper { +func NewOutputHonestHelper(t *testing.T, require *require.Assertions, game *SplitGameHelper, contract contracts.FaultDisputeGameContract, correctTrace types.TraceAccessor) *OutputHonestHelper { return &OutputHonestHelper{ t: t, require: require, diff --git a/op-e2e/e2eutils/disputegame/split_game_helper.go b/op-e2e/e2eutils/disputegame/split_game_helper.go new file mode 100644 index 0000000000000..bb9dbc15608dd --- /dev/null +++ b/op-e2e/e2eutils/disputegame/split_game_helper.go @@ -0,0 +1,567 @@ +package disputegame + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + gameTypes "github.com/ethereum-optimism/optimism/op-challenger/game/types" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-service/errutil" + "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum-optimism/optimism/op-service/sources/batching/rpcblock" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +const defaultTimeout = 5 * time.Minute + +type SplitGameHelper struct { + T *testing.T + Require *require.Assertions + Client *ethclient.Client + Opts *bind.TransactOpts + PrivKey *ecdsa.PrivateKey + Game contracts.FaultDisputeGameContract + FactoryAddr common.Address + Addr common.Address + CorrectOutputProvider types.TraceProvider + System DisputeSystem + DescribePosition func(pos types.Position, splitDepth types.Depth) string +} + +type moveCfg struct { + Opts *bind.TransactOpts + ignoreDupes bool +} + +type MoveOpt interface { + Apply(cfg *moveCfg) +} + +type moveOptFn func(c *moveCfg) + +func (f moveOptFn) Apply(c *moveCfg) { + f(c) +} + +func WithTransactOpts(Opts *bind.TransactOpts) MoveOpt { + return moveOptFn(func(c *moveCfg) { + c.Opts = Opts + }) +} + +func WithIgnoreDuplicates() MoveOpt { + return moveOptFn(func(c *moveCfg) { + c.ignoreDupes = true + }) +} + +func (g *SplitGameHelper) L2BlockNum(ctx context.Context) uint64 { + _, blockNum, err := g.Game.GetBlockRange(ctx) + g.Require.NoError(err, "failed to load l2 block number") + return blockNum +} + +func (g *SplitGameHelper) SplitDepth(ctx context.Context) types.Depth { + splitDepth, err := g.Game.GetSplitDepth(ctx) + g.Require.NoError(err, "failed to load split depth") + return splitDepth +} + +func (g *SplitGameHelper) ExecDepth(ctx context.Context) types.Depth { + return g.MaxDepth(ctx) - g.SplitDepth(ctx) - 1 +} + +func (g *SplitGameHelper) RootClaim(ctx context.Context) *ClaimHelper { + claim := g.getClaim(ctx, 0) + return newClaimHelper(g, 0, claim) +} + +func (g *SplitGameHelper) WaitForCorrectClaim(ctx context.Context, claimIdx int64) { + g.WaitForClaimCount(ctx, claimIdx+1) + claim := g.getClaim(ctx, claimIdx) + value := g.correctClaimValue(ctx, claim.Position) + g.Require.EqualValuesf(value, claim.Value, "Incorrect claim value at claim %v at position %v", claimIdx, claim.Position.ToGIndex().Uint64()) +} + +func (g *SplitGameHelper) correctClaimValue(ctx context.Context, pos types.Position) common.Hash { + claim, err := g.CorrectOutputProvider.Get(ctx, pos) + g.Require.NoErrorf(err, "Failed to get correct claim for position %v", pos) + return claim +} + +func (g *SplitGameHelper) MaxClockDuration(ctx context.Context) time.Duration { + duration, err := g.Game.GetMaxClockDuration(ctx) + g.Require.NoError(err, "failed to get max clock duration") + return duration +} + +func (g *SplitGameHelper) WaitForNoAvailableCredit(ctx context.Context, addr common.Address) { + timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + err := wait.For(timedCtx, time.Second, func() (bool, error) { + bal, _, err := g.Game.GetCredit(timedCtx, addr) + if err != nil { + return false, err + } + g.T.Log("Waiting for zero available credit", "current", bal, "addr", addr) + return bal.Cmp(big.NewInt(0)) == 0, nil + }) + if err != nil { + g.LogGameData(ctx) + g.Require.NoError(err, "Failed to wait for zero available credit") + } +} + +func (g *SplitGameHelper) AvailableCredit(ctx context.Context, addr common.Address) *big.Int { + credit, _, err := g.Game.GetCredit(ctx, addr) + g.Require.NoErrorf(err, "Failed to fetch available credit for %v", addr) + return credit +} + +func (g *SplitGameHelper) CreditUnlockDuration(ctx context.Context) time.Duration { + _, delay, _, err := g.Game.GetBalanceAndDelay(ctx, rpcblock.Latest) + g.Require.NoError(err, "Failed to get withdrawal delay") + return delay +} + +func (g *SplitGameHelper) WethBalance(ctx context.Context, addr common.Address) *big.Int { + balance, _, _, err := g.Game.GetBalanceAndDelay(ctx, rpcblock.Latest) + g.Require.NoError(err, "Failed to get WETH balance") + return balance +} + +// WaitForClaimCount waits until there are at least count claims in the game. +// This does not check that the number of claims is exactly the specified count to avoid intermittent failures +// where a challenger posts an additional claim before this method sees the number of claims it was waiting for. +func (g *SplitGameHelper) WaitForClaimCount(ctx context.Context, count int64) { + timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + err := wait.For(timedCtx, time.Second, func() (bool, error) { + actual, err := g.Game.GetClaimCount(timedCtx) + if err != nil { + return false, err + } + g.T.Log("Waiting for claim count", "current", actual, "expected", count, "game", g.Addr) + return int64(actual) >= count, nil + }) + if err != nil { + g.LogGameData(ctx) + g.Require.NoErrorf(err, "Did not find expected claim count %v", count) + } +} + +type ContractClaim struct { + ParentIndex uint32 + CounteredBy common.Address + Claimant common.Address + Bond *big.Int + Claim [32]byte + Position *big.Int + Clock *big.Int +} + +func (g *SplitGameHelper) MaxDepth(ctx context.Context) types.Depth { + depth, err := g.Game.GetMaxGameDepth(ctx) + g.Require.NoError(err, "Failed to load game depth") + return depth +} + +func (g *SplitGameHelper) waitForClaim(ctx context.Context, timeout time.Duration, errorMsg string, predicate func(claimIdx int64, claim types.Claim) bool) (int64, types.Claim) { + timedCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + var matchedClaim types.Claim + var matchClaimIdx int64 + err := wait.For(timedCtx, time.Second, func() (bool, error) { + claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) + if err != nil { + return false, fmt.Errorf("retrieve all claims: %w", err) + } + // Search backwards because the new claims are at the end and more likely the ones we want. + for i := len(claims) - 1; i >= 0; i-- { + claim := claims[i] + if predicate(int64(i), claim) { + matchClaimIdx = int64(i) + matchedClaim = claim + return true, nil + } + } + return false, nil + }) + if err != nil { // Avoid waiting time capturing game data when there's no error + g.Require.NoErrorf(err, "%v\n%v", errorMsg, g.GameData(ctx)) + } + return matchClaimIdx, matchedClaim +} + +func (g *SplitGameHelper) waitForNoClaim(ctx context.Context, errorMsg string, predicate func(claim types.Claim) bool) { + timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + err := wait.For(timedCtx, time.Second, func() (bool, error) { + claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) + if err != nil { + return false, fmt.Errorf("retrieve all claims: %w", err) + } + // Search backwards because the new claims are at the end and more likely the ones we want. + for i := len(claims) - 1; i >= 0; i-- { + claim := claims[i] + if predicate(claim) { + return false, nil + } + } + return true, nil + }) + if err != nil { // Avoid waiting time capturing game data when there's no error + g.Require.NoErrorf(err, "%v\n%v", errorMsg, g.GameData(ctx)) + } +} + +func (g *SplitGameHelper) GetClaimValue(ctx context.Context, claimIdx int64) common.Hash { + g.WaitForClaimCount(ctx, claimIdx+1) + claim := g.getClaim(ctx, claimIdx) + return claim.Value +} + +func (g *SplitGameHelper) getAllClaims(ctx context.Context) []types.Claim { + claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) + g.Require.NoError(err, "Failed to get all claims") + return claims +} + +// getClaim retrieves the claim data for a specific index. +// Note that it is deliberately not exported as tests should use WaitForClaim to avoid race conditions. +func (g *SplitGameHelper) getClaim(ctx context.Context, claimIdx int64) types.Claim { + claimData, err := g.Game.GetClaim(ctx, uint64(claimIdx)) + if err != nil { + g.Require.NoErrorf(err, "retrieve claim %v", claimIdx) + } + return claimData +} + +func (g *SplitGameHelper) WaitForClaimAtDepth(ctx context.Context, depth types.Depth) { + g.waitForClaim( + ctx, + defaultTimeout, + fmt.Sprintf("Could not find claim depth %v", depth), + func(_ int64, claim types.Claim) bool { + return claim.Depth() == depth + }) +} + +func (g *SplitGameHelper) WaitForClaimAtMaxDepth(ctx context.Context, countered bool) { + maxDepth := g.MaxDepth(ctx) + g.waitForClaim( + ctx, + defaultTimeout, + fmt.Sprintf("Could not find claim depth %v with countered=%v", maxDepth, countered), + func(_ int64, claim types.Claim) bool { + return claim.Depth() == maxDepth && (claim.CounteredBy != common.Address{}) == countered + }) +} + +func (g *SplitGameHelper) WaitForAllClaimsCountered(ctx context.Context) { + g.waitForNoClaim( + ctx, + "Did not find all claims countered", + func(claim types.Claim) bool { + return claim.CounteredBy == common.Address{} + }) +} + +func (g *SplitGameHelper) Resolve(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + candidate, err := g.Game.ResolveTx() + g.Require.NoError(err) + transactions.RequireSendTx(g.T, ctx, g.Client, candidate, g.PrivKey) +} + +func (g *SplitGameHelper) Status(ctx context.Context) gameTypes.GameStatus { + status, err := g.Game.GetStatus(ctx) + g.Require.NoError(err) + return status +} + +func (g *SplitGameHelper) WaitForBondModeDecided(ctx context.Context) { + g.T.Logf("Waiting for game %v to have bond mode set", g.Addr) + timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + err := wait.For(timedCtx, time.Second, func() (bool, error) { + bondMode, err := g.Game.GetBondDistributionMode(ctx, rpcblock.Latest) + g.Require.NoError(err) + return bondMode != types.UndecidedDistributionMode, nil + }) + g.Require.NoError(err, "Failed to wait for bond mode to be set") +} + +func (g *SplitGameHelper) WaitForGameStatus(ctx context.Context, expected gameTypes.GameStatus) { + g.T.Logf("Waiting for game %v to have status %v", g.Addr, expected) + timedCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + err := wait.For(timedCtx, time.Second, func() (bool, error) { + ctx, cancel := context.WithTimeout(timedCtx, 30*time.Second) + defer cancel() + status, err := g.Game.GetStatus(ctx) + if err != nil { + return false, fmt.Errorf("game status unavailable: %w", err) + } + g.T.Logf("Game %v has state %v, waiting for state %v", g.Addr, status, expected) + return expected == status, nil + }) + g.Require.NoErrorf(err, "wait for Game status. Game state: \n%v", g.GameData(ctx)) +} + +func (g *SplitGameHelper) WaitForInactivity(ctx context.Context, numInactiveBlocks int, untilGameEnds bool) { + g.T.Logf("Waiting for game %v to have no activity for %v blocks", g.Addr, numInactiveBlocks) + headCh := make(chan *gethtypes.Header, 100) + headSub, err := g.Client.SubscribeNewHead(ctx, headCh) + g.Require.NoError(err) + defer headSub.Unsubscribe() + + var lastActiveBlock uint64 + for { + if untilGameEnds && g.Status(ctx) != gameTypes.GameStatusInProgress { + break + } + select { + case head := <-headCh: + if lastActiveBlock == 0 { + lastActiveBlock = head.Number.Uint64() + continue + } else if lastActiveBlock+uint64(numInactiveBlocks) < head.Number.Uint64() { + return + } + block, err := g.Client.BlockByNumber(ctx, head.Number) + g.Require.NoError(err) + numActions := 0 + for _, tx := range block.Transactions() { + if tx.To().Hex() == g.Addr.Hex() { + numActions++ + } + } + if numActions != 0 { + g.T.Logf("Game %v has %v actions in block %d. Resetting inactivity timeout", g.Addr, numActions, block.NumberU64()) + lastActiveBlock = head.Number.Uint64() + } + case err := <-headSub.Err(): + g.Require.NoError(err) + case <-ctx.Done(): + g.Require.Fail("Context canceled", ctx.Err()) + } + } +} + +func (g *SplitGameHelper) GameData(ctx context.Context) string { + maxDepth := g.MaxDepth(ctx) + splitDepth := g.SplitDepth(ctx) + claims, err := g.Game.GetAllClaims(ctx, rpcblock.Latest) + g.Require.NoError(err, "Fetching claims") + info := fmt.Sprintf("Claim count: %v\n", len(claims)) + for i, claim := range claims { + pos := claim.Position + extra := g.DescribePosition(pos, splitDepth) + info = info + fmt.Sprintf("%v - Position: %v, Depth: %v, IndexAtDepth: %v Trace Index: %v, ClaimHash: %v, Countered By: %v, ParentIndex: %v Claimant: %v Bond: %v %v\n", + i, claim.Position.ToGIndex().Int64(), pos.Depth(), pos.IndexAtDepth(), pos.TraceIndex(maxDepth), claim.Value.Hex(), claim.CounteredBy, claim.ParentContractIndex, claim.Claimant, claim.Bond, extra) + } + l2BlockNum := g.L2BlockNum(ctx) + status, err := g.Game.GetStatus(ctx) + g.Require.NoError(err, "Load game status") + return fmt.Sprintf("Game %v - %v - L2 Block: %v - Split Depth: %v - Max Depth: %v:\n%v\n", + g.Addr, status, l2BlockNum, splitDepth, maxDepth, info) +} + +func (g *SplitGameHelper) LogGameData(ctx context.Context) { + g.T.Log(g.GameData(ctx)) +} + +func (g *SplitGameHelper) Credit(ctx context.Context, addr common.Address) *big.Int { + amt, _, err := g.Game.GetCredit(ctx, addr) + g.Require.NoError(err) + return amt +} + +func (g *SplitGameHelper) GetL1Head(ctx context.Context) eth.BlockID { + l1HeadHash, err := g.Game.GetL1Head(ctx) + g.Require.NoError(err, "Failed to load L1 head") + l1Header, err := g.Client.HeaderByHash(ctx, l1HeadHash) + g.Require.NoError(err, "Failed to load L1 header") + l1Head := eth.HeaderBlockID(l1Header) + return l1Head +} + +// Mover is a function that either attacks or defends the claim at parentClaimIdx +type Mover func(parent *ClaimHelper) *ClaimHelper + +// Stepper is a function that attempts to perform a step against the claim at parentClaimIdx +type Stepper func(parentClaimIdx int64) + +type defendClaimCfg struct { + skipWaitingForStep bool +} + +type DefendClaimOpt func(cfg *defendClaimCfg) + +func WithoutWaitingForStep() DefendClaimOpt { + return func(cfg *defendClaimCfg) { + cfg.skipWaitingForStep = true + } +} + +// DefendClaim uses the supplied Mover to perform moves in an attempt to defend the supplied claim. +// It is assumed that the specified claim is invalid and that an honest op-challenger is already running. +// When the game has reached the maximum depth it waits for the honest challenger to counter the leaf claim with step. +// Returns the final leaf claim +func (g *SplitGameHelper) DefendClaim(ctx context.Context, claim *ClaimHelper, performMove Mover, Opts ...DefendClaimOpt) *ClaimHelper { + g.T.Logf("Defending claim %v at depth %v", claim.Index, claim.Depth()) + cfg := &defendClaimCfg{} + for _, opt := range Opts { + opt(cfg) + } + for !claim.IsMaxDepth(ctx) { + g.LogGameData(ctx) + // Wait for the challenger to counter + claim = claim.WaitForCounterClaim(ctx) + g.LogGameData(ctx) + + // Respond with our own move + claim = performMove(claim) + } + + if !cfg.skipWaitingForStep { + claim.WaitForCountered(ctx) + } + return claim +} + +// ChallengeClaim uses the supplied functions to perform moves and steps in an attempt to challenge the supplied claim. +// It is assumed that the claim being disputed is valid and that an honest op-challenger is already running. +// When the game has reached the maximum depth it calls the Stepper to attempt to counter the leaf claim. +// Since the output root is valid, it should not be possible for the Stepper to call step successfully. +func (g *SplitGameHelper) ChallengeClaim(ctx context.Context, claim *ClaimHelper, performMove Mover, attemptStep Stepper) { + for !claim.IsMaxDepth(ctx) { + g.LogGameData(ctx) + // Perform our move + claim = performMove(claim) + + // Wait for the challenger to counter + g.LogGameData(ctx) + claim = claim.WaitForCounterClaim(ctx) + } + + // Confirm the game has reached max depth and the last claim hasn't been countered + g.WaitForClaimAtMaxDepth(ctx, false) + g.LogGameData(ctx) + + // It's on us to call step if we want to win but shouldn't be possible + attemptStep(claim.Index) +} + +func (g *SplitGameHelper) WaitForNewClaim(ctx context.Context, checkPoint int64) (int64, error) { + return g.waitForNewClaim(ctx, checkPoint, defaultTimeout) +} + +func (g *SplitGameHelper) waitForNewClaim(ctx context.Context, checkPoint int64, timeout time.Duration) (int64, error) { + timedCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + var newClaimLen int64 + err := wait.For(timedCtx, time.Second, func() (bool, error) { + actual, err := g.Game.GetClaimCount(ctx) + if err != nil { + return false, err + } + newClaimLen = int64(actual) + return int64(actual) > checkPoint, nil + }) + return newClaimLen, err +} + +func (g *SplitGameHelper) moveCfg(Opts ...MoveOpt) *moveCfg { + cfg := &moveCfg{ + Opts: g.Opts, + } + for _, opt := range Opts { + opt.Apply(cfg) + } + return cfg +} + +func (g *SplitGameHelper) Attack(ctx context.Context, claimIdx int64, claim common.Hash, Opts ...MoveOpt) { + g.T.Logf("Attacking claim %v with value %v", claimIdx, claim) + cfg := g.moveCfg(Opts...) + + claimData, err := g.Game.GetClaim(ctx, uint64(claimIdx)) + g.Require.NoError(err, "Failed to get claim data") + attackPos := claimData.Position.Attack() + + candidate, err := g.Game.AttackTx(ctx, claimData, claim) + g.Require.NoError(err, "Failed to create tx candidate") + _, _, err = transactions.SendTx(ctx, g.Client, candidate, g.PrivKey) + if err != nil { + if cfg.ignoreDupes && g.hasClaim(ctx, claimIdx, attackPos, claim) { + return + } + g.Require.NoErrorf(err, "Defend transaction failed. Game state: \n%v", g.GameData(ctx)) + } +} + +func (g *SplitGameHelper) Defend(ctx context.Context, claimIdx int64, claim common.Hash, Opts ...MoveOpt) { + g.T.Logf("Defending claim %v with value %v", claimIdx, claim) + cfg := g.moveCfg(Opts...) + + claimData, err := g.Game.GetClaim(ctx, uint64(claimIdx)) + g.Require.NoError(err, "Failed to get claim data") + defendPos := claimData.Position.Defend() + + candidate, err := g.Game.DefendTx(ctx, claimData, claim) + g.Require.NoError(err, "Failed to create tx candidate") + _, _, err = transactions.SendTx(ctx, g.Client, candidate, g.PrivKey) + if err != nil { + if cfg.ignoreDupes && g.hasClaim(ctx, claimIdx, defendPos, claim) { + return + } + g.Require.NoErrorf(err, "Defend transaction failed. Game state: \n%v", g.GameData(ctx)) + } +} + +func (g *SplitGameHelper) hasClaim(ctx context.Context, parentIdx int64, pos types.Position, value common.Hash) bool { + claims := g.getAllClaims(ctx) + for _, claim := range claims { + if int64(claim.ParentContractIndex) == parentIdx && claim.Position.ToGIndex().Cmp(pos.ToGIndex()) == 0 && claim.Value == value { + return true + } + } + return false +} + +// StepFails attempts to call step and verifies that it fails with ValidStep() +func (g *SplitGameHelper) StepFails(ctx context.Context, claimIdx int64, isAttack bool, stateData []byte, proof []byte) { + g.T.Logf("Attempting step against claim %v isAttack: %v", claimIdx, isAttack) + candidate, err := g.Game.StepTx(uint64(claimIdx), isAttack, stateData, proof) + g.Require.NoError(err, "Failed to create tx candidate") + _, _, err = transactions.SendTx(ctx, g.Client, candidate, g.PrivKey, transactions.WithReceiptFail()) + err = errutil.TryAddRevertReason(err) + g.Require.Error(err, "Transaction should fail") + validStepErr := "0xfb4e40dd" + invalidPrestateErr := "0x696550ff" + if !strings.Contains(err.Error(), validStepErr) && !strings.Contains(err.Error(), invalidPrestateErr) { + g.Require.Failf("Revert reason should be abi encoded ValidStep() or InvalidPrestate() but was: %v", err.Error()) + } +} + +// ResolveClaim resolves a single subgame +func (g *SplitGameHelper) ResolveClaim(ctx context.Context, claimIdx int64) { + candidate, err := g.Game.ResolveClaimTx(uint64(claimIdx)) + g.Require.NoError(err, "Failed to create resolve claim candidate tx") + transactions.RequireSendTx(g.T, ctx, g.Client, candidate, g.PrivKey) +} diff --git a/op-e2e/e2eutils/disputegame/super_cannon_helper.go b/op-e2e/e2eutils/disputegame/super_cannon_helper.go new file mode 100644 index 0000000000000..5d1c06be6f31f --- /dev/null +++ b/op-e2e/e2eutils/disputegame/super_cannon_helper.go @@ -0,0 +1,34 @@ +package disputegame + +import ( + "crypto/ecdsa" + "testing" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/super" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +type SuperCannonGameHelper struct { + SuperGameHelper + CannonHelper +} + +func NewSuperCannonGameHelper(t *testing.T, client *ethclient.Client, opts *bind.TransactOpts, key *ecdsa.PrivateKey, game contracts.FaultDisputeGameContract, factoryAddr common.Address, gameAddr common.Address, provider *super.SuperTraceProvider, system DisputeSystem) *SuperCannonGameHelper { + superGameHelper := NewSuperGameHelper(t, require.New(t), client, opts, key, game, factoryAddr, gameAddr, provider, system) + defaultChallengerOptions := func() []challenger.Option { + return []challenger.Option{ + challenger.WithCannon(t, system), + challenger.WithFactoryAddress(factoryAddr), + challenger.WithGameAddress(gameAddr), + } + } + return &SuperCannonGameHelper{ + SuperGameHelper: *superGameHelper, + CannonHelper: *NewCannonHelper(&superGameHelper.SplitGameHelper, defaultChallengerOptions), + } +} diff --git a/op-e2e/e2eutils/disputegame/super_dispute_system.go b/op-e2e/e2eutils/disputegame/super_dispute_system.go new file mode 100644 index 0000000000000..b2aa4c445cb52 --- /dev/null +++ b/op-e2e/e2eutils/disputegame/super_dispute_system.go @@ -0,0 +1,98 @@ +package disputegame + +import ( + "strings" + "time" + + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" + "github.com/ethereum-optimism/optimism/op-e2e/interop" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-service/endpoint" + "github.com/ethereum-optimism/optimism/op-service/sources" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/ethclient" +) + +type SuperDisputeSystem struct { + sys interop.SuperSystem +} + +func (s *SuperDisputeSystem) SupervisorClient() *sources.SupervisorClient { + return s.sys.SupervisorClient() +} + +func NewSuperDisputeSystem(sys interop.SuperSystem) *SuperDisputeSystem { + return &SuperDisputeSystem{sys} +} + +func splitName(name string) (string, string) { + parts := strings.SplitN(name, "/", 2) + if len(parts) != 2 { + panic("Invalid super system name: " + name) + } + return parts[0], parts[1] +} + +func (s *SuperDisputeSystem) L1BeaconEndpoint() endpoint.RestHTTP { + beacon := s.sys.L1Beacon() + return endpoint.RestHTTPURL(beacon.BeaconAddr()) +} + +func (s *SuperDisputeSystem) NodeEndpoint(name string) endpoint.RPC { + if name == "l1" { + return s.sys.L1().UserRPC() + } + network, node := splitName(name) + return s.sys.L2GethEndpoint(network, node) +} + +func (s *SuperDisputeSystem) NodeClient(name string) *ethclient.Client { + if name == "l1" { + return s.sys.L1GethClient() + } + network, node := splitName(name) + return s.sys.L2GethClient(network, node) +} + +func (s *SuperDisputeSystem) RollupEndpoint(name string) endpoint.RPC { + network, node := splitName(name) + return s.sys.L2RollupEndpoint(network, node) +} + +func (s *SuperDisputeSystem) RollupClient(name string) *sources.RollupClient { + network, node := splitName(name) + return s.sys.L2RollupClient(network, node) +} + +func (s *SuperDisputeSystem) DisputeGameFactoryAddr() common.Address { + return s.sys.DisputeGameFactoryAddr() +} + +func (s *SuperDisputeSystem) RollupCfgs() []*rollup.Config { + networks := s.sys.L2IDs() + cfgs := make([]*rollup.Config, len(networks)) + for i, network := range networks { + cfgs[i] = s.sys.RollupConfig(network) + } + return cfgs +} + +func (s *SuperDisputeSystem) L2Geneses() []*core.Genesis { + networks := s.sys.L2IDs() + cfgs := make([]*core.Genesis, len(networks)) + for i, network := range networks { + cfgs[i] = s.sys.L2Genesis(network) + } + return cfgs +} + +func (s *SuperDisputeSystem) PrestateVariant() challenger.PrestateVariant { + return challenger.InteropVariant +} + +func (s *SuperDisputeSystem) AdvanceTime(duration time.Duration) { + s.sys.AdvanceL1Time(duration) +} + +var _ DisputeSystem = (*SuperDisputeSystem)(nil) diff --git a/op-e2e/e2eutils/disputegame/super_game_helper.go b/op-e2e/e2eutils/disputegame/super_game_helper.go new file mode 100644 index 0000000000000..12c8e08bdd744 --- /dev/null +++ b/op-e2e/e2eutils/disputegame/super_game_helper.go @@ -0,0 +1,48 @@ +package disputegame + +import ( + "crypto/ecdsa" + "fmt" + "testing" + + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/super" + "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +type SuperGameHelper struct { + SplitGameHelper +} + +func NewSuperGameHelper(t *testing.T, require *require.Assertions, client *ethclient.Client, opts *bind.TransactOpts, privKey *ecdsa.PrivateKey, + game contracts.FaultDisputeGameContract, factoryAddr common.Address, addr common.Address, correctOutputProvider *super.SuperTraceProvider, system DisputeSystem) *SuperGameHelper { + return &SuperGameHelper{ + SplitGameHelper: SplitGameHelper{ + T: t, + Require: require, + Client: client, + Opts: opts, + PrivKey: privKey, + Game: game, + FactoryAddr: factoryAddr, + Addr: addr, + CorrectOutputProvider: correctOutputProvider, + System: system, + DescribePosition: func(pos types.Position, splitDepth types.Depth) string { + + if pos.Depth() > splitDepth { + return "" + } + timestamp, step, err := correctOutputProvider.ComputeStep(pos) + if err != nil { + return "" + } + return fmt.Sprintf("Timestamp: %v, Step: %v", timestamp, step) + }, + }, + } +} diff --git a/op-e2e/faultproofs/super_test.go b/op-e2e/faultproofs/super_test.go new file mode 100644 index 0000000000000..354001ed71fff --- /dev/null +++ b/op-e2e/faultproofs/super_test.go @@ -0,0 +1,20 @@ +package faultproofs + +import ( + "context" + "testing" + + op_e2e "github.com/ethereum-optimism/optimism/op-e2e" + "github.com/ethereum-optimism/optimism/op-e2e/config" + "github.com/ethereum/go-ethereum/common" +) + +func TestCreateSuperCannonGame(t *testing.T) { + t.Skip("Super cannon game can't yet be deployed with SuperSystem") + op_e2e.InitParallel(t, op_e2e.UsesCannon) + ctx := context.Background() + sys, disputeGameFactory, _ := StartInteropFaultDisputeSystem(t, WithAllocType(config.AllocTypeMTCannon)) + sys.L2IDs() + game := disputeGameFactory.StartSuperCannonGame(ctx, 4, common.Hash{0x01}) + game.LogGameData(ctx) +} diff --git a/op-e2e/faultproofs/util_interop.go b/op-e2e/faultproofs/util_interop.go new file mode 100644 index 0000000000000..29afb3bc6d099 --- /dev/null +++ b/op-e2e/faultproofs/util_interop.go @@ -0,0 +1,44 @@ +package faultproofs + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-chain-ops/interopgen" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/disputegame" + "github.com/ethereum-optimism/optimism/op-e2e/interop" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +func StartInteropFaultDisputeSystem(t *testing.T, opts ...faultDisputeConfigOpts) (interop.SuperSystem, *disputegame.FactoryHelper, *ethclient.Client) { + fdc := new(faultDisputeConfig) + for _, opt := range opts { + opt(fdc) + } + recipe := interopgen.InteropDevRecipe{ + L1ChainID: 900100, + L2ChainIDs: []uint64{900200, 900201}, + GenesisTimestamp: uint64(time.Now().Unix() + 3), // start chain 3 seconds from now + } + worldResources := interop.WorldResourcePaths{ + FoundryArtifacts: "../../packages/contracts-bedrock/forge-artifacts", + SourceMap: "../../packages/contracts-bedrock", + } + superCfg := interop.SuperSystemConfig{ + SupportTimeTravel: true, + } + + hdWallet, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic) + require.NoError(t, err) + l1User := devkeys.ChainUserKeys(new(big.Int).SetUint64(recipe.L1ChainID))(0) + privKey, err := hdWallet.Secret(l1User) + require.NoError(t, err) + s2 := interop.NewSuperSystem(t, &recipe, worldResources, superCfg) + factory := disputegame.NewFactoryHelper(t, context.Background(), disputegame.NewSuperDisputeSystem(s2), + disputegame.WithFactoryPrivKey(privKey)) + return s2, factory, s2.L1GethClient() +} diff --git a/op-e2e/interop/interop_test.go b/op-e2e/interop/interop_test.go index 03168d05cc1d9..67013947f2780 100644 --- a/op-e2e/interop/interop_test.go +++ b/op-e2e/interop/interop_test.go @@ -37,9 +37,9 @@ func setupAndRun(t *testing.T, config SuperSystemConfig, fn func(*testing.T, Sup L2ChainIDs: []uint64{900200, 900201}, GenesisTimestamp: uint64(time.Now().Unix() + 3), // start chain 3 seconds from now } - worldResources := worldResourcePaths{ - foundryArtifacts: "../../packages/contracts-bedrock/forge-artifacts", - sourceMap: "../../packages/contracts-bedrock", + worldResources := WorldResourcePaths{ + FoundryArtifacts: "../../packages/contracts-bedrock/forge-artifacts", + SourceMap: "../../packages/contracts-bedrock", } // create a super system from the recipe diff --git a/op-e2e/interop/supersystem.go b/op-e2e/interop/supersystem.go index bbdbd7cbfcb12..1da8484bf1ecb 100644 --- a/op-e2e/interop/supersystem.go +++ b/op-e2e/interop/supersystem.go @@ -11,7 +11,9 @@ import ( "testing" "time" + "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-service/eth" + "github.com/ethereum/go-ethereum/core" "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -62,6 +64,10 @@ import ( // kurtosis or another testing framework could be implemented type SuperSystem interface { L1() *geth.GethInstance + L1GethClient() *ethclient.Client + L1Beacon() *fakebeacon.FakeBeacon + AdvanceL1Time(duration time.Duration) + DisputeGameFactoryAddr() common.Address // Superchain level L2IDs() []string @@ -72,7 +78,9 @@ type SuperSystem interface { SupervisorClient() *sources.SupervisorClient // L2 client specific + L2GethEndpoint(id string, name string) endpoint.RPC L2GethClient(network string, node string) *ethclient.Client + L2RollupEndpoint(network string, node string) endpoint.RPC L2RollupClient(network string, node string) *sources.RollupClient SendL2Tx(network string, node string, username string, applyTxOpts helpers.TxOptsFn) *types.Receipt EmitData(ctx context.Context, network string, node string, username string, data string) *types.Receipt @@ -80,6 +88,8 @@ type SuperSystem interface { // L2 level ChainID(network string) *big.Int + RollupConfig(network string) *rollup.Config + L2Genesis(network string) *core.Genesis UserKey(nework, username string) ecdsa.PrivateKey L2OperatorKey(network string, role devkeys.ChainOperatorRole) ecdsa.PrivateKey Address(network string, username string) common.Address @@ -97,11 +107,12 @@ type SuperSystem interface { // Access a contract on a network by name } type SuperSystemConfig struct { - mempoolFiltering bool + mempoolFiltering bool + SupportTimeTravel bool } // NewSuperSystem creates a new SuperSystem from a recipe. It creates an interopE2ESystem. -func NewSuperSystem(t *testing.T, recipe *interopgen.InteropDevRecipe, w worldResourcePaths, config SuperSystemConfig) SuperSystem { +func NewSuperSystem(t *testing.T, recipe *interopgen.InteropDevRecipe, w WorldResourcePaths, config SuperSystemConfig) SuperSystem { s2 := &interopE2ESystem{recipe: recipe, config: &config} s2.prepare(t, w) return s2 @@ -115,6 +126,7 @@ type interopE2ESystem struct { t *testing.T recipe *interopgen.InteropDevRecipe logger log.Logger + timeTravelClock *clock.AdvancingClock hdWallet *devkeys.MnemonicDevKeys worldDeployment *interopgen.WorldDeployment worldOutput *interopgen.WorldOutput @@ -132,6 +144,21 @@ func (s *interopE2ESystem) L1() *geth.GethInstance { return s.l1 } +func (s *interopE2ESystem) L1Beacon() *fakebeacon.FakeBeacon { + return s.beacon +} + +func (s *interopE2ESystem) AdvanceL1Time(duration time.Duration) { + require.NotNil(s.t, s.timeTravelClock, "Attempting to time travel without enabling it.") + s.timeTravelClock.AdvanceTime(duration) +} + +func (s *interopE2ESystem) DisputeGameFactoryAddr() common.Address { + // Currently uses the dispute game factory for the first L2 chain. + // Ultimately this should be a factory shared by all chains in the dependency set + return s.worldDeployment.L2s[s.L2IDs()[0]].DisputeGameFactoryProxy +} + // prepareHDWallet creates a new HD wallet to derive keys from func (s *interopE2ESystem) prepareHDWallet() *devkeys.MnemonicDevKeys { hdWallet, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic) @@ -139,13 +166,13 @@ func (s *interopE2ESystem) prepareHDWallet() *devkeys.MnemonicDevKeys { return hdWallet } -type worldResourcePaths struct { - foundryArtifacts string - sourceMap string +type WorldResourcePaths struct { + FoundryArtifacts string + SourceMap string } // prepareWorld creates the world configuration from the recipe and deploys it -func (s *interopE2ESystem) prepareWorld(w worldResourcePaths) (*interopgen.WorldDeployment, *interopgen.WorldOutput) { +func (s *interopE2ESystem) prepareWorld(w WorldResourcePaths) (*interopgen.WorldDeployment, *interopgen.WorldOutput) { // Build the world configuration from the recipe and the HD wallet worldCfg, err := s.recipe.Build(s.hdWallet) require.NoError(s.t, err) @@ -155,8 +182,8 @@ func (s *interopE2ESystem) prepareWorld(w worldResourcePaths) (*interopgen.World require.NoError(s.t, worldCfg.Check(logger)) // create the foundry artifacts and source map - foundryArtifacts := foundry.OpenArtifactsDir(w.foundryArtifacts) - sourceMap := foundry.NewSourceMapFS(os.DirFS(w.sourceMap)) + foundryArtifacts := foundry.OpenArtifactsDir(w.FoundryArtifacts) + sourceMap := foundry.NewSourceMapFS(os.DirFS(w.SourceMap)) // deploy the world, using the logger, foundry artifacts, source map, and world configuration worldDeployment, worldOutput, err := interopgen.Deploy(logger, foundryArtifacts, sourceMap, worldCfg) @@ -182,6 +209,10 @@ func (s *interopE2ESystem) prepareL1() (*fakebeacon.FakeBeacon, *geth.GethInstan l1FinalizedDistance := uint64(3) l1Clock := clock.SystemClock + if s.config.SupportTimeTravel { + s.timeTravelClock = clock.NewAdvancingClock(100 * time.Millisecond) + l1Clock = s.timeTravelClock + } // Start the L1 chain l1Geth, err := geth.InitL1( blockTimeL1, @@ -234,6 +265,14 @@ func (s *interopE2ESystem) ChainID(network string) *big.Int { return s.l2s[network].chainID } +func (s *interopE2ESystem) RollupConfig(network string) *rollup.Config { + return s.l2s[network].l2Out.RollupCfg +} + +func (s *interopE2ESystem) L2Genesis(network string) *core.Genesis { + return s.l2s[network].l2Out.Genesis +} + // prepareSupervisor creates a new supervisor for the system func (s *interopE2ESystem) prepareSupervisor() *supervisor.SupervisorService { // Be verbose with op-supervisor, it's in early test phase @@ -306,7 +345,7 @@ func (s *interopE2ESystem) SupervisorClient() *sources.SupervisorClient { // prepare sets up the system for testing // components are built iteratively, so that they can be reused or modified // their creation can't be safely skipped or reordered at this time -func (s *interopE2ESystem) prepare(t *testing.T, w worldResourcePaths) { +func (s *interopE2ESystem) prepare(t *testing.T, w WorldResourcePaths) { s.t = t s.logger = testlog.Logger(s.t, log.LevelDebug) s.hdWallet = s.prepareHDWallet() diff --git a/op-e2e/interop/supersystem_l2.go b/op-e2e/interop/supersystem_l2.go index bae49792e6978..3a5048beb6a77 100644 --- a/op-e2e/interop/supersystem_l2.go +++ b/op-e2e/interop/supersystem_l2.go @@ -55,6 +55,10 @@ type l2Net struct { nodes map[string]*l2Node } +func (s *interopE2ESystem) L2GethEndpoint(id string, name string) endpoint.RPC { + net := s.l2s[id] + return net.nodes[name].l2Geth.UserRPC() +} func (s *interopE2ESystem) L2GethClient(id string, name string) *ethclient.Client { net := s.l2s[id] node := net.nodes[name] @@ -77,6 +81,12 @@ func (s *interopE2ESystem) L2GethClient(id string, name string) *ethclient.Clien return node.gethClient } +func (s *interopE2ESystem) L2RollupEndpoint(id string, name string) endpoint.RPC { + net := s.l2s[id] + node := net.nodes[name] + return node.opNode.UserRPC() +} + func (s *interopE2ESystem) L2RollupClient(id string, name string) *sources.RollupClient { net := s.l2s[id] node := net.nodes[name] diff --git a/op-e2e/system/e2esys/setup.go b/op-e2e/system/e2esys/setup.go index acd985d224cda..8c63057660fbd 100644 --- a/op-e2e/system/e2esys/setup.go +++ b/op-e2e/system/e2esys/setup.go @@ -16,6 +16,7 @@ import ( "testing" "time" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" "github.com/stretchr/testify/require" @@ -374,6 +375,23 @@ type System struct { clients map[string]*ethclient.Client } +func (sys *System) PrestateVariant() challenger.PrestateVariant { + switch sys.AllocType() { + case config.AllocTypeMTCannon: + return challenger.MTCannonVariant + default: + return challenger.STCannonVariant + } +} + +func (sys *System) DisputeGameFactoryAddr() common.Address { + return sys.L1Deployments().DisputeGameFactoryProxy +} + +func (sys *System) SupervisorClient() *sources.SupervisorClient { + panic("supervisor not supported for single chain system") +} + func (sys *System) Config() SystemConfig { return sys.Cfg } // AdvanceTime advances the system clock by the given duration. @@ -417,10 +435,18 @@ func (sys *System) RollupCfg() *rollup.Config { return sys.RollupConfig } +func (sys *System) RollupCfgs() []*rollup.Config { + return []*rollup.Config{sys.RollupConfig} +} + func (sys *System) L2Genesis() *core.Genesis { return sys.L2GenesisCfg } +func (sys *System) L2Geneses() []*core.Genesis { + return []*core.Genesis{sys.L2GenesisCfg} +} + func (sys *System) AllocType() config.AllocType { return sys.Cfg.AllocType }