EIP-140: REVERT — дёшево проваливаться и объяснять почему
До Byzantium контракт, столкнувшийся с плохим условием, имел только один грубый вариант — throw: он откатывал работу, но при этом сжигал весь газ вызывающего до последней капли и не возвращал никакого объяснения. REVERT сохраняет откат, возвращает газ и позволяет контракту вернуть ошибку. Каждый require("reason"), кастомная ошибка и try/catch, которые вы когда-либо писали, — потомки этого опкода.
- EVM, газ и модель вызовов
- изменения состояния
Byzantium, октябрь 2017. Крупный форк — он принёс криптографические прекомпайлы, сделавшие возможными доказательства с нулевым разглашением on-chain, и тип вызова, который не может трогать состояние. Но изменение, которое каждый Solidity-разработчик ощущает ежедневно, куда скромнее: новый способ проваливаться. До этого момента контракт, обнаруживший плохое условие — списание сверх баланса, неудачную проверку прав, — мог только throw, а throw был актом выжженной земли: он откатывал работу, но при этом сжигал каждую единицу газа, оставшуюся у вызывающего, и не возвращал ни байта объяснения. EIP-140 вводит REVERT, и вместе с ним провал становится дешёвым, точным и читаемым. Давайте выведем его.
Проблема: прерывание стоило всего и не говорило ничего
Вызов контракта либо завершается успешно и фиксирует изменения состояния, либо прерывается — и тогда их нужно откатить: вы никогда не хотите наполовину применённого перевода. До Byzantium единственным способом прерваться было throw Способ прерывания до Byzantium: выполнение невалидной инструкции (или throw в Solidity), которое откатывало изменения состояния фрейма, но потребляло весь оставшийся газ и не возвращало данных. — обычно натыкаясь на невалидный опкод. Это действительно откатывало состояние, и это правильно. Но было два жестоких побочных эффекта: потреблялся весь оставшийся у вызывающего газ, и не возвращалось никаких данных. Так что вполне разумный require(balance >= amount), который просто не выполнился, сжигал весь газовый бюджет пользователя и оставлял его смотреть на голое «транзакция не выполнена» без единой причины.
→ Шаг 2: сохраняем откат, убираем наказание, добавляем причину.
REVERT: три исправления в одном опкоде
REVERT Опкод Byzantium 0xfd. Останавливает текущий вызов, атомарно откатывает все изменения состояния этого фрейма, возвращает вызывающему неиспользованный газ и возвращает блок данных (offset, length) — им пользуются, чтобы передать причину ошибки. (опкод 0xfd) делает ровно то, что нам нужно, — сразу три вещи. Он откатывает все изменения состояния, сделанные в текущем фрейме вызова, — атомарно, будто фрейм вообще не выполнялся. Он возвращает оставшийся газ вместо того, чтобы его сжигать, поэтому ожидаемый провал стоит только газа, реально потраченного до этого момента. И он возвращает данные — произвольный блок (offset, length) из памяти, — в котором и уезжает обратно строка ошибки или структурированный код ошибки.
- 1 каждый SSTORE, изменение баланса и лог в этом фрейме откатывается — атомарно
- 2 неиспользованный газ возвращается вызывающему; теряется только уже потраченный газ
- 3 блок памяти всплывает наверх — это строка ошибки или данные кастомной ошибки
→ Шаг 3: пусть вызывающий её перехватит.
Выигрыш: провал, который вызывающий может перехватить и прочитать
Поскольку REVERT возвращает данные и не уничтожает транзакцию целиком, вложенный вызов, который делает revert, возвращается к своему вызывающему как обычный проваленный вызов, несущий свою причину, — а не убивающий всю цепочку взрыв. Собственные изменения состояния вызываемого откатываются, управление возвращается вызывающему, и тот решает: пробросить ошибку выше или обработать её. Это в точности тот механизм, что стоит за обработкой ошибок, которую вы пишете каждый день.