TL; DR: атаки повторного входа могут быть полностью предотвращены с помощью конечных отправок. Возможные отправления (подумайте об обещаниях JavaScript - обещания на самом деле исходят из конечных отправлений!) позволяют асинхронно вызывать функцию и получать обещание, даже если функция находится на другом компьютере, в другом блокчейне или другом сегменте, что делает шардинг и межсетевое взаимодействие по контракту намного проще.

15 января группа ключевых заинтересованных сторон решила остановить обновление Ethereum« Константинополь ». Остался всего день до того, как Константинополь должен был вступить в силу, но Chain Security выпустила сообщение в блоге, в котором указывалось, что новые сниженные цены на газ обойдут некоторые ранее надежные средства защиты от атак повторного входа. Сообщество Ethereum работало быстро и прозрачно, чтобы отложить обновление, чтобы можно было провести дополнительное расследование.

Мы хотели воспользоваться этой возможностью, чтобы привлечь внимание к классу проблем, частью которого являются атаки с повторным входом, и тому, как определенные схемы могут устранить весь класс проблем в целом.

Перемежающиеся опасности

Атаки повторного входа в Ethereum - это лишь часть большого класса проблем, называемых опасностями чередования. Мы могли бы подумать, что, поскольку Ethereum работает последовательно, он не может иметь чередующихся опасностей. Но что удивительно, даже полностью последовательные программы могут иметь чередующиеся опасности.

Вот пример [1], который является полностью синхронным и последовательным, но имеет большую опасность чередования. В этом примере у нас есть банковский счет, на который мы можем вносить и снимать средства:

Каждый раз, когда мы делаем что-то, что изменяет баланс, мы хотим обновить состояние с помощью нашего нового баланса и уведомить наших слушателей. Мы делаем это с помощью stateHolder:

Допустим, у нас есть два слушателя. Один из них - это финансовое приложение, которое вносит депозит на наш счет, если наш баланс падает ниже определенного уровня:

Другой слушатель просто отображает баланс нашего аккаунта на нашей веб-странице панели инструментов (мы смоделируем это с помощью console.log 😃):

Здесь не о чем беспокоиться, правда? Посмотрим, что произойдет, когда мы его выполним. Добавляем слушателей и снимаем со счета 100 долларов:

Наш банковский счет начинается с баланса в 4000 долларов. При снятии 100 долларов баланс обновляется до 3900 долларов, и мы уведомляем наших слушателей о новом балансе. В ответ на новости financialListener вносит 1000 долларов, а баланс составляет 4900 долларов. Но наш сайт показывает баланс в 3900 долларов, неправильный баланс! 😱

Почему это происходит? Вот последовательность событий:

  1. financeListener получает уведомление о том, что баланс составляет 3900 долларов, и вносит 1000 долларов в ответ.
  2. Депозит вызывает изменение состояния и снова запускает процесс уведомления. Обратите внимание, что webpageListener все еще ожидает уведомления о первом изменении баланса с 4000 до 3900 долларов.
  3. financeListener получает уведомление о том, что баланс составляет 4900 долларов, и ничего не делает, потому что баланс превышает 4000 долларов.
  4. webpageListener получает уведомление о том, что баланс составляет 4900 долларов, и отображает 4900 долларов.
  5. webpageListener наконец получает уведомление о том, что баланс составляет 3900 долларов, и обновляет веб-страницу, отображая 3900 долларов - неправильный баланс.

Мы только что показали, что даже полностью синхронные программы - программы, не имеющие ничего общего со смарт-контрактами или криптовалютами, - все еще могут иметь серьезные опасности чередования.

Как мы можем устранить опасность перемежения?

Некоторые люди предложили решения для перемежения опасностей, но многие из предложенных решений имеют следующие недостатки:

  1. Решение не является надежным (решение не работает, если условия немного меняются)
  2. Решение не устраняет всех опасностей, связанных с чередованием
  3. Решение существенно ограничивает функциональность

