Часть III · Глава 19 из 43 · Berlin

EIP-2718: конверт, позволивший Ethereum добавлять типы транзакций

У Ethereum был ровно один формат транзакции — RLP-список. Каждая новая функция (списки доступа, рынок комиссий 1559, блобы, set-code) требовала новой формы, и декодерам приходилось угадывать, какую из них они видят. EIP-2718 добавляет в начало каждой транзакции один байт типа, превращая один жёсткий формат в расширяемое, версионированное семейство.

Обновлено 23 июн. 2026 г. · 11 мин
Предполагается
  • RLP-кодирование
  • что такое транзакция

На протяжении многих лет у Ethereum был ровно один вид транзакции: единый RLP Recursive Length Prefix — каноническая сериализация Ethereum. Она кодирует вложенные списки и байтовые строки; транзакция была RLP-кодировкой списка из девяти полей [nonce, gasPrice, gasLimit, to, value, data, v, r, s]. -кодированный список из девяти полей. Это было нормально, пока люди не захотели изменить то, чем может быть транзакция — добавить списки доступа, заменить gasPrice на пару базовая-комиссия/чаевые из 1559, нести обязательства по блобам, прикреплять авторизацию set-code. Каждая новая функция требует новой формы на проводе, и в момент, когда форм становится больше одной, каждый декодер сталкивается с вопросом, на который он никогда не был рассчитан отвечать: какой это вид транзакции? EIP-2718 — это небольшое, непритязательное исправление, сделавшее возможными все знаменитые EIP после него. Давайте выведем его.

1 шаг Один формат, много желаний

Проблема: одна форма, которую все обязаны угадывать

Исходная транзакция и есть rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s]) — ничего больше. Нет ни поля версии, ни тега, ни метки. Чтобы добавить функцию, нужно менять этот список: добавить accessList, подставить новые поля комиссии. Но теперь по сети летают два разных вида транзакций, и единственный способ их различить — принюхаться к RLP: посчитать поля, изучить структуру и угадать. Догадки хрупки: два формата могут выглядеть тревожно похоже, а каждому кошельку, узлу и эксплореру нужна одна и та же хрупкая эвристика, которую придётся поддерживать в синхроне вечно.

одна форма для всех транзакций nonce gasPrice gas to value data v r s + список доступа? + новые поля комиссии? декодер: какой тип? угадать добавь функцию — декодеры вынуждены гадать тип по байтам
Одна форма RLP для каждой транзакции, без тега версии. Добавьте функцию — и байты изменятся, так что каждому декодеру приходится вынюхивать структуру и угадывать, какой вид у него в руках — хрупко, и с каждым новым форматом становится хуже.

Нет чистого способа сказать «это новый вид транзакции, декодируй его по этим правилам». У формата нет места объявить о себе.

→ Шаг 2: приклеим ярлык на конверт.

2 шаг Добавляем байт типа

Оборачиваем в конверт: один байт типа, затем полезная нагрузка

Хватит пытаться закодировать формат внутри данных. Вместо этого приклеим ярлык снаружи. Определим типизированная транзакция Конверт EIP-2718: транзакция, закодированная как один байт типа, за которым следует непрозрачная, специфичная для типа полезная нагрузка — TransactionType ‖ TransactionPayload. Байт типа называет формат; полезную нагрузку умеют читать только правила этого формата. ровно как две части, соединённые вместе: TransactionType ‖ TransactionPayload. Первый байт — небольшое число, называющее формат; всё после него — непрозрачная полезная нагрузка, которую умеют читать только правила именно этого типа. Декодер читает один байт, находит подходящий набор правил и декодирует остальное с полной уверенностью — никакого принюхивания, никаких догадок.

байт type payload 0x02 ++ байты, специфичные для формата декодировать как EIP-1559 tx типизированная tx = байт типа ++ payload; байт говорит, как читать остальное
Типизированная транзакция — это байт типа, за которым следует специфичная для типа полезная нагрузка. Ведущий байт сообщает каждому декодеру, какой именно набор правил применить — прочитайте один байт, затем декодируйте остальное с уверенностью.

