Почему подпись одной операции нельзя использовать в другой сети
Цифровая подпись подтверждает, что транзакцию создал владелец закрытого ключа, а ее параметры не менялись. Но в ранней истории сети эфириума подпись не указывала, для какой именно цепочки подготовлена операция. Это стало проблемой, когда у одного сценария стали появляться несколько возможных продолжений.
Как возникла уязвимость
20 июля 2016 года эфириум провел хардфорк на блоке 1 920 000 после взлома The DAO. Около 12 млн ETH из связанных с проектом контрактов перевели в контракт возврата средств. Примерно 85% майнеров поддержали новую цепочку, остальные могли продолжить работу по прежним правилам.
Так появилась Ethereum Classic. Все, кто владел ETH до спорного блока, получили доступ к монетам в обеих цепочках: ETH в Ethereum и ETC в Ethereum Classic. Один закрытый ключ управлял одинаковым адресом в двух сетях.
Транзакции тоже оставались совместимыми. Если владелец переводил ETC, наблюдатель мог скопировать подписанную операцию и отправить ее в Ethereum. При достаточном балансе и совпадающем порядковом номере со счета уходило такое же количество ETH. Кража ключа для этого не требовалась.
Ethereum Foundation рекомендовала сначала разделить монеты специальным контрактом: ETH и ETC переводились на разные адреса, после чего операции в одной сети уже не подходили для другой. До разделения пользователям советовали не управлять двумя остатками из одного кошелька.
Почему не спасал nonce
Nonce — порядковый номер транзакции адреса. Первая операция получает номер 0, следующая — 1. Узлы не исполняют две транзакции одного отправителя с одинаковым nonce, поэтому уже проведенный перевод нельзя еще раз включить в ту же цепочку.
После хардфорка счетчики существовали независимо. Если следующим номером адреса в обеих сетях оставался 12, транзакция с nonce 12 могла пройти дважды. Конкретная подпись переставала подходить лишь после расхождения счетчиков или нехватки средств в одной из цепочек.
Что изменил EIP-155
EIP-155 (Ethereum Improvement Proposal — предложение по улучшению эфириума) был опубликован 14 октября 2016 года и включен в обновление Spurious Dragon на блоке 2 675 000. Стандарт добавил Chain ID (идентификатор сети) в данные, по которым кошелек рассчитывает подпись.
Для основной сети Ethereum используется Chain ID 1. Ethereum Classic закрепила за собой значение 61. Поэтому одинаковая по остальным параметрам операция создает в этих сетях разные подписи. Узлы второй цепочки отклонят транзакцию, подготовленную для первой.
В старом формате к шести полям транзакции перед подписью добавлялись Chain ID и два нулевых значения. Идентификатор также отражался в параметре подписи v: для Ethereum вместо прежних 27 или 28 получались 37 или 38.
EIP-155 сохранил обратную совместимость. Транзакции старого формата без Chain ID продолжили приниматься, а значит, оставались уязвимыми для повтора. Защита работала только тогда, когда кошелек или другая программа подписывали операцию по новым правилам.
Как идентификатор используется сейчас
Кошелек запрашивает Chain ID у узла через метод eth_chainId. Ответ приходит в шестнадцатеричной записи: Ethereum возвращает 0x1, Ethereum Classic — 0x3d, то есть 61. Так приложение определяет, к какой цепочке оно подключено.
В современных транзакциях идентификатор записывается отдельным полем. Формат типа 1, введенный EIP-2930, и формат типа 2 с комиссиями EIP-1559 включают chain_id непосредственно в подписываемые данные.
Chain ID применяется и в подписях для контрактов. Например, ERC-2612 позволяет разрешить расходование токенов без отдельной транзакции «approve». В рекомендуемый разделитель домена входят идентификатор сети и адрес проверяющего контракта. Разрешение для одного контракта поэтому нельзя автоматически перенести в другую сеть.
Где защита заканчивается
Уникальность Chain ID обеспечивается не алгоритмом, а выбором разработчиков. Создатели новой сети должны сами подобрать незанятое значение. Если две совместимые цепочки используют один идентификатор и одинаковый формат транзакций, подписи снова могут оказаться действительными в обеих.
Chain ID также не исправляет ошибку выбора сети. Перевод токенов в Base вместо Ethereum будет подписан и выполнен в Base. Идентификатор защищает от копирования готовой операции в другую цепочку, но не проверяет получателя, надежность сайта или смысл вызова контракта.