EIP-2718: конверт, позволивший Ethereum добавлять типы транзакций
У Ethereum был ровно один формат транзакции — RLP-список. Каждая новая функция (списки доступа, рынок комиссий 1559, блобы, set-code) требовала новой формы, и декодерам приходилось угадывать, какую из них они видят. EIP-2718 добавляет в начало каждой транзакции один байт типа, превращая один жёсткий формат в расширяемое, версионированное семейство.
- RLP-кодирование
- что такое транзакция
На протяжении многих лет у Ethereum был ровно один вид транзакции: единый RLP Recursive Length Prefix — каноническая сериализация Ethereum. Она кодирует вложенные списки и байтовые строки; транзакция была RLP-кодировкой списка из девяти полей [nonce, gasPrice, gasLimit, to, value, data, v, r, s]. -кодированный список из девяти полей. Это было нормально, пока люди не захотели изменить то, чем может быть транзакция — добавить списки доступа, заменить gasPrice на пару базовая-комиссия/чаевые из 1559, нести обязательства по блобам, прикреплять авторизацию set-code. Каждая новая функция требует новой формы на проводе, и в момент, когда форм становится больше одной, каждый декодер сталкивается с вопросом, на который он никогда не был рассчитан отвечать: какой это вид транзакции? EIP-2718 — это небольшое, непритязательное исправление, сделавшее возможными все знаменитые EIP после него. Давайте выведем его.
Проблема: одна форма, которую все обязаны угадывать
Исходная транзакция и есть rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s]) — ничего больше. Нет ни поля версии, ни тега, ни метки. Чтобы добавить функцию, нужно менять этот список: добавить accessList, подставить новые поля комиссии. Но теперь по сети летают два разных вида транзакций, и единственный способ их различить — принюхаться к RLP: посчитать поля, изучить структуру и угадать. Догадки хрупки: два формата могут выглядеть тревожно похоже, а каждому кошельку, узлу и эксплореру нужна одна и та же хрупкая эвристика, которую придётся поддерживать в синхроне вечно.
Нет чистого способа сказать «это новый вид транзакции, декодируй его по этим правилам». У формата нет места объявить о себе.
→ Шаг 2: приклеим ярлык на конверт.
Оборачиваем в конверт: один байт типа, затем полезная нагрузка
Хватит пытаться закодировать формат внутри данных. Вместо этого приклеим ярлык снаружи. Определим типизированная транзакция Конверт EIP-2718: транзакция, закодированная как один байт типа, за которым следует непрозрачная, специфичная для типа полезная нагрузка — TransactionType ‖ TransactionPayload. Байт типа называет формат; полезную нагрузку умеют читать только правила этого формата. ровно как две части, соединённые вместе: TransactionType ‖ TransactionPayload. Первый байт — небольшое число, называющее формат; всё после него — непрозрачная полезная нагрузка, которую умеют читать только правила именно этого типа. Декодер читает один байт, находит подходящий набор правил и декодирует остальное с полной уверенностью — никакого принюхивания, никаких догадок.
Это просто версионированный конверт. Байт типа — селектор версии/формата; смысл полезной нагрузки определяется для каждого типа отдельно. Добавление нового формата больше не означает изменение существующей формы — оно означает занять новый номер и описать, как выглядит его полезная нагрузка.
→ Шаг 3: сделаем так, чтобы два вида нельзя было перепутать.
Обратная совместимость бесплатно: выбираем байты, которых RLP не производит
Вот в чём изящество. Legacy-транзакция — это RLP-список, а RLP кодирует список с первым байтом в диапазоне 0xc0–0xff. Типизированная транзакция определена так, чтобы использовать байт типа в диапазоне 0x00–0x7f. Эти диапазоны не могут пересекаться — RLP-список никогда не начинается ниже 0xc0, а байт типа никогда его не достигает. Так что декодеру достаточно взглянуть на самый первый байт: если он ≥ 0xc0 — это legacy RLP-транзакция; если ≤ 0x7f — типизированная, и значение байта говорит, какого типа. Одно сравнение, ноль неоднозначности, оба вида сосуществуют вечно.
Значения 0xc0 и выше зарезервированы именно для того, чтобы legacy-транзакции оставались действительными, а 0xff оставлен в стороне (зарезервирован). Дизайн не добавил метку к старым транзакциям — он выбрал диапазон новой метки так, чтобы старая кодировка уже сама себя различала.
- 1 один байт, 0x00–0x7f — называет формат
- 2 непрозрачные байты, которые умеет декодировать только этот тип
→ Шаг 4: смотрим, как растёт семейство.
Выигрыш: каждый последующий EIP просто занимает байт
Как только конверт существует, добавление формата транзакции становится рутиной. EIP-2930 занял 0x01 для списков доступа; EIP-1559 занял 0x02 для рынка базовой комиссии/чаевых; EIP-4844 занял 0x03 для транзакций с блобами; EIP-7702 занял 0x04 для авторизаций set-code. Каждый — просто число плюс описание полезной нагрузки. И, что важно, старый клиент, не распознающий байт типа, аккуратно отклоняет транзакцию вместо того, чтобы неверно её прочитать — неизвестное значит неизвестное, а не «угадай».