{"content":{"title":"optimism fault-proof背后的机制（三）：cannon","body":"原文链接：<https://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/blob/main/03-cannon.md>\\\r\n作者：[joohhnnn](https://github.com/joohhnnn)\r\n\r\n# CANNON\r\nCANNON 是整个 fault-proof 架构中的核心组件。本章节将对 CANNON 进行详细介绍。\r\n\r\n## 组件间关系\r\n在详细介绍 CANNON 之前，我们需要了解一个重要概念：CANNON 并不是独立运行的。CANNON 与 OP-Challenger、OP-Program、OP-Preimage 之间存在协调调用的整体关系。\r\n\r\nOP-Challenger 负责总控制，监控链上数据并执行各种操作，如创建游戏、执行 move、step 和 resolve 等。\r\n\r\nCANNON 包含两个部分：一部分是链上的 MIPS.sol，这是由 Solidity 编写的 MIPS 指令处理程序，主要负责链上验证最细粒度的执行。另一部分是链下的 mipsevm，由 Go 语言实现，主要用于生成链上所需的 witness 数据，这些数据可以理解为 step 函数的输入参数。链上和链下部分是等效的，即给定相同的输入，二者将产生相同的输出。\r\n\r\nOP-Program 也包含两部分：一部分是 client，负责将其自身转化为 ELF 文件，并加载到 CANNON 中使用。另一部分是 host，负责配置并启动 OP-Preimage 服务，供 mipsevm 使用时提供必要的额外数据。\r\n\r\nOP-Preimage 负责处理实际的 preimage 逻辑。\r\n\r\n他们之间的关系如图所示：\r\n![image](https://github.com/joohhnnn/The-book-of-optimism-fault-proof-CN/raw/main/resources/control.png)\r\n### OP-Challenger 与 Cannon 的交互\r\n当 OP-Challenger 需要生成证明去链上执行 step 操作时，它会运行 CANNON 以获取所需的 state、proof 或 preimage 的额外信息。\r\n\r\n### Cannon 与 OP-Program 的交互\r\n1. OP-Program 的 client 部分为 CANNON 提供 ELF 指令集文件，用于 CANNON 的运行。\r\n2. OP-Program 的 host 部分为 CANNON 提供额外数据，如区块号等非原生指令数据。\r\n\r\n### OP-Program 与 OP-Preimage 的交互\r\nOP-Program 的 host 部分初始化并配置启动 OP-Preimage，为 CANNON 提供所需的额外数据。\r\n\r\n\r\n## MIPS\r\n在这里，我们不需要深入了解 MIPS 本身，而是先考虑为什么需要这种中间介质。在我们之前的设计 FDG 中，涉及到在链上执行最细粒度指令以进行验证。简单来说，就是在 L1 的 EVM 环境中使用 Solidity 实现一个 L2 的后端执行客户端。将整个系统完全还原到 Solidity 中是不可能的，即使尽最大努力在 Solidity 中还原，由于复杂性，也难以保证在相同输入的情况下链上链下输出相同的结果。因此，我们需要一种中间态 VM 来确保链下和链上运行的结果是一致的，MIPS 就是这样的存在。在链下，它不直接运行 Go 程序，而是运行由 Go 程序推导出的 ELF 文件所代表的 MIPS 程序，在链上也实现了 MIPS 指令集，二者可以保持等效。\r\n\r\nMIPS 是一种简单指令集操作系统，使得在 Solidity 中的实现成为可能。\r\n\r\nMIPS 主要包含两种类型的指令：\r\n1. 常规指令，用于执行常规的程序计算和控制，如算术运算、数据加载、条件分支等。\r\n2. 系统指令，syscall 用于执行系统调用，即请求操作系统提供的服务，如读取文件、创建进程等。特别地，读取操作需要 Pre-image 合约的配合，如读取 block headers、MPT nodes、receipts、transactions、blobs 等数据。\r\n\r\n两种关键类型的数据结构包括：\r\n1. State（状态）\r\n\r\n```\r\n    struct State {\r\n        bytes32 memRoot;\r\n        bytes32 preimageKey;\r\n        uint32 preimageOffset;\r\n        uint32 pc;\r\n        uint32 nextPC;\r\n        uint32 lo;\r\n        uint32 hi;\r\n        uint32 heap;\r\n        uint8 exitCode;\r\n        bool exited;\r\n        uint64 step;\r\n        uint32[32] registers;\r\n    }\r\n```\r\n\r\n链上 MIPS 只是一个纯逻辑函数，不包含任何状态，所有状态都是通过调用时传入的。State 类型主要包含内存信息、指令信息、寄存器信息等。\r\n\r\n2. ProofData\r\n\r\nProofData 是[默克尔树证明数据](https://medium.com/crypto-0-nite/merkle-proofs-explained-6dd429623dc5)的紧密排列，用于证明 state 数据的有效性。\r\n\r\n如果你对 MIPS 的完整细节感兴趣，可以参考[这里](https://docs.optimism.io/stack/protocol/fault-proofs/mips#further-reading)。\r\n\r\n## pre-image-oracle\r\n\r\n如果将一个 transaction 的执行拆解为一系列指令，其中大部分是基础指令，少部分是系统指令，而在系统指令中，只有大约 0.1% 是需要进行读取操作的系统指令。因此，在设计 STEP() 函数时，需要将这两种情况分开处理，基础指令所需的上下文直接通过调用时的入参传入。而需要读取特殊信息的系统指令，考虑到其出现的概率很低，出于设计考虑，不占用入参，而是为其设计了一个单独的 Pre-image-oracle 组件来传递相应的数据，这就是 pre-image 在 fault-proof 中的作用，起到一个通讯中介的角色。\r\n\r\n## CANNON 在链上的体现\r\n\r\nCANNON 在链上的实体为 MIPS.sol 文件，其中最主要的部分为 [step() 函数](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/cannon/MIPS.sol#L204)，此函数由我们之前章节的 FDG 中的 step() 函数直接调用。\r\n\r\n以下是 `step()` 函数的完整代码解析，主要分为以下几部分逻辑：\r\n\r\n1. 数据校验：在解包数据前进行内存偏移和结构完整性的检查。\r\n2. 定义辅助函数：用于将 state 数据紧密存储在内存中，便于后续使用。\r\n3. 获取指令内容：通过 state 中的 pc 来获取具体的 instruction 内容，并在此过程中使用默克尔树来验证 state 数据间的关系，确保数据的正确性。\r\n4. 系统指令调用：如果是 syscall（系统指令调用），则跳到相应的处理逻辑中执行并返回结果。如果是读取操作，需要额外与 Pre-image-oracle 交互。\r\n5. 基础指令执行：执行基础指令，并更新相关变量后返回结果。\r\n\r\n对于上述第 3、4 和 5 步骤，此处不再进行额外讲解，但如果您对这些部分感兴趣，可以通过以下链接直接访问相关代码进行深入阅读：\r\n- 第 3 步：[详细代码](https://github.com/ethereum-optimism/optimism/blob/5e317379fae65b76f5a6ee27581f0e62d2fe017a/packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol#L14)\r\n- 第 4 步：[详细代码](https://github.com/ethereum-optimism/optimism/blob/5e317379fae65b76f5a6ee27581f0e62d2fe017a/packages/contracts-bedrock/src/cannon/MIPS.sol#L134)\r\n- 第 5 步：[详细代码](https://github.com/ethereum-optimism/optimism/blob/5e317379fae65b76f5a6ee27581f0e62d2fe017a/packages/contracts-bedrock/src/cannon/libraries/MIPSInstructions.sol#L41)\r\n\r\n```\r\n   function step(bytes calldata _stateData, bytes calldata _proof, bytes32 _localContext) public returns (bytes32) {\r\n        unchecked {\r\n            State memory state;\r\n            //-------------------- Part 1 start --------------------\r\n            // Packed calldata is ~6 times smaller than state size\r\n            assembly {\r\n                if iszero(eq(state, 0x80)) {\r\n                    // expected state mem offset check\r\n                    revert(0, 0)\r\n                }\r\n                if iszero(eq(mload(0x40), shl(5, 48))) {\r\n                    // expected memory check\r\n                    revert(0, 0)\r\n                }\r\n                if iszero(eq(_stateData.offset, 132)) {\r\n                    // 32*4+4=132 expected state data offset\r\n                    revert(0, 0)\r\n                }\r\n                if iszero(eq(_proof.offset, STEP_PROOF_OFFSET)) {\r\n                    // 132+32+256=420 expected proof offset\r\n                    revert(0, 0)\r\n                }\r\n            //-------------------- Part 1 end ----------------------\r\n            \r\n            //-------------------- Part 2 start --------------------\r\n                function putField(callOffset, memOffset, size) -> callOffsetOut, memOffsetOut {\r\n                    // calldata is packed, thus starting left-aligned, shift-right to pad and right-align\r\n                    let w := shr(shl(3, sub(32, size)), calldataload(callOffset))\r\n                    mstore(memOffset, w)\r\n                    callOffsetOut := add(callOffset, size)\r\n                    memOffsetOut := add(memOffset, 32)\r\n                }\r\n\r\n                // Unpack state from calldata into memory\r\n                let c := _stateData.offset // calldata offset\r\n                let m := 0x80 // mem offset\r\n                c, m := putField(c, m, 32) // memRoot\r\n                c, m := putField(c, m, 32) // preimageKey\r\n                c, m := putField(c, m, 4) // preimageOffset\r\n                c, m := putField(c, m, 4) // pc\r\n                c, m := putField(c, m, 4) // nextPC\r\n                c, m := putField(c, m, 4) // lo\r\n                c, m := putField(c, m, 4) // hi\r\n                c, m := putField(c, m, 4) // heap\r\n                c, m := putField(c, m, 1) // exitCode\r\n                c, m := putField(c, m, 1) // exited\r\n                c, m := putField(c, m, 8) // step\r\n\r\n                // Unpack register calldata into memory\r\n                mstore(m, add(m, 32)) // offset to registers\r\n                m := add(m, 32)\r\n                for { let i := 0 } lt(i, 32) { i := add(i, 1) } { c, m := putField(c, m, 4) }\r\n            }\r\n            //-------------------- Part 2 end ----------------------\r\n            \r\n            // Don't change state once exited\r\n            if (state.exited) {\r\n                return outputState();\r\n            }\r\n\r\n            state.step += 1;\r\n            \r\n            //-------------------- Part 3 start --------------------\r\n            // instruction fetch\r\n            uint256 insnProofOffset = MIPSMemory.memoryProofOffset(STEP_PROOF_OFFSET, 0);\r\n            (uint32 insn, uint32 opcode, uint32 fun) =\r\n                ins.getInstructionDetails(state.pc, state.memRoot, insnProofOffset);\r\n            //-------------------- Part 3 end ----------------------\r\n\r\n            //-------------------- Part 4 start --------------------\r\n            // Handle syscall separately\r\n            // syscall (can read and write)\r\n            if (opcode == 0 && fun == 0xC) {\r\n                return handleSyscall(_localContext);\r\n            }\r\n            //-------------------- Part 4 end ----------------------\r\n            \r\n            //-------------------- Part 5 start --------------------\r\n\r\n            // Exec the rest of the step logic\r\n            st.CpuScalars memory cpu = getCpuScalars(state);\r\n            (state.memRoot) = ins.execMipsCoreStepLogic({\r\n                _cpu: cpu,\r\n                _registers: state.registers,\r\n                _memRoot: state.memRoot,\r\n                _memProofOffset: MIPSMemory.memoryProofOffset(STEP_PROOF_OFFSET, 1),\r\n                _insn: insn,\r\n                _opcode: opcode,\r\n                _fun: fun\r\n            });\r\n            setStateCpuScalars(state, cpu);\r\n\r\n            return outputState();\r\n            //-------------------- Part 5 end ----------------------\r\n        }\r\n    }\r\n```\r\n\r\n## CANNON 在链下的体现\r\n\r\nCANNON 在链下主要体现在 [Cannon](https://github.com/ethereum-optimism/optimism/tree/develop/cannon) 组件中，该组件可用于生成指令的单独执行流程，或持续执行并在执行过程中产生输出。\r\n\r\n### 主要内容\r\n\r\n执行并提供游戏中 move/step 的输入参数内容。\r\n\r\n#### 执行\r\n当我们执行 `cannon run -h` 时，可以看到运行时需要传入的 flag。通过这些 flag，我们可以深入理解 Cannon 链下执行的机制。\r\n\r\n```\r\n./bin/cannon run -h\r\nNAME:\r\n   cannon run - Run VM step(s) and generate proof data to replicate onchain.\r\n\r\nUSAGE:\r\n   cannon run [command options] [arguments...]\r\n\r\nDESCRIPTION:\r\n   Run VM step(s) and generate proof data to replicate onchain. See flags to match when to output a proof, a snapshot, or to stop early.\r\n\r\nOPTIONS:\r\n   --type value                          VM type to run. Options are 'cannon' (default) (default: \"cannon\")\r\n   --input value                         path of input JSON state. Stdin if left empty. (default: \"state.json\")\r\n   --output value                        path of output JSON state. Not written if empty, use - to write to Stdout. (default: \"out.json\")\r\n   --proof-at value                      step pattern to output proof at: 'never' (default), 'always', '=123' at exactly step 123, '%123' for every 123 steps\r\n   --proof-fmt value                     format for proof data output file names. Proof data is written to stdout if -. (default: \"proof-%d.json\")\r\n   --snapshot-at value                   step pattern to output snapshots at: 'never' (default), 'always', '=123' at exactly step 123, '%123' for every 123 steps\r\n   --snapshot-fmt value                  format for snapshot output file names. (default: \"state-%d.json\")\r\n   --stop-at value                       step pattern to stop at: 'never' (default), 'always', '=123' at exactly step 123, '%123' for every 123 steps\r\n   --stop-at-preimage value              stop at the first preimage request matching this key\r\n   --stop-at-preimage-type value         stop at the first preimage request matching this type\r\n   --stop-at-preimage-larger-than value  stop at the first step that requests a preimage larger than the specified size (in bytes)\r\n   --meta value                          path to metadata file for symbol lookup for enhanced debugging info during execution. (default: \"meta.json\")\r\n   --info-at value                       step pattern to print info at: 'never' (default), 'always', '=123' at exactly step 123, '%123' for every 123 steps (default: %100000)\r\n   --pprof.cpu                           enable pprof cpu profiling (default: false)\r\n   --debug                               enable debug mode, which includes stack traces and other debug info in the output. Requires --meta. (default: false)\r\n   --debug-info value                    path to write debug info to\r\n   --help, -h                            show help\r\n```\r\n\r\n以下是几个核心的 flag：\r\n- `type`: 指的是 VM 的类型，目前仅支持 cannon，未来将支持更多类型的虚拟机。\r\n- `input`: 指的是 State 类型的 JSON 形式文件的路径，由 ELF 文件加载而来，可以理解为 client 代码在运行时的环境，如内存分布、寄存器等的状态。\r\n- `output`: 根据 input 执行后的最新状态的输出路径。\r\n- `proof-at`: 运行到 x 位置后输出 proof。\r\n- `snapshot-at`: 运行到 x 位置后输出 state。\r\n- `stop-at`: 在第 x 次 step 后终止。\r\n\r\nCannon 的命令实际上并不是为手动单次执行设计的，而是为了 op-challenger 而设计。让我们看一下 op-challenger 中是如何使用的。\r\n\r\n```\r\nfunc (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) error {\r\n\tsnapshotDir := filepath.Join(dir, snapsDir)\r\n\tstart, err := e.selectSnapshot(e.logger, snapshotDir, e.absolutePreState, i)\r\n\tif err != nil {\r\n\t\treturn fmt.Errorf(\"find starting snapshot: %w\", err)\r\n\t}\r\n\tproofDir := filepath.Join(dir, proofsDir)\r\n\tdataDir := filepath.Join(dir, preimagesDir)\r\n\tlastGeneratedState := filepath.Join(dir, finalState)\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.infoFreq), 10),\r\n\t\t\"--proof-at\", \"=\" + strconv.FormatUint(i, 10),\r\n\t\t\"--proof-fmt\", filepath.Join(proofDir, \"%d.json.gz\"),\r\n\t\t\"--snapshot-at\", \"%\" + strconv.FormatUint(uint64(e.snapshotFreq), 10),\r\n\t\t\"--snapshot-fmt\", filepath.Join(snapshotDir, \"%d.json.gz\"),\r\n\t}\r\n\tif i < math.MaxUint64 {\r\n\t\targs = append(args, \"--stop-at\", \"=\"+strconv.FormatUint(i+1, 10))\r\n\t}\r\n```\r\n\r\n可以看到，它是在 GenerateProof 函数下使用的，而这个函数主要由 move（attack & defend）和 step 使用，即每次 move 和 step 前都会调用 Cannon。我们继续看一下它如何向 flag 中传值，可以看到 start 来自第 i 步操作，并且 cannon 执行到 i+1 处停止，因此整个 cannon 的执行只进行了一步。\r\n\r\n因此，我们需要纠正一个常见误区：cannon 在链下的虚拟机并不是持续运行的，而是按上述方式单次运行以获取特定位置的数据。\r\n\r\n#### 具体实现\r\n链下 cannon 执行时的核心组件在于 cannon 中的 [Step()](https://github.com/ethereum-optimism/optimism/blob/develop/cannon/mipsevm/singlethreaded/instrumented.go#L66) 函数和其中的 [mipsStep()](https://github.com/ethereum-optimism/optimism/blob/develop/cannon/mipsevm/singlethreaded/mips.go#L50) 函数。\r\n\r\n可以看到，链下的 Go 代码中的 step 逻辑与链上 Solidity 的 step 逻辑高度一致，首先将内存等状态加载到固定位置，然后在 mipsStep() 中进一步处理，例如判定是否为 syscall 等操作。\r\n\r\n\r\n```\r\nfunc (m *InstrumentedState) Step(proof bool) (wit *mipsevm.StepWitness, err error) {\r\n\tm.preimageOracle.Reset()\r\n\tm.memoryTracker.Reset(proof)\r\n\r\n\tif proof {\r\n\t\tinsnProof := m.state.Memory.MerkleProof(m.state.Cpu.PC)\r\n\t\tencodedWitness, stateHash := m.state.EncodeWitness()\r\n\t\twit = &mipsevm.StepWitness{\r\n\t\t\tState:     encodedWitness,\r\n\t\t\tStateHash: stateHash,\r\n\t\t\tProofData: insnProof[:],\r\n\t\t}\r\n\t}\r\n\terr = m.mipsStep()\r\n\tif err != nil {\r\n\t\treturn nil, err\r\n\t}\r\n\r\n\tif proof {\r\n\t\tmemProof := m.memoryTracker.MemProof()\r\n\t\twit.ProofData = append(wit.ProofData, memProof[:]...)\r\n\t\tlastPreimageKey, lastPreimage, lastPreimageOffset := m.preimageOracle.LastPreimage()\r\n\t\tif lastPreimageOffset != ^uint32(0) {\r\n\t\t\twit.PreimageOffset = lastPreimageOffset\r\n\t\t\twit.PreimageKey = lastPreimageKey\r\n\t\t\twit.PreimageValue = lastPreimage\r\n\t\t}\r\n\t}\r\n\treturn\r\n}\r\n```\r\n```\r\nfunc (m *InstrumentedState) mipsStep() error {\r\n\tif m.state.Exited {\r\n\t\treturn nil\r\n\t}\r\n\tm.state.Step += 1\r\n\t// instruction fetch\r\n\tinsn, opcode, fun := exec.GetInstructionDetails(m.state.Cpu.PC, m.state.Memory)\r\n\r\n\t// Handle syscall separately\r\n\t// syscall (can read and write)\r\n\tif opcode == 0 && fun == 0xC {\r\n\t\treturn m.handleSyscall()\r\n\t}\r\n\r\n\t// Exec the rest of the step logic\r\n\treturn exec.ExecMipsCoreStepLogic(&m.state.Cpu, &m.state.Registers, m.state.Memory, insn, opcode, fun, m.memoryTracker, m.stackTracker)\r\n}\r\n```\r\n\r\n## 总结\r\n\r\n经过上述详细讲解，我们了解到链上部分主要用于 Fault Proof Game 的最细粒度验证，而链下部分则旨在逐步推导出相应的最细粒度位置的数据，供链上执行使用。如果细致观察，可以发现二者的架构模式基本一致，但仍存在细微差别。链上的 MIPS 系统主要用于验证，因此只需关注结果；而链下的 MIPS 系统则更注重于产生可用的数据，其在数据输出和存储方面更加友好和高效。\r\n\r\n通过这种设计，CANNON 在确保链上与链下数据一致性的同时，也优化了数据处理和验证过程，使整个系统的运行更加高效和可靠。这种双层验证机制为 Fault Proof Game 提供了坚实的技术支持，确保了游戏的公平性和透明性。"},"author":{"user":"https://learnblockchain.cn/people/4858","address":"0xf8E30C251AA7974aA0A2d9e452d863681f8B59Ac"},"history":"bafkreidzynhbgxxyyri64va74dfnqvajigzdynazq4w4sa64usyx7pfu6y","timestamp":1722960183,"version":1}