{"author":{"address":"0xf8E30C251AA7974aA0A2d9e452d863681f8B59Ac","user":"https://learnblockchain.cn/people/4858"},"content":{"body":"原文链接：\u003chttps://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/blob/main/05-op-challenger.md\u003e\\\r\n作者：[joohhnnn](https://github.com/joohhnnn)\r\n\r\n# op-challenger\r\n\r\nop-challenger 主要负责操作 FDG (Fault Dispute Game)，它通过直接使用 Cannon、op-program 等组件来维持整个 FDG 的正常执行。\r\n\r\n![image](https://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/raw/main/resources/control.png)\r\n\r\n我们将 op-challenger 分为两部分：\r\n\r\n- 监控（monitor）：监控游戏进程，并做出相应操作。\r\n- 执行子任务，如 step、move、upload preimage 等。\r\n\r\n## 监控 (monitor)\r\nmonitor 组件负责订阅 L1 上的区块。每当有新的区块生成时，它会检索所有游戏，查看是否需要分配并执行具体操作。\r\n\r\n### 启动监控\r\n使用 [StartMonitoring()](https://github.com/ethereum-optimism/optimism/blob/develop/op-challenger/game/monitor.go#L152) 函数启动监控。`onNewL1Head()` 函数作为回调参数传入 `resubscribeFunction()`，并最终注册到 `eth.WatchHeadChanges` 中。\r\n每 10 秒钟检索一次，当检索到新区块后，将该区块的哈希和区块号传入 `progressGames()` 进行处理。\r\n\r\n```\r\n\r\nfunc (m *gameMonitor) onNewL1Head(ctx context.Context, sig eth.L1BlockRef) {\r\n\tm.clock.SetTime(sig.Time)\r\n\tif err := m.progressGames(ctx, sig.Hash, sig.Number); err != nil {\r\n\t\tm.logger.Error(\"Failed to progress games\", \"err\", err)\r\n\t}\r\n\tif err := m.preimages.Schedule(sig.Hash, sig.Number); err != nil {\r\n\t\tm.logger.Error(\"Failed to validate large preimages\", \"err\", err)\r\n\t}\r\n}\r\n\r\nfunc (m *gameMonitor) resubscribeFunction() event.ResubscribeErrFunc {\r\n\t// The ctx is cancelled as soon as the subscription is returned,\r\n\t// but is only used to create the subscription, and does not affect the returned subscription.\r\n\treturn func(ctx context.Context, err error) (event.Subscription, error) {\r\n\t\tif err != nil {\r\n\t\t\tm.logger.Warn(\"resubscribing after failed L1 subscription\", \"err\", err)\r\n\t\t}\r\n\t\treturn eth.WatchHeadChanges(ctx, m.l1Source, m.onNewL1Head)\r\n\t}\r\n}\r\n\r\nfunc (m *gameMonitor) StartMonitoring() {\r\n\tm.runState.Lock()\r\n\tdefer m.runState.Unlock()\r\n\tif m.l1HeadsSub != nil {\r\n\t\treturn // already started\r\n\t}\r\n\tm.l1HeadsSub = event.ResubscribeErr(time.Second*10, m.resubscribeFunction())\r\n}\r\n```\r\n\r\n### 分配子任务\r\n\r\n[progressGames](https://github.com/ethereum-optimism/optimism/blob/f940301caf531996eee4172e710b0decb7b78dde/op-challenger/game/monitor.go#L106) 函数在监听到新的区块后执行，其主要作用是获取所有有效的 game，并将这些 game 传入 Schedule 中用于后续的任务派发。需要注意的是，schedule 分为多个类别，如 bondSchedule（用于管理 claim 对应的 bond）和 pre-image schedule（用于上传 pre-image 数据）。我们在这里仅针对最基础的 move 和 step 的 schedule 进行讲解。\r\n\r\n```\r\nfunc (m *gameMonitor) progressGames(ctx context.Context, blockHash common.Hash, blockNumber uint64) error {\r\n\tminGameTimestamp := clock.MinCheckedTimestamp(m.clock, m.gameWindow)\r\n\tgames, err := m.source.GetGamesAtOrAfter(ctx, blockHash, minGameTimestamp)\r\n\tif err != nil {\r\n\t\treturn fmt.Errorf(\"failed to load games: %w\", err)\r\n\t}\r\n\tvar gamesToPlay []types.GameMetadata\r\n\tfor _, game := range games {\r\n\t\tif !m.allowedGame(game.Proxy) {\r\n\t\t\tm.logger.Debug(\"Skipping game not on allow list\", \"game\", game.Proxy)\r\n\t\t\tcontinue\r\n\t\t}\r\n\t\tgamesToPlay = append(gamesToPlay, game)\r\n\t}\r\n\tif err := m.claimer.Schedule(blockNumber, gamesToPlay); err != nil {\r\n\t\treturn fmt.Errorf(\"failed to schedule bond claims: %w\", err)\r\n\t}\r\n\tif err := m.scheduler.Schedule(gamesToPlay, blockNumber); errors.Is(err, scheduler.ErrBusy) {\r\n\t\tm.logger.Info(\"Scheduler still busy with previous update\")\r\n\t} else if err != nil {\r\n\t\treturn fmt.Errorf(\"failed to schedule games: %w\", err)\r\n\t}\r\n\treturn nil\r\n}\r\n```\r\n\r\n\r\n[schedule()](https://github.com/ethereum-optimism/optimism/blob/f940301caf531996eee4172e710b0decb7b78dde/op-challenger/game/scheduler/coordinator.go#L60) 函数处理接收到的 game，并在 createJob 中判断 game 是否需要新的子操作，然后通过 enqueueJob 函数将所有的子操作添加到 jobQueue 中进行传递。\r\n\r\n```\r\nfunc (c *coordinator) schedule(ctx context.Context, games []types.GameMetadata, blockNumber uint64) error {\r\n    \r\n        ……\r\n        \r\n\t// Next collect all the jobs to schedule and ensure all games are recorded in the states map.\r\n\t// Otherwise, results may start being processed before all games are recorded, resulting in existing\r\n\t// data directories potentially being deleted for games that are required.\r\n\tfor _, game := range games {\r\n\t\tif j, err := c.createJob(ctx, game, blockNumber); err != nil {\r\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to create job for game %v: %w\", game.Proxy, err))\r\n\t\t} else if j != nil {\r\n\t\t\tjobs = append(jobs, *j)\r\n\t\t\tc.m.RecordGameUpdateScheduled()\r\n\t\t}\r\n\t}\r\n    \r\n        ……\r\n        \r\n\t// Finally, enqueue the jobs\r\n\tfor _, j := range jobs {\r\n\t\tif err := c.enqueueJob(ctx, j); err != nil {\r\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to enqueue job for game %v: %w\", j.addr, err))\r\n\t\t}\r\n\t}\r\n\treturn errors.Join(errs...)\r\n}\r\n```\r\n\r\n## 子任务的执行操作\r\n\r\n### 生成 action\r\n当 jobQueue 中出现数据后，需要在 [CalculateNextActions()](https://github.com/ethereum-optimism/optimism/blob/f940301caf531996eee4172e710b0decb7b78dde/op-challenger/game/fault/solver/game_solver.go#L26) 中将这些子任务信号转化为具体的 action。以 step 操作为例，当 game depth 达到 MaxDepth 时，我们会生成对应 step 的 action。\r\n\r\n```\r\nfunc (s *GameSolver) CalculateNextActions(ctx context.Context, game types.Game) ([]types.Action, error) {\r\n\r\n        ……\r\n\tvar actions []types.Action\r\n\tagreedClaims := newHonestClaimTracker()\r\n\r\n\tfor _, claim := range game.Claims() {\r\n\t\tvar action *types.Action\r\n\t\tif claim.Depth() == game.MaxDepth() {\r\n\t\t\taction, err = s.calculateStep(ctx, game, claim, agreedClaims)\r\n\t\t} else {\r\n\t\t\taction, err = s.calculateMove(ctx, game, claim, agreedClaims)\r\n\t\t}\r\n        ……\r\n\t\tif action == nil {\r\n\t\t\tcontinue\r\n\t\t}\r\n\t\tactions = append(actions, *action)\r\n\t}\r\n\treturn actions, nil\r\n}\r\n```\r\n```\r\nfunc (s *GameSolver) calculateStep(ctx context.Context, game types.Game, claim types.Claim, agreedClaims *honestClaimTracker) (*types.Action, error) {\r\n\tif claim.CounteredBy != (common.Address{}) {\r\n\t\treturn nil, nil\r\n\t}\r\n\tstep, err := s.claimSolver.AttemptStep(ctx, game, claim, agreedClaims)\r\n\tif err != nil {\r\n\t\treturn nil, err\r\n\t}\r\n\tif step == nil {\r\n\t\treturn nil, nil\r\n\t}\r\n\treturn \u0026types.Action{\r\n\t\tType:        types.ActionTypeStep,\r\n\t\tParentClaim: step.LeafClaim,\r\n\t\tIsAttack:    step.IsAttack,\r\n\t\tPreState:    step.PreState,\r\n\t\tProofData:   step.ProofData,\r\n\t\tOracleData:  step.OracleData,\r\n\t}, nil\r\n}\r\n```\r\n\r\n```\r\nfunc (s *claimSolver) AttemptStep(ctx context.Context, game types.Game, claim types.Claim, honestClaims *honestClaimTracker) (*StepData, error) {\r\n\r\n        ……\r\n\tpreState, proofData, oracleData, err := s.trace.GetStepData(ctx, game, claim, position)\r\n\tif err != nil {\r\n\t\treturn nil, err\r\n\t}\r\n\r\n\treturn \u0026StepData{\r\n\t\tLeafClaim:  claim,\r\n\t\tIsAttack:   !claimCorrect,\r\n\t\tPreState:   preState,\r\n\t\tProofData:  proofData,\r\n\t\tOracleData: oracleData,\r\n\t}, nil\r\n}\r\n```\r\n\r\n`GetStepData()` 函数间接调用了 [DoGenerateProof()](https://github.com/ethereum-optimism/optimism/blob/develop/op-challenger/game/fault/trace/vm/executor.go#L74) 函数，启动了 Cannon 以生成 step 所需的 state data 和 proof data。\r\n\r\n```\r\nfunc (e *Executor) DoGenerateProof(ctx context.Context, dir string, begin uint64, end uint64, extraVmArgs ...string) error {\r\n        ……\r\n\targs := []string{\r\n\t\t\"run\",\r\n\t\t\"--input\", start,\r\n\t\t\"--output\", lastGeneratedState,\r\n\t\t\"--meta\", \"\",\r\n\t\t\"--info-at\", \"%\" + strconv.FormatUint(uint64(e.cfg.InfoFreq), 10),\r\n\t\t\"--proof-at\", \"=\" + strconv.FormatUint(end, 10),\r\n\t\t\"--proof-fmt\", filepath.Join(proofDir, \"%d.json.gz\"),\r\n\t\t\"--snapshot-at\", \"%\" + strconv.FormatUint(uint64(e.cfg.SnapshotFreq), 10),\r\n\t\t\"--snapshot-fmt\", filepath.Join(snapshotDir, \"%d.json.gz\"),\r\n\t}\r\n\tif end \u003c math.MaxUint64 {\r\n\t\targs = append(args, \"--stop-at\", \"=\"+strconv.FormatUint(end+1, 10))\r\n\t}\r\n\tif e.cfg.DebugInfo {\r\n\t\targs = append(args, \"--debug-info\", filepath.Join(dataDir, debugFilename))\r\n\t}\r\n\targs = append(args, extraVmArgs...)\r\n\targs = append(args,\r\n\t\t\"--\",\r\n\t\te.cfg.Server, \"--server\",\r\n\t\t\"--l1\", e.cfg.L1,\r\n\t\t\"--l1.beacon\", e.cfg.L1Beacon,\r\n\t\t\"--l2\", e.cfg.L2,\r\n\t\t\"--datadir\", dataDir,\r\n\t\t\"--l1.head\", e.inputs.L1Head.Hex(),\r\n\t\t\"--l2.head\", e.inputs.L2Head.Hex(),\r\n\t\t\"--l2.outputroot\", e.inputs.L2OutputRoot.Hex(),\r\n\t\t\"--l2.claim\", e.inputs.L2Claim.Hex(),\r\n\t\t\"--l2.blocknumber\", e.inputs.L2BlockNumber.Text(10),\r\n\t)\r\n        ……\r\n\terr = e.cmdExecutor(ctx, e.logger.New(\"proof\", end), e.cfg.VmBin, args...)\r\n        ……\r\n\treturn err\r\n}\r\n```\r\n\r\n### 执行 action\r\n在 [PerformAction()](https://github.com/ethereum-optimism/optimism/blob/f940301caf531996eee4172e710b0decb7b78dde/op-challenger/game/fault/responder/responder.go#L90) 中执行获取到的 action。此函数根据 action 的类别进行判断并执行相应的上链操作：\r\n\r\n- 判断是否需要上传 Pre-image data。\r\n- 判断操作类型是否为 Attack/Defend。\r\n- 判断是否为 Step 操作。\r\n- 判断是否可以从 L2BlockNumber 角度否定 root claim。\r\n```\r\nfunc (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error {\r\n\tif action.OracleData != nil {\r\n\t\tvar preimageExists bool\r\n\t\tvar err error\r\n\t\tif !action.OracleData.IsLocal {\r\n\t\t\tpreimageExists, err = r.oracle.GlobalDataExists(ctx, action.OracleData)\r\n\t\t\tif err != nil {\r\n\t\t\t\treturn fmt.Errorf(\"failed to check if preimage exists: %w\", err)\r\n\t\t\t}\r\n\t\t}\r\n\t\t// Always upload local preimages\r\n\t\tif !preimageExists {\r\n\t\t\terr := r.uploader.UploadPreimage(ctx, uint64(action.ParentClaim.ContractIndex), action.OracleData)\r\n\t\t\tif errors.Is(err, preimages.ErrChallengePeriodNotOver) {\r\n\t\t\t\tr.log.Debug(\"Large Preimage Squeeze failed, challenge period not over\")\r\n\t\t\t\treturn nil\r\n\t\t\t} else if err != nil {\r\n\t\t\t\treturn fmt.Errorf(\"failed to upload preimage: %w\", err)\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\tvar candidate txmgr.TxCandidate\r\n\tvar err error\r\n\tswitch action.Type {\r\n\tcase types.ActionTypeMove:\r\n\t\tif action.IsAttack {\r\n\t\t\tcandidate, err = r.contract.AttackTx(ctx, action.ParentClaim, action.Value)\r\n\t\t} else {\r\n\t\t\tcandidate, err = r.contract.DefendTx(ctx, action.ParentClaim, action.Value)\r\n\t\t}\r\n\tcase types.ActionTypeStep:\r\n\t\tcandidate, err = r.contract.StepTx(uint64(action.ParentClaim.ContractIndex), action.IsAttack, action.PreState, action.ProofData)\r\n\tcase types.ActionTypeChallengeL2BlockNumber:\r\n\t\tcandidate, err = r.contract.ChallengeL2BlockNumberTx(action.InvalidL2BlockNumberChallenge)\r\n\t}\r\n\tif err != nil {\r\n\t\treturn err\r\n\t}\r\n\treturn r.sender.SendAndWaitSimple(\"perform action\", candidate)\r\n}\r\n```\r\n\r\n## 总结\r\n\r\nop-challenger 是一个为Fault proof设计的高度自动化系统，旨在实时监控和响应链上游戏状态的变化。通过持续监听区块链事件，并根据游戏状态动态执行攻击或防御操作，op-challenger 提供了一个策略性强、反应迅速的解决方案。该系统与 cannon op-program 等关键组件紧密集成，能够自动化地生成游戏步骤所需的数据输入，并确保游戏决策的准确执行。","title":"optimism fault-proof背后的机制（五）：op-challenger"},"history":null,"timestamp":1722960555,"version":1}