{"content":{"title":"十道EVM谜题，学习以太坊原理与opcode!","body":"Evm puzzles 是一套练习和入门 evm 执行原理和 opcode 的习题，里边涉及到简单的 opcode 操作，如操作堆栈，操作内存，操作 calldata ，部署合约等等，更重要的是它只有十道题，即使是新手也可以在几个小时内解决谜题！让我们开始吧！\r\n\r\n规则：输入恰当的 calldata 和 value，使得题目的 opcode 正确执行，直到执行STOP。\r\n\r\npuzzle 交互工具：https://github.com/fvictorio/evm-puzzles\r\n\r\n## puzzle 1:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode  opcode name\r\n00      34      CALLVALUE\r\n01      56      JUMP\r\n02      FD      REVERT\r\n03      FD      REVERT\r\n04      FD      REVERT\r\n05      FD      REVERT\r\n06      FD      REVERT\r\n07      FD      REVERT\r\n08      5B      JUMPDEST\r\n09      00      STOP\r\n```\r\n\r\n此题简单明了，我们需要跳转至`pc = 0x08`才能避免中途 revert，所以`0x01`位置的`JUMP`应该跳转至 `0x08`。\r\n根据evm文档，JUMP 操作 的模式为：\r\n\r\n| stack | name      | gas  | initial stack | result stack | mem/storage |\r\n| ----- | --------- | ---- | ------------- | ------------ | ----------- |\r\n| 56    | JUMP      | 8    | dst           | /            | /           |\r\n| 34    | CALLVALUE | 2    | /             | msg.value    | /           |\r\n\r\n即：CALLVALUE 把 msg.value 放在调用栈上，JUMP 从调用栈的一个值，跳转至该值位置。\r\n现在应该很清楚了 msg.value == 8 即可通关。\r\n\r\n## puzzle 2:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode  opcode name\r\n00      34      CALLVALUE\r\n01      38      CODESIZE\r\n02      03      SUB\r\n03      56      JUMP\r\n04      FD      REVERT\r\n05      FD      REVERT\r\n06      5B      JUMPDEST\r\n07      00      STOP\r\n08      FD      REVERT\r\n09      FD      REVERT\r\n```\r\n\r\n此题跟 puzzle 1 差不多，只新增了两个 opcode，我们依然先看文档：\r\n\r\n\r\n\r\n| stack | name     | gas  | initial stack | result stack   | mem/storage |\r\n| ----- | -------- | ---- | ------------- | -------------- | ----------- |\r\n| 38    | CODESIZE | 2    | /             | len(this.code) | /           |\r\n| 03    | SUB      | 3    | a,b           | a - b          | /           |\r\n\r\n我们先使用 CALLVALUE 将 msg.value 压入调用栈，再把代码长度压栈，我们可以看到我们的 code 总长度为10，也就是 `10 - msg.value == 0x06`，可得 msg.value == 4。题解结束。\r\n\r\n## puzzle 3:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode  opcode name\r\n00      36      CALLDATASIZE\r\n01      56      JUMP\r\n02      FD      REVERT\r\n03      FD      REVERT\r\n04      5B      JUMPDEST\r\n05      00      STOP\r\n```\r\n\r\n新增了一个 opcode:\r\n\r\n| stack | name         | gas  | initial stack | result stack  | mem/storage |\r\n| ----- | ------------ | ---- | ------------- | ------------- | ----------- |\r\n| 36    | CALLDATASIZE | 2    | /             | len(msg.data) | /           |\r\n\r\n只要我们的 `len(msg.data) == 4` 即可完成题解，例如 `msg.data == 0x00000000`\r\n\r\n## puzzle 4:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode  opcode name\r\n00      34      CALLVALUE\r\n01      38      CODESIZE\r\n02      18      XOR\r\n03      56      JUMP\r\n04      FD      REVERT\r\n05      FD      REVERT\r\n06      FD      REVERT\r\n07      FD      REVERT\r\n08      FD      REVERT\r\n09      FD      REVERT\r\n0A      5B      JUMPDEST\r\n0B      00      STOP\r\n```\r\n\r\nXOR 大家应该都很熟悉了, 就是 pop 两个操作栈里的值，然后把异或结果 pop 回去：\r\n\r\n| stack | name | gas  | initial stack | result stack | mem/storage |\r\n| ----- | ---- | ---- | ------------- | ------------ | ----------- |\r\n| 18    | XOR  | 3    | a,b           | a ^ b        | /           |\r\n\r\n我们需要保证 `msg.value xor codesize == 0x0A`。0x0A 的二进制表示为 0000 1010，而我们可以知道 codesize == 0c（10进制的12）,二进制表示为0000 1100，可以计算出 `msg.data == 6` 即二进制表示为 0000 0110。\r\n\r\n## puzzle 5:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode      opcode name\r\n00      34          CALLVALUE\r\n01      80          DUP1\r\n02      02          MUL\r\n03      610100      PUSH2 0100\r\n06      14          EQ\r\n07      600C        PUSH1 0C\r\n09      57          JUMPI\r\n0A      FD          REVERT\r\n0B      FD          REVERT\r\n0C      5B          JUMPDEST\r\n0D      00          STOP\r\n0E      FD          REVERT\r\n0F      FD          REVERT\r\n```\r\n\r\n认识一下新的 opcode:\r\n\r\n\r\n\r\n| stack | name  | gas  | initial stack  | result stack | mem/storage |\r\n| ----- | ----- | ---- | -------------- | ------------ | ----------- |\r\n| 80    | DUP1  | 3    | a              | a,a          | /           |\r\n| 02    | MUL   | 5    | a,b            | a * b        | /           |\r\n| 60    | PUSH1 | 3    | .              | uint8        | /           |\r\n| 61    | PUSH2 | 3    | .              | uint16       | /           |\r\n| 14    | EQ    | 3    | a,b            | a == b       | /           |\r\n| 57    | JUMPI | 10   | dst, condition | /            | /           |\r\n\r\nDUP1：将栈顶元素再复制一份放在栈顶。\r\nMUL：pop 栈顶两元素，push a*b 的结果到栈顶。\r\nPUSH1/PUSH2：把一个元素放在栈顶，如610100 即使用 PUSH2(61) 将 8(0100) 放在栈顶。\r\nEQ：pop 栈顶两元素，push a==b 的结果到栈顶。\r\nJUMPI：取栈顶两元素，分别是 dst 和 condition，如果 condition == 1 则跳转到 `pc == dst`的位置，否则，执行下一条语句。\r\n\r\n我们可以从结果往前推，目的是达到`0x0d`位置的 STOP，需要通过跳转达到`0x0c`，也就是来自 `pc == 0x09`位置的 JUMPI 。\r\n`pc == 0x07` 处我们得知，此时的栈顶元素是 PUSH1 操作符操作的 `0x0c`，这正好与我们的目标地址相等。也就是说明 JU MPI 的 condition 为真。即`pc == 0x06`处的 EQ 返回为真。\r\n再往上就简单多了 ，其实只需要满足 `msg.value*msg.value == 0x0100` 注意，这里的0100是16进制数字而非二进制数字，所以`msg.data*msg.data == 256` 即  `msg.data == 16` 完成题解。\r\n\r\n## puzzle 6:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode  opcode name\r\n00      6000      PUSH1 00\r\n02      35        CALLDATALOAD\r\n03      56        JUMP\r\n04      FD        REVERT\r\n05      FD        REVERT\r\n06      FD        REVERT\r\n07      FD        REVERT\r\n08      FD        REVERT\r\n09      FD        REVERT\r\n0A      5B        JUMPDEST\r\n0B      00        STOP\r\n```\r\n\r\n新增一个 opcode:\r\n\r\n| stack | name         | gas  | initial stack | result stack         | mem/storage |\r\n| ----- | ------------ | ---- | ------------- | -------------------- | ----------- |\r\n| 35    | CALLDATALOAD | 3    | idx           | msg.data[idx:idx+32] | /           |\r\n\r\n简单明了 pop 栈顶元素作为 idx ，把 msg.data 从 idx 开始的32位写入操作栈。\r\n这样我们可以直接获得构造条件：`msg.data[0:32]== 0x000000000000000000000000000000000000000000000000000000000000000a` 即可\r\n\r\n## puzzle 7:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n00      36        CALLDATASIZE\r\n01      6000      PUSH1 00\r\n03      80        DUP1\r\n04      37        CALLDATACOPY\r\n05      36        CALLDATASIZE\r\n06      6000      PUSH1 00\r\n08      6000      PUSH1 00\r\n0A      F0        CREATE\r\n0B      3B        EXTCODESIZE\r\n0C      6001      PUSH1 01\r\n0E      14        EQ\r\n0F      6013      PUSH1 13\r\n11      57        JUMPI\r\n12      FD        REVERT\r\n13      5B        JUMPDEST\r\n14      00        STOP\r\n```\r\n\r\n从第7题开始难度会增加一些，我们先看一下新引入的opcode吧\r\n\r\n\r\n\r\n| stack | name         | gas      | initial stack    | result stack   | mem/storage                                         |\r\n| ----- | ------------ | -------- | ---------------- | -------------- | --------------------------------------------------- |\r\n| 37    | CALLDATACOPY | 3+       | dstOst, ost, len | /              | mem[dstOst:dstOst+len-1] := msg.data[ost:ost+len-1] |\r\n| F0    | CREATE       | 32000+   | val, ost, len    | addr           | /                                                   |\r\n| 3B    | EXTCODESIZE  | 100/2600 | addr             | len(addr.code) | /                                                   |\r\n\r\nCALLDATACOPY 简单明了，就是 pop 出三个栈顶元素，记为dstOst, ost, len。把`msg.data[ost:ost+len-1]`\t复制到内存：`msg.data[ost:ost+len-1]`\r\n\r\nCREATE 相对复杂，它是一个部署合约的 opcode，pop 出三个栈顶元素，记为val, ost, len。\r\nval 是创建合约时传入的 eth 数目。\r\nmem[ost:ost+len-1] 是合约的 contract code。\r\naddr 返回值是已创建合约的地址。\r\n\r\nEXTCODESIZE 取出栈顶元素，然后返回该合约的 code 长度。\r\n\r\n我们先看第一段，`0x00` - `0x05`\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n00      36        CALLDATASIZE\r\n01      6000      PUSH1 00\r\n03      80        DUP1\r\n04      37        CALLDATACOPY\r\n05      36        CALLDATASIZE\r\n```\r\n\r\n这一段其实就是把 `msg.data` 整个复制到内存`mem`。\r\n\r\n第二段，`0x06`-`0x0a`\r\n\r\n```assembly\r\npc      opcode  opcode name\r\n06      6000      PUSH1 00\r\n08      6000      PUSH1 00\r\n0A      F0        CREATE\r\n```\r\n\r\n把 `mem`上的代码部署到链。并把合约地址写回栈顶。\r\n\r\n第三段，`0x0b`-`0x14`\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n0B      3B        EXTCODESIZE\r\n0C      6001      PUSH1 01\r\n0E      14        EQ\r\n0F      6013      PUSH1 13\r\n11      57        JUMPI\r\n12      FD        REVERT\r\n13      5B        JUMPDEST\r\n14      00        STOP\r\n```\r\n\r\n我们可以看到要正确跳转需要满足的条件是 `len(address.code) == 1`。\r\n\r\n现在的问题是，新创建合约使用的code是我们的 msg.data，那是不是意味着部署后合约的 code 也是msg.data 呢？其实不然。创建合约的 code 被称为 creation code, 而最终留在区块链里的合约代码被称为 runtime code。这其实隐含着 conrtuctor的内容，contructor 只存在于 creation code 而非 runtime code ，这也是构造函数只被执行一次的原因。合约 creation code 会在一个交易里执行，并把 runtime code通过 `RETURN` 返回。\r\n\r\n我们还需要额外引入一个 opcode:\r\n\r\n| stack | name   | gas  | initial stack | result stack | mem/storage |\r\n| ----- | ------ | ---- | ------------- | ------------ | ----------- |\r\n| F3    | RETURN | *    | ost, len      | /            | /           |\r\n\r\nost 为 runtime code 在内存的起始位置，len 为截取长度，我们尝试构建一个 creation code:\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n00      6001      PUSH1 01\r\n02      6000      PUSH1 00\r\n04      F3        RETURN\r\n```\r\n\r\n这样我们返回的 runtime code 的长度就只是从内存中取出的一位操作符了。\r\n\r\n也就是 `msg.data == 0x60016000f3` 即可完成 puzzle ！\r\n\r\n## puzzle 8:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n00      36        CALLDATASIZE\r\n01      6000      PUSH1 00\r\n03      80        DUP1\r\n04      37        CALLDATACOPY\r\n05      36        CALLDATASIZE\r\n06      6000      PUSH1 00\r\n08      6000      PUSH1 00\r\n0A      F0        CREATE\r\n0B      6000      PUSH1 00\r\n0D      80        DUP1\r\n0E      80        DUP1\r\n0F      80        DUP1\r\n10      80        DUP1\r\n11      94        SWAP5\r\n12      5A        GAS\r\n13      F1        CALL\r\n14      6000      PUSH1 00\r\n16      14        EQ\r\n17      601B      PUSH1 1B\r\n19      57        JUMPI\r\n1A      FD        REVERT\r\n1B      5B        JUMPDEST\r\n1C      00        STOP\r\n```\r\n\r\n难度比起 puzzle 7 更上一层楼，我们先看看涉及到了几个新的 opcode:\r\n\r\n\r\n\r\n| stack | name  | gas                           | initial stack                                  | result stack | mem/storage                               |\r\n| ----- | ----- | ----------------------------- | ---------------------------------------------- | ------------ | ----------------------------------------- |\r\n| 94    | SWAP5 | 3                             | a, ..., b                                      | b, ..., a    | /                                         |\r\n| 5A    | GAS   | 2                             | /                                              | gasRemaining | /                                         |\r\n| F1    | CALL  | base_gas + gas_sent_with_call | gas, addr, val, argOst, argLen, retOst, retLen | success      | mem[retOst:retOst+retLen-1] := returndata |\r\n\r\nSWAP 系列的 opcode 很好理解，SWAP1 是把栈顶的 a，b 变成 b，a，SWAP5 就是把 a,x,x,x,x,,b 变成 b,x,x,x,x,a。\r\nGAS 是计算 gas 费并写入栈顶。\r\nCALL 即为调用合约，其使用到的栈顶元素 分别为 gas, addr(合约地址), val(msg.data), argOst(在内存中截取的输入数据起点), argLen(输入数据长度), retOst(在内存中截取的返回数据起点), retLen(返回数据长度)。若交易成功 将 1 写入栈顶，若失败，将0写入栈顶。\r\n\r\n我们先看第一段：\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n00      36        CALLDATASIZE\r\n01      6000      PUSH1 00\r\n03      80        DUP1\r\n04      37        CALLDATACOPY\r\n05      36        CALLDATASIZE\r\n06      6000      PUSH1 00\r\n08      6000      PUSH1 00\r\n0A      F0        CREATE\r\n```\r\n\r\nz这一段跟 puzzle 7 一样，把我们 msg.data 的数据当成合约部署在链上，再把新创建的合约地址写回栈顶。\r\n\r\n第二段：\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n0B      6000      PUSH1 00\r\n0D      80        DUP1\r\n0E      80        DUP1\r\n0F      80        DUP1\r\n10      80        DUP1\r\n11      94        SWAP5\r\n12      5A        GAS\r\n13      F1        CALL\r\n```\r\n\r\n这一段运行下到最后，我们可以知道这是一个 call 命令，参数依次是 gas , addr , 0, 0, 0, 0, 0。也就是不传入数据，没有返回值，不传入value, 若运行成功则把 1 压栈，反之压栈 0。\r\n\r\n第三段\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n14      6000      PUSH1 00\r\n16      14        EQ\r\n17      601B      PUSH1 1B\r\n19      57        JUMPI\r\n1A      FD        REVERT\r\n1B      5B        JUMPDEST\r\n1C      00        STOP\r\n```\r\n\r\n这一段就是用来反推我们合约返回结果的部分了，我们需要从上一个部分得到一个 `0x00`，使得EQ返回值为 1，方能成功触发 JUMPI 跳到终点。也就说明我们需要本次合约交易失败。这其实很简单，我们构造一个合约使得 runtime code只有一个 revert 就可以了，类似 puzzle 7，再稍微加点东西：\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n00      60FD      PUSH1 FD //FD 是revert操作符的编号\r\n02      6000      PUSH1 00\r\n04      53        MSTORE8\r\n05      6001      PUSH1 01\r\n07      6000      PUSH1 00\r\n09      F3        RETURN\r\n```\r\n\r\n这样，我们把 `0xfd` 写入内存 `0x00`了。这也就是我们的 runtime code。\r\n\r\n这样我们得出题解：`msg.data == 0x60fd60005360016000f3`\r\n\r\n## puzzle 9:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n00      36        CALLDATASIZE\r\n01      6003      PUSH1 03\r\n03      10        LT\r\n04      6009      PUSH1 09\r\n06      57        JUMPI\r\n07      FD        REVERT\r\n08      FD        REVERT\r\n09      5B        JUMPDEST\r\n0A      34        CALLVALUE\r\n0B      36        CALLDATASIZE\r\n0C      02        MUL\r\n0D      6008      PUSH1 08\r\n0F      14        EQ\r\n10      6014      PUSH1 14\r\n12      57        JUMPI\r\n13      FD        REVERT\r\n14      5B        JUMPDEST\r\n15      00        STOP\r\n```\r\n\r\n经历了 puzzle 7 && puzzle 8 的折磨，接下来的两道题实际上已经难不住我们了，先看新出现的 opcode 文档，非常简单：\r\n\r\n\r\n\r\n| stack | name | gas  | initial stack | result stack | mem/storage |\r\n| ----- | ---- | ---- | ------------- | ------------ | ----------- |\r\n| 10    | LT   | 3    | a, b          | a < b        | /           |\r\n| 02    | MUL  | 5    | a,b           | a * b        | /           |\r\n\r\n第一段：\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n00      36        CALLDATASIZE\r\n01      6003      PUSH1 03\r\n03      10        LT\r\n04      6009      PUSH1 09\r\n06      57        JUMPI\r\n07      FD        REVERT\r\n08      FD        REVERT\r\n09      5B        JUMPDEST\r\n```\r\n\r\n我们可以得出一个限制：`0x03 < len(msg.data)`\r\n\r\n第二段：\r\n\r\n```assembly\r\npc      opcode    opcode name\r\n0A      34        CALLVALUE\r\n0B      36        CALLDATASIZE\r\n0C      02        MUL\r\n0D      6008      PUSH1 08\r\n0F      14        EQ\r\n10      6014      PUSH1 14\r\n12      57        JUMPI\r\n13      FD        REVERT\r\n14      5B        JUMPDEST\r\n15      00        STOP\r\n```\r\n\r\n很容易得出另一个限制 `len(msd.data) * msg.value == 8`, 又根据第一部分的限制，我们可以随意构造一个长度为4的m sg.data，然后令 msg.value == 2。\r\n\r\n即可构造出一个符合题意的题解：\r\n`msg.value == 2`\r\n`msg.data == 0x12121212`\r\n\r\n## puzzle 10:\r\n\r\n题目：\r\n\r\n```assembly\r\npc      opcode      opcode name\r\n00      38          CODESIZE\r\n01      34          CALLVALUE\r\n02      90          SWAP1\r\n03      11          GT\r\n04      6008        PUSH1 08\r\n06      57          JUMPI\r\n07      FD          REVERT\r\n08      5B          JUMPDEST\r\n09      36          CALLDATASIZE\r\n0A      610003      PUSH2 0003\r\n0D      90          SWAP1\r\n0E      06          MOD\r\n0F      15          ISZERO\r\n10      34          CALLVALUE\r\n11      600A        PUSH1 0A\r\n13      01          ADD\r\n14      57          JUMPI\r\n15      FD          REVERT\r\n16      FD          REVERT\r\n17      FD          REVERT\r\n18      FD          REVERT\r\n19      5B          JUMPDEST\r\n1A      00          STOP\r\n```\r\n\r\n和第九题很类似，我们看一下新增的几个 opcode，也都很简单:\r\n\r\n| stack | name     | gas  | initial stack | result stack   | mem/storage |\r\n| ----- | -------- | ---- | ------------- | -------------- | ----------- |\r\n| 38    | CODESIZE | 2    | /             | len(this.code) | /           |\r\n| 11    | GT       | 3    | a,b           | a > b          | /           |\r\n| 01    | ADD      | 3    | a,b           | a + b          | /           |\r\n| 06    | MOD      | 5    | a,b           | a % b          | /           |\r\n| 15    | ISZERO   | 3    | a             | a == 0         | /           |\r\n\r\n值得注意的是，codesize是指我们正在运行的代码的长度，本题中我们可以看到是 21 (0x1b)。\r\n\r\n第一段：\r\n\r\n```assembly\r\npc      opcode      opcode name\r\n00      38          CODESIZE\r\n01      34          CALLVALUE\r\n02      90          SWAP1\r\n03      11          GT\r\n04      6008        PUSH1 08\r\n06      57          JUMPI\r\n07      FD          REVERT\r\n08      5B          JUMPDEST\r\n```\r\n\r\n我们得到第一条限制 ` 21 > msg.value`\r\n\r\n第二段：\r\n\r\n```assembly\r\npc      opcode      opcode name\r\n09      36          CALLDATASIZE\r\n0A      610003      PUSH2 0003\r\n0D      90          SWAP1\r\n0E      06          MOD\r\n0F      15          ISZERO\r\n10      34          CALLVALUE\r\n11      600A        PUSH1 0A\r\n13      01          ADD\r\n14      57          JUMPI\r\n15      FD          REVERT\r\n16      FD          REVERT\r\n17      FD          REVERT\r\n18      FD          REVERT\r\n19      5B          JUMPDEST\r\n1A      00          STOP\r\n```\r\n\r\n我们从结果反推`0x14`处， JUMPI 的 dst 操作数 应为 `0x19`指向`0x19`处的JUMPDEST，condition 操作数应为 1。\r\n这其实就是 `0x13`处 ADD 的结果为 `0x19`，`0x0f`处的 ISZERO 结果为 1。\r\n\r\n即 `len(msg.data) % 3 == 0`，`msg.data + 10 == 25`（0x0A的十进制是10，0x19的十进制是25）。\r\n\r\n我们可以构造一个题解：\r\n`msg.value == 15`\r\n`msg.data == 0x121212`"},"author":{"user":"https://learnblockchain.cn/people/9803","address":"0x8958fF5A2b77C0624e6069d81e3893D65058672e"},"history":null,"timestamp":1678777613,"version":1}