EVM: превращаем аккаунт в компьютер
Глава 1 дала нам аккаунты и неподделываемые подписанные транзакции — но транзакция всё ещё могла делать только одну жёстко закодированную вещь: перемещать ценность. Эта глава выводит машину, которая позволяет аккаунту выполнять *любую* программу: стек, способ платить за вычисления и три места для хранения данных. Постройте её — и EVM появится сама собой.
- аккаунты и подписанные транзакции
К концу главы 1 у нас были общий реестр, аккаунты и транзакции, которые неподделываемы — подписаны ключом, которым владеет только их хозяин. Но присмотритесь, что транзакция на самом деле умеет: она перемещает ценность от одного аккаунта к другому. И всё. Это правило намертво впаяно в протокол. Если вам нужно что-то богаче — эскроу, который раскрывается, когда двое соглашаются, токен, который не ETH, аукцион — вы не можете это выразить, потому что некуда положить логику и нечем её выполнить. Эта глава чинит ровно это, и вещь, которую мы строим для этого — Ethereum Virtual Machine. Мы изобретём её так же, как и всё остальное: назовём недостающую часть, добавим наименьшую вещь, которая её обеспечивает, и посмотрим, что сломается дальше.
Проблема: единственный глагол — «перевести»
Транзакция называет получателя и сумму, а протокол применяет одно фиксированное правило: вычесть здесь, прибавить там. Нет способа сказать «переместить это, только если X» или «сделать вот это совсем другое», потому что аккаунт хранит лишь баланс и nonce — никакого поведения. Чтобы сделать аккаунты программируемыми, идея напрашивается сама: дать аккаунту поле код и дать протоколу машину, которая выполняет этот код при каждом обращении к аккаунту.
Аккаунт с кодом — это контрактный аккаунт Аккаунт, чьё поведение определяется кодом, хранящимся в блокчейне. При обращении к нему EVM детерминированно выполняет этот код на каждом узле; у него есть собственный баланс и хранилище, но нет закрытого ключа. . Теперь вопрос в том, как выглядит эта машина. Она должна быть предельно простой, потому что каждый узел на Земле должен выполнить её и получить побитово одинаковый ответ — иначе они разойдутся во мнении о новом состоянии, и цепочка расколется.
→ Шаг 2: простейший компьютер, способный вычислить что угодно.
Выводим машину: стек, и больше ничего
ЦПУ с регистрами трудно специфицировать в точности, и по нему легко не сойтись во мнениях. Поэтому берём самую минимальную модель, которая всё ещё тьюринг-полна: стековая машина Компьютер без именованных регистров или переменных. Инструкции берут свои операнды с вершины стека по принципу «последним пришёл — первым ушёл» и кладут результаты обратно на него. Прост для точной спецификации — именно поэтому EVM устроен так. . В ней нет ни переменных, ни регистров — только LIFO-стек значений. Каждая инструкция ( опкод Однобайтовая инструкция EVM, например 0x01 = ADD, 0x60 = PUSH1, 0x02 = MUL. Код контракта — это просто строка таких байтов; EVM читает их по одному. ) занимает один байт: она снимает с вершины то, что ей нужно, и кладёт обратно то, что произвела.
Звучит абстрактно, пока вы не увидите это своими глазами. Вот программа для (2 + 3) × 4 — пять опкодов — выполняющаяся по одному шагу за раз. Следите за стеком:
Заметьте, что только что произошло. ADD не нужно было говорить, где находятся 2 и 3 — они просто были верхними двумя элементами, и он заменил их на 5. В этом весь фокус: поскольку операнды всегда берутся с вершины стека, поведение каждого опкода тривиально определить, и любой узел, выполняющий те же байты, получит тот же результат. Соедините достаточно таких однобайтовых инструкций — и сможете вычислить что угодно.
→ Шаг 3: сделаем так, чтобы каждый шаг что-то стоил.
Газ: вычисления, за которые платят и которые всегда останавливаются
В общем случае нельзя посмотреть на программу и решить, остановится ли она вообще — это проблема остановки, и это не брешь, которую можно обойти инженерными средствами. Так что даже не пытайтесь. Вместо этого назначьте цену каждому опкоду и заставьте каждую транзакцию нести бюджет. Выполнение инструкции тратит из этого бюджета; когда бюджет достигает нуля, выполнение немедленно останавливается. Этот бюджет — газ Единица, измеряющая работу EVM. У каждого опкода фиксированная цена в газе; транзакция задаёт лимит газа и платит за сожжённый газ. Когда газ заканчивается, выполнение останавливается и откатывается — именно это делает безграничные циклы безопасными. .
Один этот ход решает сразу две проблемы. Он делает вычисление оплаченным, поэтому заспамить сеть тяжёлой работой стоит реальных денег; и он делает выполнение ограниченным, потому что на конечный бюджет можно купить лишь конечное число шагов. Бесконечный цикл больше не опасен — он просто прожигает свой газ и останавливается.
- 1 фиксированная цена за инструкцию — 3 за ADD, 20000 за запись нового слота хранилища
- 2 бюджет, который прикладывает отправитель; достигнув его, выполнение откатывается
→ Шаг 4: три места для данных, по времени жизни.
Три места для данных, оценённые по тому, сколько они живут
У разных данных разное время жизни, и именно оно должно определять цену. Поэтому EVM даёт коду три разных места для хранения значений, и вся конструкция построена так, что цена отслеживает постоянство.
- стек Рабочая поверхность, где опкоды берут операнды и оставляют результаты. Максимум 1024 элемента, каждый — 256-битное слово. Практически бесплатен, но существует только в рамках текущего вызова. — для значений, которые нужны инструкции прямо сейчас.
- память Линейное, адресуемое по байтам черновое пространство, которое начинается пустым при каждом вызове и растёт по мере необходимости. Дёшево использовать, но она отбрасывается по завершении вызова. — черновик на время вызова: собрать возвращаемое значение, захешировать блок байтов.
- хранилище Постоянное хранилище контракта ключ→значение, зафиксированное в дереве состояния и хранимое на диске каждого узла. Чтение и особенно запись (SSTORE) — самые дорогие из обычных операций, потому что все хранят это вечно. — постоянная память контракта, единственное место, где данные переживают одну транзакцию и доживают до следующей.
Поскольку хранилище записывается на диск каждого узла и хранится вечно, оно намеренно сделано самым дорогим ресурсом машины — на порядки дороже обращения к стеку. Именно этот разрыв в цене — зерно удивительного числа более поздних апгрейдов: ценообразование холодного и тёплого доступа, чтобы первое обращение отражало реальную цену диска, и временное хранилище для данных, которым нужно прожить лишь одну транзакцию. Держите этот разрыв в уме — мы вернёмся к нему в Части III.
→ Шаг 5: запустите байткод сами.
Настоящая машина, шаг за шагом
Всё, что было выше, анимировано, чтобы объяснить форму. А это — настоящая машина: реальная реализация EVM, выполняющая настоящий байткод. Пройдите пресеты пошагово — тот же (2 + 3) × 4, запись-и-чтение, RETURN и обратный отсчёт в цикле — и следите, как стек, память, хранилище, газ и счётчик команд меняются на каждой инструкции. Отредактируйте байткод — и он выполнится заново.