EVM-Puzzles: изучайте Ethereum, решая интерактивные головоломки
29 марта 2022 г.[EVM-Puzzles] (https://github.com/fvictorio/evm-puzzles) — это набор задач, которые помогут вам лучше понять виртуальную машину Ethereum. Каждая головоломка начинается с предоставления вам серии кодов операций и предлагает вам ввести правильное значение транзакции или данные вызова, которые позволят выполнить последовательность без возврата. Это пошаговое руководство призвано стать кратким руководством по каждой головоломке, позволяющим любому человеку с любым уровнем опыта полностью понять, почему и как стоит каждое решение. В этом пошаговом руководстве предполагается, что вы знакомы со стековыми машинами. Если нет, посмотрите как работают стековые машины перед тем, как начать. Полезно знать, что каждый элемент стека в EVM составляет 32 байта (т. е. одно слово). В этом репо 10 головоломок. Для тех, у кого нет опыта работы с EVM, это должно занять около 1-2 часов. Для кого-то с базовым опытом EVM это должно занять около 1 часа. Если вам очень удобно работать с EVM, но вы все еще хотите пройти пошаговое руководство, это займет около 30 минут. С этой запиской мы готовы начать!
Сначала перейдите в [репозиторий EVM-Puzzles] (https://github.com/fvictorio/evm-puzzles), клонируйте проект и настройте локальную среду. Убедитесь, что у вас установлена каска. Если вы этого не сделаете, вы можете просто ввести «npm install --save-dev hardhat», когда находитесь в корневой папке проекта.
Далее, если вы новичок в EVM, взгляните на [коды операций EVM] (https://www.evm.codes/) (не нужно понимать все, просто получите общее представление).
Со всем этим давайте проверим первую головоломку. Чтобы начать первую головоломку, перейдите в корневой каталог проекта и введите в терминале «npx hardhat play».
Головоломка №1
Давайте посмотрим на первую загадку. Вам дается ряд кодов операций, которые представляют собой контракт. Головоломка предлагает вам ввести значение для отправки, или, другими словами, если вы отправили транзакцию на этот контракт, какой должна быть стоимость транзакции, чтобы этот контракт работал без нажатия [инструкции REVERT] (https://www .evm.codes/#fd)? Идите вперед и попробуйте, а затем не стесняйтесь возвращаться сюда, если вы застряли или хотите подробно изучить решение после решения головоломки.
```javascript
Головоломка 1
00 34 ЗНАЧЕНИЕ ВЫЗОВА
01 56 ПРЫЖОК
02 FD ВОЗВРАТ
03 FD ВОЗВРАТ
04 FD ВОЗВРАТ
05 FD ВОЗВРАТ
06 FD ВОЗВРАТ
07 FD ВОЗВРАТ
08 5B ПРЫЖОК
09 00 СТОП
? Введите значение для отправки: (0)
Хорошо, теперь объяснитель. Во-первых, нам нужно знать, что делает [инструкция CALLVALUE] (https://www.evm.codes/#34). Этот код операции получает значение текущего вызова (т. е. значение транзакции) в wei и помещает это значение на вершину стека. Итак, если мы ввели значение 10, прежде чем инструкция CALLVALUE
оценивается, стек будет выглядеть так.
```js
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
После оценки кода операции CALLVALUE
стек будет выглядеть следующим образом.
```js
[10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Далее нам нужно знать, что делает инструкция JUMP. Этот код операции использует верхнее значение в стеке и переходит к n
инструкции в последовательности, где n
— это значение наверху стека. Быстрый пример сделает это более ясным. Допустим, у нас есть следующая последовательность.
```js
00 34 ЗНАЧЕНИЕ ВЫЗОВА
01 56 ПРЫЖОК
02 FD ВОЗВРАТ
03 FD ВОЗВРАТ
04 80 ПРЫЖОК
05 80 ДУП1
06 00 СТОП
? Введите значение для отправки: (0)
Если мы введем 4 в качестве значения для отправки, код операции CALLVALUE
поместит 4
в стек. После CALLVALUE
наш стек теперь выглядит так.
```js
[4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Затем код операции JUMP
использует верхнее значение в стеке и переходит к инструкции в этой позиции. Поскольку значение на вершине стека равно 4
, счетчик программ переходит к четвертой инструкции и продолжает работу. Код операции JUMP
должен изменить программный счетчик, чтобы он оказался на команде JUMPDEST
. Для приведенного выше примера мы можем думать о программе, выглядящей так после выполнения инструкции JUMP
.
```js
05 80 ДУП1
06 00 СТОП
Теперь, когда все ясно, давайте вернемся к головоломке. Нам нужно ввести значение, чтобы программа работала без нажатия инструкции REVERT.
```js
00 34 ЗНАЧЕНИЕ ВЫЗОВА
01 56 ПРЫЖОК
02 FD ВОЗВРАТ
03 FD ВОЗВРАТ
04 FD ВОЗВРАТ
05 FD ВОЗВРАТ
06 FD ВОЗВРАТ
07 FD ВОЗВРАТ
08 5B ПРЫЖОК
09 00 СТОП
Для этого мы можем ввести значение вызова 8, что заставит инструкцию CALLVALUE
поместить 8
в стек, где инструкция JUMP
затем использует это значение и переходит к 8-й инструкции, пропуская все инструкции REVERT
. Хорошая работа, одна головоломка позади!
Головоломка №2
Теперь, когда у вас мокрые ноги, давайте взглянем на вторую загадку. Попробуйте самостоятельно и, как и раньше, не стесняйтесь вернуться, чтобы проверить решение, а также объяснение. Вот загадка.
```js
Головоломка 2
00 34 ЗНАЧЕНИЕ ВЫЗОВА
01 38 КОД РАЗМЕР
02 03 СУБ
03 56 ПРЫЖОК
04 FD ВОЗВРАТ
05 FD ВОЗВРАТ
06 5B ПРЫЖОК
07 00 СТОП
08 FD ВОЗВРАТ
09 FD ВОЗВРАТ
? Введите значение для отправки: (0)
Как и раньше, нам нужно ввести значение транзакции для отправки, которое заставит программу работать без возврата. Если мы посмотрим на последовательность инструкций, то увидим, что нам нужен код операции JUMP
, чтобы изменить программный счетчик на 6-ю инструкцию. Как и раньше, первая инструкция — CALLVALUE, поэтому мы знаем, что введенное значение окажется на вершине стека после первой инструкции.
Давайте посмотрим на [инструкцию CODESIZE] (https://www.evm.codes/#38). Этот код операции получает размер кода, работающего в текущей среде. В этом примере мы можем вручную проверить размер кода, посмотрев, сколько опкодов в последовательности. Каждый код операции составляет 1 байт, и в этой головоломке у нас есть 10 кодов операций, что означает, что размер кода составляет 10 байт. Важно отметить, что EVM использует шестнадцатеричные числа для представления байтового кода. Если вы не знакомы, посмотрите как работают шестнадцатеричные числа. Имея это в виду, мы можем знать, что 0a
помещается в стек, представляя 10 байтов.
Следующий код операции, с которым мы сталкиваемся, — это [инструкция SUB] (https://www.evm.codes/#03), которая берет первый элемент стека минус второй элемент стека, помещая результат на вершину стека. Оба входа в верхней части стека перед инструкцией SUB
потребляются. Например, если бы у нас был стек, который выглядел бы так.
```js
[3 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Выполнение инструкции SUB
приведет к следующему результату стека.
```js
[1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
С этой информацией давайте вернемся к головоломке. Теперь мы знаем, что программа сначала оценивает инструкцию CALLVALUE, помещая введенное значение в стек. Затем программа оценивает инструкцию CODESIZE, которая помещает в стек значение 0a (представляющее 10 байт). Мы также знаем, что нам нужен JUMP
, чтобы изменить программный счетчик на 6-ю инструкцию. Вот как выглядит стек после инструкции CODESIZE.
```js
[ваш_ввод 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Если вы еще не закончили головоломку, попробуйте использовать приведенную выше информацию, чтобы ввести правильное значение. В противном случае не стесняйтесь читать последний шаг решения.
Поскольку мы знаем, что следующей будет инструкция SUB
, нам нужно ввести такое значение, чтобы 0a - your_input
было равно 6
, что делает наш ответ равным 4.
Головоломка №3
Приготовьтесь немного переключиться. Вместо того, чтобы вводить значение транзакции для решения головоломки, нам придется вводить данные вызова. Calldata — это доступное только для чтения пространство с байтовой адресацией, в котором хранятся данные транзакции во время сообщения или вызова. Проще говоря, это полезная нагрузка в виде байтового кода, прикрепленная к сообщению (нажмите здесь, чтобы узнать больше об анатомии транзакции в Ethereum) . Давайте посмотрим на ребус.
```js
Головоломка 3
00 36 CALLDATASIZE
01 56 ПРЫЖОК
02 FD ВОЗВРАТ
03 FD ВОЗВРАТ
04 5B ПРЫЖОК
05 00 СТОП
? Введите данные вызова:
Для этой головоломки полезно знать, что 1 байт равен 8 битам и что числа 0-255 могут представлять один байт в EVM. Эта головоломка также знакомит нас с новым опкодом под названием [CALLDATASIZE] (https://www.evm.codes/#36). Эта инструкция получает размер calldata в байтах и помещает его в стек.
С этим знанием это делает головоломку довольно простой. Нам нужно будет передать данные вызова таким образом, чтобы инструкция CALLDATASIZE
поместила 4 в стек. Оттуда инструкция JUMP
перейдет к четвертой инструкции в последовательности, достигнув JUMPDEST
. Для простоты 0xff
можно использовать для представления 1 байта, так как ff
в шестнадцатеричном формате оценивается как 255 в десятичном формате. Все, что нам нужно сделать, это скопировать ff
четыре раза, создав байтовый код, который мы должны ввести: 0xffffffff
. Еще одна головоломка вниз!
Головоломка №4
Вводить побитово. В этой головоломке мы видим нашу первую инструкцию XOR. Как обычно, не стесняйтесь попробовать и понять это самостоятельно. Когда будете готовы, вернитесь сюда за решением и объяснением.
```js
Головоломка 4
00 34 ЗНАЧЕНИЕ ВЫЗОВА
01 38 КОД РАЗМЕР
02 18 Исключающее ИЛИ
03 56 ПРЫЖОК
04 FD ВОЗВРАТ
05 FD ВОЗВРАТ
06 FD ВОЗВРАТ
07 FD ВОЗВРАТ
08 FD ВОЗВРАТ
09 FD ВОЗВРАТ
0A 5B ПРЫЖОК
0B 00 СТОП
? Введите значение для отправки: (0)
Мы знаем, что CALLVALUE
поместит введенное нами значение на вершину стека. Также мы можем узнать, насколько велик CODESIZE
, взглянув на количество инструкций. В этой программе у нас есть 12 инструкций, что составляет 12 байтов или «0c» в шестнадцатеричном формате, которые помещаются в стек. Итак, теперь наш стек выглядит так.
```js
[c your_input 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Давайте посмотрим на [инструкцию XOR] (https://www.evm.codes/#18). Эта инструкция оценивает два числа в их двоичном представлении и возвращает 1 в каждой битовой позиции, где биты одного, но не обоих операндов равны 1. Давайте посмотрим на быстрый пример. Скажем, у нас есть два числа на вершине стека.
```js
[5 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
При выполнении инструкции «XOR» мы можем представить два числа в двоичном представлении следующим образом.
```js
Затем по крупицам эти два числа сравниваются друг с другом. Если один бит равен «0», а другой бит равен «1», результирующий бит будет «1», если оба бита равны «0» или оба бита равны «1», результирующее число равно 0
. Итак, результат «5 XOR 3» таков.
```js
Вернемся к головоломке. Мы знаем, что у нас есть 0c
наверху стека, а your_input
во второй позиции стека. После «XOR» код операции «JUMP» должен отправить нас к 10-й инструкции. Теперь, когда вся эта информация известна, нам просто нужно ввести значение вызова, чтобы «0c XOR callvalue» приводило к шестнадцатеричному «10». Давай, попробуй сам.
Хорошо, теперь последние шаги. Мы знаем, что нам нужно, чтобы результатом «XOR» было «10», что в двоичном виде представлено как «1010». У нас также есть 0c
в стеке, который в двоичном виде представлен как 1100
. Итак, теперь нам нужно найти такое число, что c XOR your_input
приводит к 1010
, что делает число, которое нам нужно ввести, 0110
. Это оценивается как шестнадцатеричное число 06
. 6 наш ответ!
Головоломка №5
Добро пожаловать в следующую головоломку, где нас встречают несколько новых опкодов. Не стесняйтесь попробовать. А пока давайте посмотрим на последовательность инструкций для этой головоломки.
```js
Головоломка 5
00 34 ЗНАЧЕНИЕ ВЫЗОВА
01 80 ДУП1
02 02 МУЛ
03 610100 ПУШ2 0100
06 14 эквалайзер
07 600С НАЖМИТЕ1 0С
09 57 ДЖУМПИ
0A ПОВТОР
0B FD ВОЗВРАТ
0C 5B ПРЫЖОК
0D 00 СТОП
0E ПЕРЕЗАГРУЗКА
0F ПОВТОР
? Введите значение для отправки: (0)
DUP1
соответствует считывателю, считыватель соответствует DUP1
. [Инструкция DUP1] (https://www.evm.codes/#80) довольно проста. Он дублирует значение в 1-й позиции в стеке и помещает дубликат на вершину стека. Точно так же DUP2
будет дублировать значение во второй позиции в стеке и помещать повторяющееся значение наверх. Существуют инструкции DUP для всех позиций в стеке (DUP1-DUP16
).
Взглянув на первые две инструкции головоломки, первая CALLVALUE
выполняется, помещая введенное значение на вершину стека. Затем выполняется DUP1
, дублируя введенное значение и помещая его на вершину стека. Итак, после первых двух инструкций наш стек выглядит так.
```javascript
[your_input your_input 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Затем нас встречает еще одна новая инструкция. Инструкция MUL берет первые два значения из стека, перемножает их и помещает результат на вершину стека. Итак, в этом случае your_input
умножается на your_input
, и результирующий стек выглядит следующим образом.
```js
[mul_result 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Далее мы сталкиваемся с [инструкцией PUSH2] (https://www.evm.codes/#61). Эта инструкция помещает 2-байтовое значение на вершину стека. Когда вы видите любую инструкцию PUSH
, она всегда будет сопровождаться значением, которое она будет нажимать. Например, в нашей головоломке у нас есть «PUSH2 0100», что означает, что он поместит 2-байтовое шестнадцатеричное число «0100» на вершину стека. Существуют инструкции push от PUSH1
до PUSH32
.
Возвращаясь к нашей головоломке, поскольку следующая инструкция — «PUSH2 0100», наш результирующий стек теперь будет выглядеть так.
```js
[0100 mul_result 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Теперь мы сталкиваемся с [инструкцией EQ] (https://www.evm.codes/#14). Эта инструкция берет первые два значения из стека, выполняет сравнение на равенство и помещает результат на вершину стека. Если первые два значения равны, «1» помещается наверх, в противном случае вместо этого в стек помещается «0». Оба значения в позициях 1 и 2 в стеке потребляются из инструкции «EQ».
Для простоты предположим, что mul_result равен 0100, поэтому, когда выполняется инструкция EQ, в стек помещается 1, в результате чего наш стек теперь выглядит так.
```js
[1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Следующей оцениваемой командой является «PUSH1 0C», которая помещает «0c» на вершину стека. Следуя этой инструкции, мы видим еще одну новую инструкцию. [Инструкция JUMPI] (https://www.evm.codes/#57) условно изменит счетчик программ. Эта инструкция смотрит на второй элемент стека, чтобы узнать, должен ли он переходить или нет, в зависимости от того, является ли второй элемент стека «1» или «0». Затем используется первый элемент стека, чтобы узнать, на какую позицию перейти. Инструкция JUMPI
использует оба значения на вершине стека во время этого процесса. Итак, взглянув на нашу головоломку, после инструкции PUSH1 0c
наш стек выглядит так.
```js
[0c 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Сначала инструкция JUMPI
проверяет второй элемент стека. В данном случае это «1», указывающее, что программа должна перейти. Затем JUMPI
проверяет первый элемент стека, чтобы узнать, куда он должен перейти. Верхнее значение стека равно 0c
, что означает, что он перейдет к 12-й инструкции, которая является нашей JUMPDEST
.
И это завершит нашу головоломку! Итак, со всей этой информацией мы теперь знаем, что нам нужно ввести значение вызова, чтобы, когда оно дублируется один раз (делая первые два элемента в стеке значением вызова) и после умножения значений верхнего стека, наш результат был шестнадцатеричным числом 0100
. Не стесняйтесь попробовать отсюда и посмотреть, сможете ли вы это понять.
Хорошо, теперь последние шаги. Мы можем преобразовать «0100» в десятичное число и получить 256. Затем мы можем извлечь квадратный корень из 256, так как «DUP1 MUL», по сути, умножает число само на себя. В результате получится число 16, которое является ответом на эту загадку!
Головоломка №6
5 головоломок позади, 5 осталось! Как обычно, попробуйте решить головоломку, а затем не стесняйтесь возвращаться сюда за решением и объяснением.
```js
Головоломка 6
00 6000 НАЖАТЬ1 00
02 35 ВЫЗОВ ЗАГРУЗКА ДАННЫХ
03 56 ПРЫЖОК
04 FD ВОЗВРАТ
05 FD ВОЗВРАТ
06 FD ВОЗВРАТ
07 FD ВОЗВРАТ
08 FD ВОЗВРАТ
09 FD ВОЗВРАТ
0A 5B ПРЫЖОК
0B 00 СТОП
? Введите данные вызова:
Поздоровайтесь с инструкцией CALLDATALOAD. Эта инструкция получает входные данные из данных вызова, прикрепленных к транзакции. Есть несколько важных вещей, которые следует отметить об этом опкоде. CALLDATALOAD
ожидает целое число наверху стека, чтобы знать, с какого байта начинать загрузку данных вызова. Например, если вы отправляете транзакцию с 32-байтовой последовательностью в качестве данных вызова и помещаете «08» на вершину стека, когда вы выполняете «CALLDATALOAD», все данные вызова от байта 8 до байта 32 будут помещены на вершину стека. стека. В качестве дополнительного примечания, если данные вызова составляют 64 байта и вам нужно получить доступ ко вторым 32 байтам последовательности, вы можете поместить «20» в стек, а затем использовать «CALLDATALOAD», чтобы получить вторые 32 байта последовательности.
Теперь вернемся к головоломке. Мы видим, что за PUSH1 00 следует CALLDATALOAD, что означает, что данные вызова будут загружены, начиная с байта 0, а байты 0-32 данных вызова будут помещены на вершину стека. Мы видим, что инструкция JUMP
должна изменить программный счетчик на 0a
(т.е. 10-я инструкция). Не стесняйтесь остановиться здесь и попытаться решить оставшуюся часть головоломки.
Хорошо, давайте пройдемся по последним шагам. Мы знаем, что данные вызова представлены в шестнадцатеричном формате, поэтому может показаться интуитивно понятным ввод «0x0a» в качестве данных вызова для завершения головоломки, но вы могли заметить, что это не работает. Это связано с тем, что при отправке данных вызова, поскольку последовательность байтов не была 32 байта, она дополняется вправо, поэтому то, что мы считали «0a», на самом деле превращается в «a00000000000000000000000000000000000000000000000000000000000000000000». Итак, что нам нужно сделать, это дополнить наш 0x0a
31 байтом слева, что сделает его 0x000000000000000000000000000000000000000000000000000000000000000000000a
. Ну вот, это наш ответ!
Головоломка №7
Вы знаете, что делать. Попробуйте решить головоломку, а затем вернитесь, чтобы увидеть полное решение / объяснение.
```js
Головоломка 7
00 36 CALLDATASIZE
01 6000 НАЖМИТЕ1 00
03 80 ДУП1
04 37 КОПИРОВАНИЕ ДАННЫХ ВЫЗОВА
05 36 CALLDATASIZE
06 6000 НАЖМИТЕ1 00
08 6000 НАЖМИТЕ1 00
0A F0 СОЗДАТЬ
0B 3B EXTCODESIZE
0С 6001 НАЖАТЬ1 01
0E 14 эквалайзер
0F 6013 НАЖАТЬ1 13
11 57 ЮМПИ
12 FD ВОЗВРАТ
13 5B ПРЫЖОК
14 00 СТОП
? Введите данные вызова:
Во-первых, мы видим CALLDATASIZE и знаем, что нам нужно будет ввести данные вызова определенного размера, чтобы решить эту головоломку. Давайте примем это к сведению и вернемся позже. После того, как размер данных вызова помещается в стек, появляется PUSH1 00 и DUP1, из-за чего наш стек в этот момент выглядит следующим образом.
```js
[0 0 calldata_size 0 0 0 0 0 0 0 0 0 0 0 0 0]
Далее мы сталкиваемся с инструкцией CALLDATACOPY. Эта инструкция копирует входные данные из транзакции и сохраняет их в памяти. Этот код операции ожидает три элемента в верхней части стека, которые имеют размер смещения [destOffset offset]
, в этом порядке. destOffset
– это смещение в байтах в памяти, куда будет скопирован результат. На данный момент мы мало говорили о памяти, и если вы хотите узнать больше, вы можете прочитать об этом здесь. Сокращенная версия заключается в том, что существует временная структура данных, которая выделяет пространство для хранения значений во время выполнения функции, а destOffset
сообщает программе, в каком слоте памяти хранить данные, которые копируются из calldata. «Смещение» определяет, откуда начинать копирование данных вызова (точно так же, как «CALLDATALOAD» делает это в последнем примере), а «размер» сообщает программе, какую часть последовательности байтов нужно хранить в памяти. Во время этого процесса потребляются все три верхних элемента стека.
Зная все это, давайте вернемся к нашему текущему стеку.
```js
[0 0 calldata_size 0 0 0 0 0 0 0 0 0 0 0 0 0]
Когда выполняется инструкция CALLDATALOAD
, она сохраняет данные вызова в слоте памяти 0
, начиная с байта 0
, и сохраняет размер всех данных вызова. Наш получившийся стек после этой инструкции выглядит так.
```js
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Сразу после этого выполняются все команды CALLDATASIZE
PUSH1 00
PUSH1 00
, в результате чего стек выглядит точно так же, как он был до выполнения CALLDATALOAD
.
```js
[0 0 calldata_size 0 0 0 0 0 0 0 0 0 0 0 0 0]
Далее мы знакомимся с другим новым кодом операции, [инструкцией CREATE] (https://www.evm.codes/#f0). Эта инструкция создает новую учетную запись (т.е. контракт или EOA). Давайте немного углубимся в код операции CREATE, так как он пригодится позже во время прохождения (и, как правило, его полезно знать).
При развертывании нового контракта с опкодом CREATE стек должен иметь [value offset size]
наверху стека в указанном порядке. «Значение» — это количество wei для отправки нового создаваемого контракта, «смещение» — это место в памяти, где начинается байт-код, который будет выполняться при развертывании, а «размер» — это размер байт-кода, который будет выполняться. при развертывании. Когда вы развертываете контракт с опкодом CREATE, байт-код из «смещения» не является байт-кодом нового контракта, а скорее байт-код из «смещения» выполняется во время развертывания, и возвращаемое значение становится новым байт-код созданного контракта.
Давайте пробежимся по быстрому примеру, который облегчит понимание. Если вы используете код операции CREATE
с байт-кодом развертывания 0x6160016000526002601Ef3, поскольку возвращаемое значение этой последовательности байт-кода 6001
, байт-код вновь созданного контракта будет 6001
, т.е. НАЖМИТЕ1 01
. Поэтому, когда вы вызываете этот контракт, он просто выполняет PUSH1 01! Обязательно запомните эту концепцию, так как она пригодится позже.
Когда выполняется инструкция CREATE, все три значения используются, а адрес, на который была развернута учетная запись, помещается на вершину стека. После выполнения этого опкода наш стек выглядит так.
```js
[address_deployed 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Далее мы сталкиваемся с инструкцией EXTCODESIZE, которая ожидает адрес на вершине стека и возвращает размер кода по этому адресу. В этом процессе используется адрес в верхней части стека. После EXTCODESIZE мы видим PUSH1 01, из-за чего наш стек выглядит так.
```js
[01 address_code_size 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Сразу после этого выполняется инструкция EQ
, проверяющая, равны ли два верхних значения, помещая результат в стек. Оттуда используются PUSH1 13
и JUMPI
, чтобы перейти к JUMPDEST
! Итак, возвращаясь к началу головоломки, нам нужно будет ввести данные вызова так, чтобы размер кода был равен 01 байту! Это немного сложно, поэтому, чтобы понять это, мы можем посмотреть на пример игровой площадки из [инструкции EXTCODESIZE] (https://www.evm.codes/#3b). Вот как выглядит пример.
```js
// Создает конструктор, который создает контракт с 32 FF в виде кода
PUSH32 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
НАЖАТЬ1 0
МАГАЗИН
//Опкоды для возврата 32 ff
PUSH32 0xFF60005260206000F3000000000000000000000000000000000000000000000000
НАЖМИТЕ1 32
МАГАЗИН
// Создаем контракт с кодом конструктора выше
НАЖМИТЕ1 41
НАЖАТЬ1 0
НАЖАТЬ1 0
CREATE // Помещает новый адрес контракта в стек
// Адрес находится в стеке, мы можем запросить размер
EXTCODESIZE
Давайте подробнее рассмотрим коды операций в конструкторе.
```js
// Поместить 32-байтовое значение в стек
PUSH32 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
НАЖМИТЕ1 00
// Сохраняем 32-байтовое значение в ячейке памяти 0
МАГАЗИН
// Возвращаем 32-байтовое значение, начиная с 0-го слота памяти
НАЖАТЬ1 20
НАЖМИТЕ1 00
ВЕРНУТЬ
ОСТАНОВКА
ОСТАНОВКА
Когда этот код запускается, он возвращает значение ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
, которое составляет 32 байта. Если мы изменим возвращаемый размер на 16 байтов вместо 32 байтов, EXTCODESIZE
будет 10
, что составляет 16 байтов в шестнадцатеричном формате. Это указывает на то, что EXTCODESIZE
использует размер возвращаемого значения для определения размера кода.
Закончим головоломку. Теперь мы знаем, что EXTCODESIZE
оценивает размер возвращаемого значения из развернутого байт-кода. С помощью этой информации мы можем передавать данные вызова таким образом, чтобы при их развертывании возвращалось значение размером 1 байт! Вы можете использовать любую последовательность кодов операций, которая возвращает 1 байт, но в этом пошаговом руководстве мы будем использовать «0x60016000526001601ff3». И с этим, еще одна головоломка решена!
Головоломка №8
Добро пожаловать в восьмую головоломку. Давайте посмотрим, что есть в магазине.
```js
Головоломка 8
00 36 CALLDATASIZE
01 6000 НАЖМИТЕ1 00
03 80 ДУП1
04 37 КОПИРОВАНИЕ ДАННЫХ ВЫЗОВА
05 36 CALLDATASIZE
06 6000 НАЖМИТЕ1 00
08 6000 НАЖМИТЕ1 00
0A F0 СОЗДАТЬ
0B 6000 НАЖМИТЕ1 00
0D 80 ДУП1
0E 80 ДУП1
0F 80 ДУП1
10 80 ДУП1
11 94 ОБМЕН5
12 5А ГАЗ
13 ВЫЗОВ F1
14 6000 НАЖАТЬ1 00
16 14 Эквалайзер
17 601Б ПУШ1 1Б
19 57 ЮМПИ
1A ПЕРЕДН.
1B 5B ПРЫЖОК
1С 00 СТОП
? Введите данные вызова:
Это может показаться более сложным, но на самом деле это довольно просто. Сначала мы видим очень похожую CALLDATASIZE PUSH1 00 DUP1 CALLDATACOPY CALLDATASIZE PUSH1 00 PUSH1 00 CREATE
, которая, как и предыдущая головоломка, создает новый контракт из передаваемых вами данных вызова и возвращает адрес развертывания. Итак, с самого начала мы знаем, что нам нужно будет ввести calldata с байт-кодом для контракта, чтобы решить головоломку. Давайте быстро вспомним, как выглядит стек на данный момент. Поскольку инструкция CREATE использует три верхних значения стека и помещает адрес, на который была развернута учетная запись, наш стек теперь выглядит следующим образом.
```js
[address_deployed 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Все следующие 5 инструкций относятся к [инструкции CALL] (https://www.evm.codes/#f1). Эта инструкция создает новый подконтекст и выполняет код данной учетной записи, а затем возобновляет текущий. Проще говоря, инструкция CALL используется для взаимодействия с другим контрактом. Этот код операции ожидает, что стек будет иметь несколько значений в верхней части стека [значение адреса газа argsOffset argsSize retOffset retSize]
, в этом порядке. Давайте рассмотрим каждый из аргументов один за другим. gas
- это количество газа, которое будет отправлено с вызовом сообщения. address
— это адрес, на который будет отправлено сообщение. «значение» — это количество wei, которое будет отправлено вместе с сообщением. argsOffset
— это место в памяти в текущем контексте (т. е. msg.sender), которое будет использоваться в качестве данных вызова для вызова сообщения. argsSize
— это размер calldata для отправки с вызовом сообщения. retOffset
— это место в памяти в текущем контексте, где будет храниться возвращаемое значение из вызова. Наконец, retSize
— это размер возвращаемого значения, которое будет храниться в памяти.
Теперь давайте снова посмотрим на ребус. Следующие четыре кода операции — «PUSH1 00 DUP1 DUP1 DUP1 DUP1», поэтому стек выглядит следующим образом.
```js
[0 0 0 0 0 address_deployed 0 0 0 0 0 0 0 0 0 0]
Далее видим инструкцию SWAP5. Эта инструкция меняет местами 1-й и 6-й элементы стека. Инструкции SWAP
существуют для всех позиций в стеке (SWAP1
-SWAP16
). В этом случае SWAP5
заменяет 0
на address_deployed
, делая наш стек теперь в правильном порядке, чтобы соответствовать [значение адреса газа argsOffset argsSize retOffset retSize]
. Вот как сейчас выглядит наш стек.
```js
[address_deployed 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Затем мы выполняем инструкцию CALL
, которая возвращает 0
, если подконтекст вернулся, и 1
, если это было успешно. После инструкции «CALL» мы видим «PUSH1 00 EQ», что означает, что нам нужно «CALL», чтобы поместить «0» в стек. Идите вперед и попробуйте остальную часть головоломки, а затем не стесняйтесь вернуться, чтобы увидеть остальную часть решения.
Итак, теперь мы знаем, что инструкция CALL
должна возвращать 0
, что означает, что нам нужно ввести данные вызова, которые вызывают ошибку CALL
. Есть три способа заставить CALL
потерпеть неудачу. Один из способов, которым он может выйти из строя, - это если не хватает газа. Второй способ может дать сбой, если в стеке недостаточно значений. Третий способ может привести к сбою, если текущий контекст выполнения исходит из STATICCALL, а значение в wei (индекс стека 2) не равно 0 (начиная с форка Byzantium) . Также важно отметить, что CALL
всегда будет считаться истинным, если вы CALL
учетной записи без кода (или с размером кода 0).
Чтобы закончить эту головоломку, давайте вернемся к тому, как работает код операции CREATE. Мы знаем, что возвращаемое значение байт-кода, запускаемого при развертывании, становится байт-кодом для вновь созданного контракта. Зная эту информацию, мы можем передать данные вызова с последовательностью байт-кода, чтобы возвращаемое значение последовательности вызывало REVERT
при запуске.
Вы можете указать любой, который приведет к REVERT
, но для прохождения мы будем использовать 0x60016000526001601ff3 как байт-код развертывания. Поскольку возвращаемое значение этой последовательности байт-кода равно 01
, код вновь созданного контракта будет 01
, т.е. инструкция «ДОБАВИТЬ». Таким образом, когда вы вызываете этот контракт, он выполняет инструкцию ADD
, и, поскольку в стеке нет значений в подконтексте контракта, CALL
не удастся (т.е. REVERT
)! Итак, наш ответ: 0x60016000526001601ff3
!
Головоломка №9
Мы на финишной прямой, давайте посмотрим на головоломку № 9. Эта головоломка добавляет еще один уровень сложности, требуя, чтобы вы вводили как значение вызова, так и данные вызова, чтобы решить головоломку.
```js
Головоломка 9
00 36 CALLDATASIZE
01 6003 НАЖМИТЕ1 03
03 10 ДВ
04 6009 НАЖМИТЕ1 09
06 57 ДЖУМПИ
07 FD ВОЗВРАТ
08 FD ВОЗВРАТ
09 5B ПРЫЖОК
0A 34 ЗНАЧЕНИЕ ВЫЗОВА
0B 36 CALLDATASIZE
0C 02 МУЛЬТ
0D 6008 НАЖМИТЕ1 08
0F 14 эквалайзер
10 6014 НАЖМИТЕ1 14
12 57 ЮМПИ
13 FD ВОЗВРАТ
14 5B ПРЫЖОК
15 00 СТОП
? Введите значение для отправки: (0)
Мы уже знакомы с первыми двумя кодами операций, поэтому мы можем знать, что после инструкций CALLDATASIZE PUSH1 03
наш стек выглядит следующим образом.
```js
[03 calldata_size 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Инструкция LT выполняет сравнение первых двух значений стека, чтобы определить, меньше ли первый элемент стека, чем второй элемент стека. Если значение LT равно true, в стек помещается 1, в противном случае вместо него помещается 0. В этом процессе используются два значения, используемые при сравнении. Для примера предположим, что CALLDATASIZE имеет размер 4 байта, поэтому в результате LT поместит 1.
Поскольку значение LT равно true, код затем переходит к JUMPDEST в инструкции 09. После перехода CALLVALUE и CALLDATASIZE помещаются в стек, а MUL умножает их вместе, потребляя в процессе два верхних значения стека. PUSH1 08
помещает 08
в стек, а затем EQ
проверяет, равен ли результат MUL
08
, потребляя значения в процессе. EQ
должен поместить 1
в стек, чтобы позволить JUMPI
привести нас к концу головоломки.
Со всей этой информацией мы теперь знаем, что нам нужно вводить данные вызова таким образом, чтобы CALLDATASIZE был больше 3 байт, а произведение CALLDATASIZE * CALLVALUE было равно 08.
Немного посчитав, мы можем использовать любую комбинацию значений, которые при умножении дают 8 и удовлетворяют приведенным выше условиям. Для пошагового руководства мы введем «0x00000001» в качестве данных вызова и «2» в качестве значения вызова. Еще одна головоломка впереди!
Головоломка №10
Вот она, последняя загадка. Давайте прыгать.
```js
Головоломка 10
00 38 КОД РАЗМЕР
01 34 ЗНАЧЕНИЕ ВЫЗОВА
02 90 ОБМЕН1
03 11 ГТ
04 6008 НАЖМИТЕ1 08
06 57 ДЖУМПИ
07 FD ВОЗВРАТ
08 5B ПРЫЖОК
09 36 CALLDATASIZE
0A 610003 НАЖМИТЕ2 0003
0D 90 ОБМЕН1
0E 06 МОД
0F 15 ИСНУЛЕВОЙ
10 34 ВЫЗОВ ЗНАЧЕНИЕ
11 600А НАЖМИТЕ1 0А
13 01 ДОБАВИТЬ
14 57 ЮМПИ
15 FD ВОЗВРАТ
16 FD ВОЗВРАТ
17 FD ВОЗВРАТ
18 FD ВОЗВРАТ
19 5B ПРЫЖОК
1А 00 СТОП
? Введите значение для отправки: (0)
В этой головоломке вам нужно будет ввести значение вызова, а также данные вызова. Давайте взглянем на первые несколько инструкций. Сначала мы видим CODESIZE CALLVALUE SWAP1
, который подталкивает размер кода, за которым следует значение вызова, которое вы передали, а затем меняет их позиции. На данный момент наш стек выглядит так.
```js
[1b callvalue 0 0 0 0 0 0 0 0 0 0 0 0 0]
Затем мы используем [инструкцию GT] (https://www.evm.codes/#11), которая работает точно так же, как LT
, но оценивает больше, чем меньше, чем. Для этой головоломки нам нужно GT
, чтобы поместить 1
в стек, поэтому мы знаем, что наше значение вызова должно быть меньше, чем 1b
(т.е. 27 в десятичном представлении). Это позволит программе перейти к первому JUMPDEST
в инструкции 08
.
Теперь мы видим CALLDATASIZE PUSH2 0003 SWAP1
, который помещает размер данных вызова, а также 0003
в стек и меняет их позиции. Теперь наш стек выглядит так.
```js
[calldata_size 3 0 0 0 0 0 0 0 0 0 0 0 0 0]
Далее мы видим инструкцию MOD. Эта инструкция выполняет модуль первого элемента стека и второго элемента стека, помещая остаток в стек. После инструкции MOD
мы видим инструкцию ISZERO, которая помещает в стек 1
, если верхнее значение в стеке равно 0
. Если любое другое число находится на вершине стека, вместо него в стек помещается 0
. В нашем случае нам нужно ISZERO
, чтобы поместить 1
в стек (мы еще вернемся к этому). Затем мы видим CALLVALUE PUSH1 0A ADD
. Инструкция ADD просто добавляет первые два значения в стек и помещает результат в стек. После этой последовательности следует JUMPI
, что означает, что CALLVALUE PUSH1 0A ADD
необходимо поместить позицию JUMPDEST
в стек. Не стесняйтесь попробовать остальную часть головоломки отсюда.
Со всей этой информацией мы теперь знаем несколько вещей. Во-первых, нам нужно ввести данные вызова таким образом, чтобы размер данных вызова делился на 3 байта, позволяя последовательности CALLDATASIZE PUSH2 0003 SWAP1 MOD поместить в стек значение 0. Это позволяет ISZERO
помещать 1
в стек, где программа затем может перейти ко второму JUMPDEST
. Во-вторых, нам нужно ввести значение вызова таким образом, чтобы значение было меньше 26, а callvalue + 0a
равнялось 0x19
. Зная эти коэффициенты, мы можем ввести «0x000001» в качестве данных вызова и «15» (в десятичном формате) в качестве значения вызова. Вот так мы и собрали последнюю головоломку!
Оригинал