EIP-1153: черновик, который живёт ровно одну транзакцию
Контрактам часто нужно передавать данные между вызовами внутри одной транзакции — блокировки от реентрантности, временные апрувы, флэш-бухгалтерия. Единственным общим изменяемым хранилищем было постоянное storage: ≈20 000 газа за слот, записанное на диск навсегда, хотя значение выбрасывается по окончании транзакции. EIP-1153 добавляет временное хранилище (transient storage) — та же идея, но исчезает в конце транзакции.
- storage против memory
- модель вызовов EVM
EVM даёт контракту два места для хранения данных, и ни одно не подходит для распространённой нужды. Memory (память) дешёвая, но стирается в конце каждого вызова — она не может пронести что-либо через подвызов. Storage (хранилище) переживает вызовы (и остаётся навсегда после), но это самый дорогой ресурс в EVM, потому что записывается на диск. При этом контрактам постоянно нужна третья вещь: место, куда можно спрятать данные, которые переживают вызовы, но только в рамках текущей транзакции, — блокировка от реентрантности, временный апрув, флэш-бухгалтерия в AMM. Имея под рукой только storage, они платят цену диска за стикер-напоминание. EIP-1153 добавляет ровно недостающий инструмент. Давайте его выведем.
Проблема: storage — единственное общее изменяемое хранилище
Возьмите классическую защиту от реентрантности (reentrancy guard): установите флаг locked перед внешним вызовом, снимите его после и отказывайте в повторном входе, пока он установлен. Флаг должен быть виден реентрантному вызову, поэтому он не может жить в memory — он обязан лежать в постоянное хранилище (persistent storage) Постоянное хранилище контракта вида ключ→значение, читаемое/записываемое через SLOAD/SSTORE. Оно фиксируется в state trie и живёт на диске каждого узла навсегда, поэтому запись в него — самая дорогая распространённая операция EVM. . Поэтому вы делаете SSTORE флага (≈20 000 газа, чтобы установить из нуля, ≈5 000 — чтобы обновить), выполняете работу, затем SSTORE обратно в ноль — при каждом отдельном вызове. Вы пишете на диск каждого узла, навсегда, чтобы удержать значение, чей весь полезный срок жизни — несколько микросекунд внутри одной транзакции.
Хуже того: половина с «сбросом в ноль» опирается на возвраты газа (gas refunds), чтобы отыграть часть стоимости, но EIP-3529 резко урезал и ограничил эти возвраты (ими злоупотребляли как газ-токенами). Так что паттерн дорогой, косвенный и навсегда раздувает состояние значениями, которые больше никому никогда не понадобятся.
→ Шаг 2: дать контрактам черновик.
Добавляем второе хранилище, которое живёт одну транзакцию
Исправление — перестать перегружать storage и добавить параллельное хранилище с другим временем жизни. временное хранилище (transient storage) Второе хранилище на контракт вида ключ→слово, введённое EIP-1153. Оно ведёт себя как storage внутри транзакции, но отбрасывается (сбрасывается в ноль) по окончании транзакции и никогда не записывается в state trie или на диск. — это привязанная к контракту карта slot → 32-байтовое слово, точно как storage, — за исключением того, что она никогда не записывается в state trie или на диск, и она обнуляется целиком по окончании транзакции. Поскольку ничего не сохраняется постоянно, не нужно платить за запись на диск, и нет постоянного следа: это черновик, который протокол выдаёт каждому контракту на время одной транзакции, а затем уничтожает.
Именно это единственное изменение — не сохранять постоянно — делает его дешёвым. Стоимость SSTORE определяется в первую очередь диском и вызванным им ростом состояния; уберите их — и операция становится плоской записью по цене «тёплого» доступа.
→ Шаг 3: два опкода, общих для всего стека вызовов.
Два опкода — и они общие для всех вызовов
Transient storage получает два новых опкода, зеркальных паре storage: TSTORE / TLOAD TSTORE(key, value) записывает слово во временное хранилище; TLOAD(key) читает его. Оба имеют фиксированную стоимость (100 газа, как «тёплый» доступ к storage) без механизма возвратов и оперируют собственным временным пространством имён вызывающего контракта. — TSTORE(key, value) и TLOAD(key). Каждый стоит плоские 100 газа (цена «тёплого» доступа), без учёта возвратов, о котором нужно думать. И, как и постоянное storage, временное хранилище сохраняется через фреймы вызовов транзакции: если вызывающий делает TSTORE(k, v), а затем совершает подвызов, TLOAD(k) у вызываемого прочитает обратно v. Эта видимость между вызовами — вся суть: именно она позволяет реентрантному внутреннему вызову увидеть блокировку, установленную внешним фреймом, или роутеру спрятать контекст, который прочитает колбэк.
- 1 записать слово во временное хранилище
- 2 прочитать его обратно — между вызовами, в рамках одной транзакции
- 3 плоская «тёплая» цена; без возвратов, и диск никогда не затрагивается
→ Шаг 4: заставляем его следовать правилам revert и конца транзакции.
Он откатывается при revert и сам очищается по окончании транзакции
Два правила очистки делают это безопасным. Во-первых, временное хранилище подчиняется revert точно так же, как постоянное storage: если фрейм вызова реверится, каждый сделанный им TSTORE откатывается, как будто его никогда не было. (Это правило добавили в ходе стандартизации именно для того, чтобы защиты от реентрантности оставались корректными — блокировка, установленная во фрейме, который затем реверится, не должна оставаться застрявшей включённой.) Во-вторых, когда транзакция заканчивается, всё временное хранилище автоматически сбрасывается в ноль. Так что защите от реентрантности больше вообще не нужен завершающий сброс SSTORE(slot, 0) — очистка бесплатна и автоматична.