Давайте посмотрим, что люди предлагают для Ethereum.

Ограниченность ресурсов как защита от чередующихся опасностей

В Рекомендациях по безопасности смарт-контрактов в твердой среде Consensys говорится следующее:

someAddress.send () и someAddress.transfer () считаются безопасными для повторного входа. Хотя эти методы по-прежнему запускают выполнение кода, вызываемому контракту предоставляется только стипендия в размере 2300 единиц газа, которой в настоящее время достаточно только для регистрации события ... Использование send () или transfer () предотвратит повторный вход, но делает это за счет несовместимости с любым контрактом, резервная функция которого требует более 2300 единиц газа.

Как мы видели в обновлении Константинополя, эта защита не работает, если количество газа, необходимое для изменения состояния, составляет менее 2300 единиц газа. Со временем мы ожидаем, что требуемый газ изменится, как это произошло с обновлением Константинополя, так что это не надежный подход (недостаток № 1).

Вызов внешних функций в последнюю очередь после любых изменений переменных состояния в вашем контракте.

Документация Solidity рекомендует следующее:

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

Однако в приведенном выше примере все вызовы функций внешнего прослушивателя при снятии и депозите происходят после изменения состояния. Тем не менее, по-прежнему существует опасность чередования (недостаток №2). Кроме того, мы можем захотеть вызвать несколько внешних функций, которые в этом случае будут уязвимы друг для друга, что сделает рассуждения об уязвимостях огромным беспорядком.

Не называйте другие контракты

Эмин Гюн Сирер предлагает:

не выполняйте внешние вызовы в контрактах. Если вы это сделаете, убедитесь, что это последнее, что вы делаете. Если это невозможно, используйте мьютексы для защиты от повторных вызовов. И используйте мьютексы во всех своих функциях, а не только в тех, которые выполняют внешний вызов.

Очевидно, что это серьезное ограничение функциональности (недостаток 3). Если мы не можем вызывать другие контракты, у нас фактически не будет возможности компоновки. Кроме того, мьютексы могут привести к тупиковой ситуации, и сами по себе их нелегко компоновать.

Трудно избежать программирования слишком сложных монолитов, если ни одна из ваших программ не может взаимодействовать друг с другом.

- «Искусство программирования Unix»

Что мы подразумеваем под компонуемостью и почему мы этого хотим?

StackOverflow дает нам отличное объяснение возможности компоновки:

«Простым примером возможности компоновки является командная строка Linux, где вертикальная черта позволяет комбинировать простые команды (ls, grep, cat, другие и т. Д.) Практически неограниченным количеством способов, тем самым« составляя »большое количество сложных поведение из небольшого числа более простых примитивов.

У возможности компоновки есть несколько преимуществ:

  1. Более единообразное поведение: например, имея единственную команду, которая реализует «показывать результаты по одной странице за раз» (more), вы получаете степень единообразия подкачки страниц, которая была бы невозможна, если бы каждая команда реализовывала свои собственные механизмы (и флаги командной строки) для разбиения на страницы.
  2. Меньше повторяющихся реализаций (DRY): вместо бесчисленных различных реализаций разбиения по страницам, везде используется только одна.
  3. Больше функциональности при заданном объеме усилий по реализации: существующие примитивы могут быть объединены для решения гораздо более широкого круга задач, чем это было бы, если бы те же усилия были вложены в реализацию монолитных, несоставных команд ».

Компоновка дает огромные преимущества, но мы еще не видели платформу смарт-контрактов, которая могла бы легко составлять контракты без чередования опасностей. Это нужно изменить.

Что представляет собой составное решение?

Мы можем решить проблемы, связанные с перемежением, используя концепцию, называемую возможная отправка. Eventual-send позволяет вам вызывать функцию асинхронно, даже если она находится на другом компьютере, в другом блокчейне или другом сегменте. По сути, конечная отправка - это асинхронное сообщение, которое немедленно возвращает объект (обещание), представляющий будущий результат. Как указывалось в обзоре безопасности Ethereum, проведенном в 2015 году (до атаки DAO), Ethereum чрезвычайно уязвим для атак с повторным входом, и если Ethereum переключится на возможную отправку, они полностью устранят опасность повторного входа .