Это просто версионированный конверт. Байт типа — селектор версии/формата; смысл полезной нагрузки определяется для каждого типа отдельно. Добавление нового формата больше не означает изменение существующей формы — оно означает занять новый номер и описать, как выглядит его полезная нагрузка.

→ Шаг 3: сделаем так, чтобы два вида нельзя было перепутать.

3 шаг Никогда не сталкиваться с legacy

Обратная совместимость бесплатно: выбираем байты, которых RLP не производит

Вот в чём изящество. Legacy-транзакция — это RLP-список, а RLP кодирует список с первым байтом в диапазоне 0xc00xff. Типизированная транзакция определена так, чтобы использовать байт типа в диапазоне 0x000x7f. Эти диапазоны не могут пересекаться — RLP-список никогда не начинается ниже 0xc0, а байт типа никогда его не достигает. Так что декодеру достаточно взглянуть на самый первый байт: если он ≥ 0xc0 — это legacy RLP-транзакция; если ≤ 0x7f — типизированная, и значение байта говорит, какого типа. Одно сравнение, ноль неоднозначности, оба вида сосуществуют вечно.

первый байт решает, какой декодер запускать первый байт типизированная · байт type пропуск · не используется legacy · RLP 0x000x7f0xc00xff RLP-список всегда начинается ≥ 0xc0; байты типа ≤ 0x7f пустой промежуток 0x80–0xbf — они никогда не столкнутся
Одного первого байта достаточно для различения. Legacy RLP-список всегда начинается с ≥ 0xc0; типизированным транзакциям выделен диапазон 0x00–0x7f, который RLP никогда не может произвести. Поэтому старые и новые транзакции сосуществуют без столкновений и без принюхивания.

Значения 0xc0 и выше зарезервированы именно для того, чтобы legacy-транзакции оставались действительными, а 0xff оставлен в стороне (зарезервирован). Дизайн не добавил метку к старым транзакциям — он выбрал диапазон новой метки так, чтобы старая кодировка уже сама себя различала.

Формула
typed_tx = TransactionType 1 TransactionPayload 2
  1. 1 один байт, 0x00–0x7f — называет формат
  2. 2 непрозрачные байты, которые умеет декодировать только этот тип
Один ведущий байт превращает «формат транзакции» в расширяемое, версионированное семейство.

→ Шаг 4: смотрим, как растёт семейство.

4 шаг Версионированное семейство

Выигрыш: каждый последующий EIP просто занимает байт

Как только конверт существует, добавление формата транзакции становится рутиной. EIP-2930 занял 0x01 для списков доступа; EIP-1559 занял 0x02 для рынка базовой комиссии/чаевых; EIP-4844 занял 0x03 для транзакций с блобами; EIP-7702 занял 0x04 для авторизаций set-code. Каждый — просто число плюс описание полезной нагрузки. И, что важно, старый клиент, не распознающий байт типа, аккуратно отклоняет транзакцию вместо того, чтобы неверно её прочитать — неизвестное значит неизвестное, а не «угадай».

версионированное семейство; каждая фича занимает байт 0x00 legacy (pre-2718) 0x01 access lists · EIP-2930 0x02 base fee + tip · EIP-1559 0x03 blobs · EIP-4844 0x04 set EOA code · EIP-7702 0x05 the next type slots in старые клиенты чисто отвергают неизвестный байт типа
С версионированным конвертом каждая новая функция просто занимает следующий байт типа и описывает свою полезную нагрузку — 0x01 списки доступа, 0x02 рынок комиссий 1559, 0x03 блобы, 0x04 set-code. Старые клиенты аккуратно отклоняют неизвестный тип вместо того, чтобы неверно его декодировать.
04 Глубже Куда двигаться дальше