Флэшботы: короли мемпула
Сегодня мы разберемся в том, кто такие флэшботы и с чем их едят!
Перевод данной статьи - ТЫК
Начнем
Итак, вы слышали о Mempool, Flashbots, Bundles & Searcher Auctions, но действительно ли вы понимаете, что это такое и как они работают?
Эта статья призвана обеспечить глубокое понимания Mempool через призму Flashbots auctions
Вы отправляетесь в путешествие по MEV Geth, пользовательскому клиенту Ethereum, который позволил Flashbots изменять форму и структуру Mempool
MEV Geth вместе с ретранслятором Flashbots предоставили шлюз между searchers и майнерами, навсегда изменив игру MEV
Mempool
Мемпул (memory pool, пул памяти) — это небольшая база данных неподтвержденных или ожидающих выполнения транзакций (pending), которую хранит каждая нода. Когда транзакция подтверждается включением в блок, она удаляется из мемпула
Определение — отличное место для начала. Давайте рассмотрим еще некоторые основы
- Нода, т.е. Geth (или MEV Geth), хранит список неподтвержденных или ожидающих выполнения транзакций
- Эти транзакции передаются нодам через их пиры (одноранговые сети) или принимаются напрямую через их конечную RPC
- Каждая нода имеет свой собственный мемпул, глобального мемпула не существует
- Это означает, что ноды имеют разные мемпулы в зависимости от того, где они находятся географически и от пиров, к которым они подключены
- Ноды ограничивают количество транзакций, которые они хранят в своем мемпуле, таким образом они гарантируют, что они не будут перегружены
Мемпул представлен в ноде Geth через структуру TxPool, которую можно увидеть ниже
На картинке были выделены два элемента. Первый элемент «locals» представляет собой набор адресов. Если транзакция в TxPool (Mempool) происходит с одного из этих адресов, она будет считаться локальной транзакцией (в следующем разделе вы поймете, почему это важно)
Второй выделенный элемент — «pending», который представляет все обрабатываемые в настоящее время транзакции в TxPool
Вы также можете заметить поле «queue» в строке 242, оно представляет транзакции, которые находятся в TxPool, но не могут быть обработаны. Примером может быть Tx с nonce, который не является последовательным, что указывает на наличие другой транзакции с того же адреса, которая должна быть обработана первой
Когда вы думаете о MEV, вам нужно задать следующий вопрос: как эти транзакции попадают из TxPool в блок?
Ниже приведен код Geth, который выполняет эту функцию. Вы увидите, что транзакции на самом деле разделены на localTxs и remoteTxs, упорядоченные по их gas price/nonce, и что localTxs имеют приоритет над remoteTxs
- 1) Функция «commitNewWork» делает много вещей, включая заполнение блока транзакциями из TxPool
- 2) Этот раздел получает транзакции из TxPool и разделяет их на local и remote транзакции
- В строке 954 вы можете увидеть комментарий «Заполнить блок всеми доступными pending транзакциями». После этой строки мы устанавливаем переменную «pending» через w.eth.TxPool().Pending(true). Это возвращает mapping адреса с транзакцией
- Строки 967–974 показывают, как мы используем переменную «locals», которую мы выделили ранее, для разделения транзакций на local и remote. Мы перебираем local адреса и проверяем, не поступил ли какой-либо из pending Txs с local адреса. Если он совпадает, эти транзакции удаляются из remoteTxs и добавляются в localsTxs. (Locals — это набор адресов, которые нода может определить в своей конфигурации, они имеют приоритет над remote транзакциями и не будут удалены нодой)
- Конечным результатом является то, что pending транзакции разделены на localTxs, которые пришли с «locals» адресов, и remoteTx, которые пришли от всех остальных
- 3) Этот блок представляет сортируемые и зафиксированные в блоке localTx. Поскольку localTxs обрабатываются первыми в коде, они имеют приоритет над remoteTxs
- 4) Транзакции упорядочиваются по gas price и nonce через «NewTransactionsByPriceAndNonce», а затем фиксируются в блоке через «commitTransactions»
- 5) «NewTransactionsByPriceAndNonce» принимает набор Txs, упорядочивает их по gas price и гарантирует, что nonces аккаунта в порядке. Для этого он использует структуру данных кучи, которую мы можем видеть инициализированной в строке 443
- Переменная head — это массив TxByPriceAndTime, который внутри представляет собой просто массив транзакций. Он реализует интерфейс кучи, который позволяет нам использовать вызовы кучи для сортировки наших данных. Он представляет собой «Следующую транзакцию для каждой уникальной учетной записи (куча цен)». Подробнее о кучах ТУТ
- Транзакции упорядочены для каждой учетной записи по gas price, но порядок nonce сохраняется. Это означает, что транзакция с высокой ценой газа с nonce, который не является следующим, может не оказаться в верхней части блока. Приведенная ниже картинка тестового примера Geth демонстрирует это
- Если несколько транзакций имеют одинаковую цену, то та, которая была замечена ранее, имеет приоритет, чтобы избежать сетевых спам-атак, направленных на определенный порядок (timestamp фиксируется тут в коде)
- 6) Мы возвращаем набор транзакций, который будет использоваться в функции «commitTransactions» для фиксации транзакций в блоке
- 7) Точно такой же процесс происходит с remote транзакциями, они сортируются по цене газа и nonce и фиксируются в блоке. Количество совершенных транзакций ограничено gas limit блока. Remote транзакции могут быть удалены на нодах, когда количество Txs в TxPool превышает лимит нод
Это возвращает нас во времена «Priority Gas Auctions (приоритетные газовые аукционы)» (PGA), когда searchers и боты повышали цену на газ в своих транзакциях, чтобы добраться до вершины блока
Боты будут постоянно делать ставки друг против друга в течение ~ 13,5 секунд времени блока, чтобы быть первыми в блоке
Когда вы проиграли аукцион, вам все равно пришлось заплатить цену газа за самую высокую ставку, поскольку ваша транзакция все равно была бы выполнена, но она была бы отменена. Боты могут потерять на этом много денег
При торгах в PGA боты и searchers должны были следовать этим правилам, чтобы увеличить цену газа для транзакции
Теперь, когда мы рассмотрели ванильный Geth, давайте посмотрим, что изменилось с первым официальным выпуском MEV Geth
MEV-Geth v0.1 - 31 Mar 2021
Для тех, кто не знает: MEV Geth был новым пользовательским клиентом, который позволял майнерам получать бандлы MEV
«Бандлы MEV» представляли собой наборы транзакций, которые должны были выполняться атомарно, т.е. должны быть выполнены либо все транзакции в бандле, либо ни одна из них
Флэшботы предоставили ретранслятор, соединяющий Searchers с майнерами. Searchers отправляли бандлы в ретранслятор Flashbots, а ретранслятор пересылал их майнерам. Система полагалась доверие между сторонами для функционирования
MEV Geth и Flashbots Relay стремились вывести PGA из сети, где они влияли на обычных пользователей, и переместить их в ретранслятор
Новые правила аукциона были определены и предоставлены Searchers. Проигрышные ставки не будут выполняться, что сэкономит ботам их деньги, а Ethereum — его блочное пространство
Бандлы также представили возможность платить майнеру через coinbase.transfer(), прямой платеж майнеру, а не через комиссию за газ
Для версии 0.1 можно было выбрать только один бандл для включения в каждый блок
Давайте взглянем на клиентские изменения, которые были внесены для включения этих бандлов MEV
Transaction Ordering
- TxPool Реализация Mempool от Geth, которую мы видели ранее
- В TxPool добавляется новое поле под названием «mevBundles», которое представляет собой массив типа mevBundle. MevBundle состоит из 4 элементов
- «txs» — список транзакций в бандле. Каждая транзакция подписывается и кодируется RLP
- «blockNumber» — точный номер блока, на котором может быть выполнен бандл
- «minTimestamp» — минимальный timestamp блока (включительно), при котором бандл может быть выполнен
- «maxTimestamp» — максимальный timestamp блока (включительно), при котором бандл может быть выполнен
- «CommitNewWork» — это та же функция, которую мы рассмотрели выше, которая обрабатывает заполнение блока всеми pending транзакциями
- Разделы «Local Transactions» и «Remote Transactions» для упорядочения каждой группы транзакций и их фиксации в блоке. Вы заметите, что оба находятся под разделом «Flashbots Bundle», что означает, что транзакции бандла будут иметь приоритет и будут помещены в верхнюю часть блока
- В этом разделе обрабатываются «mevBundles», полученные от Flashbots. Строка 1148 захватывает все бандлы из TxPool. Функция «findMostProfitableBundle» (которую мы рассмотрим в следующем разделе) используется для определения наиболее прибыльного бандла. Информация о бандле регистрируется в ноде, а затем бандл фиксируется в блоке с помощью «commitBundle» в строке 1155
Так как же Flashbots определяют самый прибыльный бандл?
Most Profitable Bundle
Ранее мы говорили: «Новые правила аукциона были определены и предоставлены Searchers»
Ниже приведены новые правила аукциона, они выглядят сложными, но уверяю вас, это не так
- «s» — это «скорректированная цена на газ», которую мы должны максимизировать, чтобы выиграть аукцион
- Delta coinbase — это общая сумма ETH, выплаченная майнеру из транзакций в бандле посредством прямых платежей, т. е. coinbase.transfer().
- Сумма всего газа транзакций в бандле (gas price * gas used), представляет собой общий платеж за газ для бандла
- Разделив эти 2 значения, вы получите цену ETH за единицу газа для бандла (Adjusted Gas Price (скорректированная цена газа))
- Чтобы максимизировать это значение, нам нужно либо:
Хорошо, давайте посмотрим, как этот расчет реализован в MEV Geth. Мы начинаем с того места, где остановились в предыдущем примере, с функции «findMostProfitableBundle»
- 1) Функция «findMostProfitableBundle» перебирает бандлы, чтобы определить «maxBundle» (наиболее прибыльный) и возвращает этот бандл вместе с некоторыми другими данными, которые регистрируются на ноде Geth
- 2) Каждый бандл запускается через функцию «computeBundleGas». Эта функция возвращает «totalEth», который представляет «coinbaseDiff» (насколько увеличился баланс майнера) и «totalGasUsed», который представляет собой газ, потребляемый транзакциями в бандле
- 3) Мы вычисляем использованный газ, перебирая транзакции в бандле и запуская «ApplyTransaction», что позволяет нам смоделировать транзакцию и получить квитанцию о транзакции, которая содержит «GasUsed». Суммируя «GasUsed» для каждой транзакции, мы можем получить «totalGasUsed» для всего бандла. Обновленное состояние используется для следующей транзакции в цикле, чтобы оно отражало то, что происходит в реальном блоке
- 4) В строке 1245 мы объявили переменную «coinbaseBalanceBefore», которая получает баланс адреса coinbase (майнера) до того, как будут смоделированы какие-либо транзакции. Строка 1254 объявляет «coinbaseBalanceAfter», который захватывает баланс coinbase после того, как все транзакции были применены к состоянию. Строка 1255 устанавливает «coinbaseDiff», который вычитает «coinbaseBalanceBefore» из «coinbaseBalanceAfter», чтобы получить прибыль майнера. Значение используется для установки возвращаемого значения totalEth
- 5) Теперь у нас есть «Adjusted GasPrice», у нас есть числитель и знаменатель, нам просто нужно запустить расчет. В строке 1221 объявляется переменная «mevGasPrice», мы делим «totalEth» на «totalGasUsed», что возвращает нашу «Adjusted GasPrice»
- 6) После расчета «Adjusted GasPrice» каждый бандл сравнивается с текущим максимальным значением (строки 1222–1227), что гарантирует возврат бандла с самым высоким «Adjusted GasPrice» («mevGasPrice»)
Это была первая версия MEV Geth, но впереди еще много всего, в течение следующих нескольких месяцев была выпущена версия 0.2. Посмотрим, какие улучшения были сделаны
MEV-Geth v0.2 - 12 Apr 2021 →16 Jun 2021
В v0.2 был внесен ряд улучшений, но мы сосредоточимся на трех основных изменениях. Изменение расчета «Adjusted GasPrice», объединение бандлов и настройка структуры MevBundle в клиенте
Adjusted Gas Price
Изменение в расчете было небольшим. Это включало удаление платежей за газ из Txs, которые были в комплекте, но уже были в TxPool. Они были представлены так:
Цель этого состояла в том, чтобы предотвратить «наполнение бандлов», когда searchers наполняли свои бандлы транзакциями с высоким gwei из TxPool, чтобы завысить свою оценку «Adjusted GasPrice»
- Мы вернемся к «computeBundleGas», чтобы увидеть, что изменилось в расчетах
- Как и прежде, мы пробегаем по Tx в бандле, запуская «ApplyTransaction», чтобы смоделировать Tx и увидеть изменение состояния
- Сначала мы устанавливаем для «txInPendingPool» значение false
- Затем мы получаем все транзакции, ожидающие в TxPool, с адреса бандла Tx
- Получить nonce бандла Tx
- Для каждой транзакции в TxPool, связанной с этим адресом, проверить совпадение nonces
- Если все верно, то они устанавливают для txInPendingPool значение true и выходят из цикла
- Если транзакция не находится в pending пуле, добавьте соответствующие комиссии за газ из этого Tx к сборам за газ для всего бандла. (Это фактически действует как минус плата за газ от Txs, которые присутствовали в TxPool)
- Сравнив расчет скорректированной цены на газ с кодом, мы можем теперь понять, что
Bundle Merging
В версии 0.1 мы могли включать только 1 бандл в блок, а с объединением бандлов в версии 0.2 мы могли объединить 3 бандла, чтобы максимизировать прибыль. Посмотрим, как
- «commitNewWork», который обрабатывает заполнение блока транзакциями
- Как и прежде, у нас есть секция бандла Flashbots, секция localTxs и секция remoteTxs. Раздел «Flashbots Bundle» изменился по сравнению с v0.1, у нас есть новая функция, которая называется generateFlashbotsBundle
- «generateFlashbotsBundle» берет массив бандлов, моделирует их, сортирует, а затем объединяет. Давайте посмотрим поближе
- У нас есть новая структура под названием «simulatedBundle», которая содержит исходный бандл плюс ключевые данные, необходимые для нашего расчета «Adjusted Gas Price», totalGasUsed, totalEth и т. д. Функция SimulatedBundles в строке 1217 берет бандл и моделирует его относительно головы цепочки, чтобы убедиться, что она не возвращается, и вычисляет газ бандла
- Смоделированные бандлы сортируются на основе «mevGasPrice» (Adjusted Gas Price)
- Эти отсортированные бандлы передаются функции «mergeBundles». В начале этой функции объявляется переменная finalBundle, представляющая набор транзакций. Функция «mergeBundles» предназначена для включения в блок более одного бандла
- Этот раздел происходит внутри цикла бандла, каждый бандл снова моделируется с помощью «computeBundleGas», но вместо того, чтобы все моделировались в отношении состояния головы цепочки, они моделируются в отношении состояния цепочки, в котором предыдущий бандл оставил её. Если «computeBundleGas» возвращает ошибку или «mevGasPrice» ниже floorGasPrice (это новый жестко заданный нижний предел, введенный в v0.2), тогда состояние/gasPool будет сброшено до того, что было в предыдущем цикле. «continue» вернет программу к началу цикла со следующим элементом, что означает, что бандл не будет включен
- Если бандл проходит проверку «computeBundleGas», его Txs добавляются в «finalBundle», а некоторые ключи/значения обновляются, например, totalGasUsed, totalEth и т. д. Наконец, счетчик увеличивается, по умолчанию максимум 3 бандла могут быть объединены вместе. Значение «maxMergeBundles» уравновешивает прибыль с производительностью ноды и может быть обновлено в конфигурации
- Этот раздел проверяет счетчик и прерывает цикл for, если счетчик достиг «maxMergeBundles»
- Возвращается ряд переменных, включая «finalBundle», который возвращается в generateFlashbotsBundle, внутри функции «commitNewWork» в строке 1146 устанавливается «bundleTxs», который соответствует «finalBundle». Затем это значение фиксируется в блоке в строке 1155
Reverting Tx Hashes
Еще одно незначительное изменение коснулось структуры MevBundle, определенной в клиентском коде. Было добавлено дополнительное поле RevertingTxHashes, представляющее список хэшей Tx, которым разрешено возвращать статус 0 (revert) при получении транзакций
Идея разрешить отмену транзакций кажется странной. Как searcher, вы должны были смоделировать бандл и убедиться, что он может выполняться в верхней части блока, так зачем нам это нужно
Это было правильное предположение, когда у нас был один бандл, отправленный в версии 0.1, но теперь у нас включено слияние бандлов на ноде
Внезапно вы не можете быть уверены в состоянии цепочки, когда ваш бандл включен. Вы можете пометить некоторые транзакции, например, ликвидацию, как разрешенные к отмене, если до вас был включен другой бандл
Если ваш бандл может быть прибыльным с reverting транзакцией, имеет смысл включить его в список. Альтернативой является то, что бандл отбрасывается нодой, поэтому searcher ничего не теряет
Стратегии, требующие выполнения более одной транзакции, такие как сэндвичи, скорее всего, не будут разрешены, поскольку вы можете оказаться в ситуации, когда одна часть сделки возвращается, а другая нет, что может сделать вас незащищенным
Далее мы собираемся сразу перейти к версии v0.4, в которой были представлены мегабандлы. Для тех, кто заинтересован, v0.3 включает в себя совместимость MEV Geth с EIP-1559, вы можете прочитать об этом ТУТ
MEV-Geth v0.4 - 27 Sep 2021 →10 Jan 2022
Если вы посмотрите на реализацию «mergeBundles» в v0.2, то увидите, что она довольно наивна и ограничивает то, как происходит слияние бандлов
Причина этого в том, что вы не хотите внедрять изменение в ПО (MEV Geth), которое полностью меняет характеристики необходимого оборудования (CPU MEM)
В идеале вы хотите, чтобы слияние бандлов выполнялось для максимального количества бандлов с оптимизированным оборудованием и алгоритмами. Не имеет смысла реализовывать это на каждой ноде
Мегабандлы позволили это сделать, они являются предшественниками бандлов «full block», которые мы увидим в MEV Boost
Одним интересным изменением, которое вводит Megabundles, была возможность ранжировать бандлы по прибыльности, а не по цене газа
Представьте себе 2 бандла, один из которых приносит вам прибыль в 1 ETH с «Adjusted Gas Price» 100, а другой приносит вам 10 ETH со «Adjusted Gas Price» 80. Эти цифры преувеличены, но как майнер вам нужен бандл в 10 эфира. Вы не знаете, когда в следующий раз получите блок, поэтому хотите максимизировать прибыль, когда придет ваша очередь
Посмотрите этот твит Берта Миллера для получения дополнительной информации
Ценностное предложение было простым: мы проведем расчеты и найдем для вас бандл, максимизирующий вашу прибыль
Megabundles
- Вернувшись к структуре TxPool (Mempool), мы видим, что у нас есть новое поле под названием Megabundles, которое представляет собой mapping адресов с «MevBundles». Адрес представляет ретранслятор, из которого поступает MevBundle
- Структура sendMegabundleArgs очень похожа на структуру MevBundle с одним дополнительным полем RelaySignature. Это подпись от ретранслятора для этого конкретного мегабандла
- RecoverRelayAddress берет RelaySignature и определяет адрес, который его сделал
- Авторизованные адреса ретранслятора поддерживаются через MinerTrustedRelaysFlag. Восстановленный адрес из подписи мегабандла будет сопоставлен с адресами доверенных ретрансляторов, чтобы гарантировать, что мегабандлы принимаются только от доверенных ретрансляторов
Почему у Megabundles есть эта дополнительная безопасность, когда нода проверяет, что это от доверенного ретранслятора. Давайте рассмотрим реализацию того, как Megabundle фиксируется в блоке, чтобы понять, почему
Committing a Megabundle
Мегабандлы — это результат объединения нескольких бандлов в Relay для получения максимальной прибыли
Они предназначены для снятия вычислительной нагрузки с ноды MEV Geth. Они делают это, устраняя необходимость для ноды объединять и упорядочивать бандлы, чтобы клиент не выполнял эти вычисления, должен быть уровень доверия
Давайте посмотрим, какие шаги пропускаются, когда Megabundle фиксируется в блоке
- «commitNewWork», который обрабатывает заполнение блока транзакциями
- Существует оператор if, проверяющий, является ли полученный бандл Megabundle, если это так, Megabundle извлекается из TxPool и фиксируется в блоке. Выше строки 1220 в коде находится альтернативный путь, когда != Megabundle нода берет ванильные бандлы и объединяет их, как мы видели ранее
- Megabundle зафиксирован в блоке, обратите внимание, что нет «simulateBundles», «mergeBundles» или «computeBundleGas». Это уменьшает количество вычислений, которые необходимо выполнить ноде
- «commitBundle», в свою очередь, вызывает «commitTransaction»
- «commitTransaction» применяет транзакцию к состоянию, проверяя, не отменяется ли она, обновляя прибыль и возвращая receipt logs
Мы видим, что реализация Megabundles фокусируется на скорости и производительности
В v0.5 скорость и производительность были увеличены еще больше за счет скоростной обработки мегабандла, если он был лучше, чем самый известный блок на данный момент. Вы можете копаться в изменениях кода ТУТ
В последней версии клиента MEV Geth v0.6 были представлены приватные транзакции. Это позволило пользователям отправлять транзакции, которые будут связаны с remote транзакциями, но не будут отправлены одноранговым нодам в течение определенного периода времени
Эти частные транзакции не отправляются в «мемпул», где «мемпул» означает TxPool на других узлах Geth
Фронтранеры будут отсеивать прибыльные транзакции, которые они видят в мемпуле. Хотя бандл Flashbot защищает от этого, поскольку они не отправляются в мемпул, они записываются в API Flashbots
Searchers часто просматривают эти транзакции в поисках альфы. Searchers может захотеть замаскировать свой Tx, поместив его с другими remote транзакциями
MEV Boost
Как я упоминал ранее, мегабандлы были предшественниками полноблочных сборок и MEV Boost
В ETH 2 возникла концепция построителя блоков. Сущность, посвященная созданию оптимизированных блоков
В этом новом мире валидаторы (не майнеры) будут иметь право упорядочивать блоки и смогут подписаться на получение полных блоков от строителей для постановки на выделенные им слоты
Эти блоки будут слепыми — валидатор не сможет увидеть транзакции, пока блок не будет отправлен в сеть. Это не позволяет валидаторам украсть MEV для себя, снижая необходимое доверие между сторонами
MEV Boost будет дополнительным контейнером, соединяющим валидатор с ретранслятором, который, в свою очередь, подключатся к сети «строителей блоков»
Если вы хотите узнать больше, то это хорошее место для начала!