Вы могли заметить, что обещания в JavaScript имеют много общего с конечными отправками. Это не совпадение - обещания в JavaScript являются прямыми потомками конечных отправлений и взяты из работы Дина Триббла и Марка С. Миллера из Agoric. (Есть отличное видео о происхождении обещаний, которое объясняет больше).

Давайте воспользуемся обещаниями JavaScript, чтобы предотвратить опасность чередования в нашем примере. Что мы хотим сделать, так это превратить любые немедленные вызовы между объектом bankAccount и нашими слушателями в асинхронные вызовы. Теперь наш stateHolder будет уведомлять слушателей асинхронно:

И мы делаем то же самое с вызовом депозита в нашем financialListener:

В нашей новой версии, которая включает обещания, наш дисплей обновляется правильно, и мы предотвратили опасность чередования!

Есть одно важное различие между обещаниями JavaScript и конечными отправками: конечные отправления, в отличие от обещаний JavaScript, могут использоваться с удаленными объектами. Например, с помощью event-send мы можем прочитать файл на удаленной машине («~.» - синтаксический сахар) [2]:

Шардинг

В дополнение к устранению атак повторного входа, таких как атака DAO, возможная отправка позволяет вам составлять контракты по шардам и даже по блокчейнам, потому что ваша модель выполнения уже является асинхронной. Если мы собираемся масштабироваться и взаимодействовать, будущее блокчейна должно быть асинхронным.

Ограничения и компромиссы

Есть несколько компромиссов при выборе возможных посылок. Например, отладка в асинхронной среде обычно сложнее, но работа уже проделана, чтобы позволить разработчикам просматривать причинный граф событий в асинхронной среде.

Еще одно ограничение заключается в том, что асинхронные сообщения кажутся менее эффективными. Как указал Виталик Бутерин, для взаимодействия с другим контрактом может потребоваться несколько раундов обмена сообщениями. Однако конечная отправка упрощает задачу, разрешая конвейерную обработку обещаний [3]. Возможная отправка дает вам обещание, которое разрешится в будущем, и вы можете выполнить конечную отправку для этого обещания, таким образом составляя функции и отправляя сообщения, не дожидаясь ответа.

Заключение

Интеллектуальные контракты Agoric используют эвентуальные посылки, которые устраняют весь класс чередующихся опасностей. По сравнению с другими предлагаемыми решениями, конечные посылки более надежны, более компонуемы и расширять функциональность, включая даже возможность обмена данными между шардами и блокчейнами.

Таким образом, платформы смарт-контрактов * могут * предотвращать уязвимости повторного входа. Вместо того, чтобы полагаться на хрупкие механизмы, такие как ограничения на газ, нам нужно отказаться от синхронной связи между смарт-контрактами и использовать конечные посылки.

Сноски

[1] Этот пример взят из главы 13 тезиса Марка С. Миллера Надежная композиция: на пути к единому подходу к контролю доступа и контролю параллелизма и был переписан на JavaScript.

[2] Обещания JavaScript в этом примере эквивалентны только конечной отправке для локальных объектов и для обещаний локальным объектам. Для удаленных объектов требуется другой API. Например, библиотеки Kris Kowal Q и Q-connection позволяют:

Promise.resolve (слушатель) .invoke (‘stateChanged’, newState);

который мы можем написать, используя синтаксический сахар для возможных посылок:

слушатель ~ .stateChanged (newState);

[3] Миллер М.С., Триббл Э.Д., Шапиро Дж. (2005) Параллелизм среди незнакомцев. В: Де Никола Р., Санджорджи Д. (ред.) Надежные глобальные вычисления. TGC 2005. Lecture Notes in Computer Science, vol 3705. Springer, Berlin, Heidelberg